2、自定义控件基础:继承Drawable类、重写draw()方法、处理invalidate()

好,咱们正式开始动手了。

上一章我们聊了TouchGFX的控件体系,知道了所有可见控件都继承自Drawable。那问题来了——如果官方控件库满足不了你的UI需求,怎么办?

答案很简单:自己写一个。

这一章,我就带你走一遍自定义控件最核心的三个步骤:继承Drawable、重写draw()、处理invalidate()。说白了,这就是自定义控件的"三板斧"。掌握了它们,你就能在TouchGFX里画出任何你想要的东西。

2.1 为什么非要继承Drawable?

你可能会有疑问:我直接用Container或者Image不行吗?

行,但有限制。Container本身不绘制任何内容,它只是装子控件的盒子。Image只能显示位图。如果你想要一个动态绘制的控件——比如一个实时波形图、一个自定义进度环、或者一个手写签名板——那你就必须直接继承Drawable。

核心概念:Drawable是所有可绘制对象的基类。它定义了控件的"几何存在"(位置和大小)和"绘制契约"(draw方法)。任何能在屏幕上显示的东西,最终都是Drawable。

我个人习惯把Drawable理解成一张"画布"。你继承它,就等于拿到了一张空白画布。画多大、画什么、怎么画,全由你决定。

2.2 第一步:继承Drawable并实现构造函数

先看一个最简单的例子。假设我们要做一个圆形指示灯——就是那种绿了表示正常、红了表示报警的小圆点。

#include <touchgfx/Drawable.hpp>

class LedIndicator : public touchgfx::Drawable
{
public:
    LedIndicator()
        : Drawable(), ledColor(touchgfx::Color::getColorFrom24BitRGB(0, 255, 0))
    {
        // 设置控件尺寸为40x40像素
        setWidth(40);
        setHeight(40);
    }

    virtual void draw(const touchgfx::Rect& invalidatedArea) const override;

    void setColor(touchgfx::colortype color)
    {
        ledColor = color;
        // 颜色变了,必须重绘
        invalidate();
    }

private:
    touchgfx::colortype ledColor;
};

注意几个关键点:

  • 构造函数里必须调用setWidth()和setHeight()。不设尺寸,TouchGFX不知道这个控件占多大区域,绘制时会被裁剪掉。
  • 成员变量ledColor保存当前颜色。这是控件的"状态"——任何自定义控件都需要维护自己的状态数据。
  • setColor()里调用了invalidate()。这个后面细说,你先记住:状态变了,就要通知框架重绘。

小技巧:我建议你在构造函数里就把控件的默认尺寸设好。曾经有个同事忘了设宽度,结果控件画出来了但看不见,排查了半天才发现是宽度为0。嗯,这种坑踩过一次就记住了。

2.3 第二步:重写draw()方法——真正的绘制逻辑

draw()是自定义控件的灵魂。框架会在每一帧需要重绘时调用它,你在这里用TouchGFX的Canvas或PainterAPI画东西。

先看我们的圆形指示灯怎么画:

void LedIndicator::draw(const touchgfx::Rect& invalidatedArea) const
{
    // 获取画布
    touchgfx::Canvas canvas(this, invalidatedArea);

    // 计算圆心和半径
    int16_t centerX = getWidth() / 2;
    int16_t centerY = getHeight() / 2;
    int16_t radius = MIN(centerX, centerY) - 2;  // 留2像素边距

    // 画一个实心圆
    canvas.fillCircle(centerX, centerY, radius, ledColor);
}

等等,这里有个参数invalidatedArea。它是什么意思?

这个参数告诉你的draw()方法:屏幕上有哪一块区域需要重绘。TouchGFX不会每次都重绘整个控件,它只重绘"脏了"的那部分。这是一种性能优化手段。

但在我们这个例子里,整个圆形都可能需要重绘,所以直接用Canvas的裁剪机制就好——它会自动把绘制限制在invalidatedArea范围内。

注意:draw()方法被声明为const。这意味着你不能在draw()里修改控件的成员变量。所有需要改变的状态,必须在draw()之前通过invalidate()触发重绘时准备好。我曾经见过新手在draw()里改颜色,结果编译报错一脸懵——嗯,const限定符就是防你这个的。

2.4 第三步:理解invalidate()——重绘的触发器

invalidate()是Drawable提供的一个方法。它的作用很简单:告诉框架"我变脏了,下一帧请重绘我"。

什么时候需要调用它?

  • 控件的外观状态发生变化时(比如颜色、文本、透明度)
  • 控件的尺寸或位置发生变化时
  • 控件的内部数据更新后需要反映到界面上时

看一个更完整的例子——带闪烁效果的指示灯:

class BlinkingLed : public touchgfx::Drawable
{
public:
    BlinkingLed()
        : Drawable(), isOn(true), tickCounter(0)
    {
        setWidth(40);
        setHeight(40);
    }

    void handleTickEvent()  // 由框架每帧调用
    {
        tickCounter++;
        if (tickCounter >= 30)  // 30帧切换一次(约0.5秒@60fps)
        {
            tickCounter = 0;
            isOn = !isOn;
            invalidate();  // 状态变了,请求重绘
        }
    }

    virtual void draw(const touchgfx::Rect& invalidatedArea) const override
    {
        touchgfx::Canvas canvas(this, invalidatedArea);
        int16_t cx = getWidth() / 2;
        int16_t cy = getHeight() / 2;
        int16_t r = MIN(cx, cy) - 2;

        if (isOn)
        {
            canvas.fillCircle(cx, cy, r, touchgfx::Color::getColorFrom24BitRGB(0, 255, 0));
        }
        else
        {
            // 熄灭状态画一个灰色圆
            canvas.fillCircle(cx, cy, r, touchgfx::Color::getColorFrom24BitRGB(100, 100, 100));
        }
    }

private:
    bool isOn;
    int tickCounter;
};

你看,invalidate()的调用时机非常关键。调少了,界面不更新;调多了,性能浪费。我个人经验是:只在状态真正改变时才调invalidate()。比如上面的闪烁逻辑,如果isOn没变,就不调。

2.5 三者的协作流程

为了让你更直观地理解这三者的关系,我画了一张流程图:

自定义控件绘制流程 控件状态发生变化 调用 invalidate() 框架标记控件区域为"脏" 下一帧:框架调用 draw() 绘制完成,显示更新 关键原则: • 状态变 → invalidate() • invalidate() → draw() • draw() 内只读不写

这个流程你仔细看一遍:状态变化 → 调invalidate() → 框架标记脏区域 → 下一帧调draw() → 绘制完成。环环相扣,缺一不可。

2.6 常见踩坑与避坑指南

做自定义控件时,有几个坑我几乎每次培训都会提到:

常见问题 原因 解决方案
控件画了但看不见 忘记设宽高,或宽高为0 构造函数里调用setWidth()/setHeight()
状态变了但界面不更新 忘记调invalidate() 每次状态变化后立即调invalidate()
绘制时崩溃 draw()里访问了未初始化的成员 确保所有成员在draw()前已初始化
性能卡顿 每帧都调invalidate(),导致全屏重绘 只在状态真正改变时调invalidate()

我曾经踩过的坑:有一次做仪表盘控件,指针每帧都动,我就在handleTickEvent()里无条件调invalidate()。结果整个屏幕每帧都全量重绘,帧率直接掉到20fps。后来改成只在新角度和旧角度不同时才调invalidate(),帧率立刻回到60fps。嗯,invalidate()虽好,可不要贪杯。

2.7 本章小结

来,我们捋一下这章的核心要点:

  • 继承Drawable:拿到空白画布,设好宽高,定义自己的状态数据。
  • 重写draw():在这里用Canvas API画东西。记住它是const方法,别想着改状态。
  • 处理invalidate():状态变了就调它,让框架知道该重绘了。别多调,也别少调。

这三板斧看着简单,但它们是所有自定义控件的基石。后面章节里那些炫酷的圆形菜单、滑动选择器、自定义键盘,本质上都是在这个框架上叠加更复杂的绘制逻辑和交互处理。