3. 复杂界面架构设计原则

各位同学,欢迎来到第三章。前两章我们把TouchGFX的环境搭好,也跑通了第一个Demo。但说实话,那个Demo里的界面太素了,离真正的产品还差得远。

从这一章开始,我们要深入拆解TouchGFX的UI组件。说白了,就是搞清楚那些按钮、文字、图片到底是怎么工作的。我个人习惯,学任何框架都要先把基础组件吃透,不然后面做复杂界面肯定会踩坑。

本章核心:掌握Button、TextArea、Image、Box、Circle五个基础组件的属性配置、样式定制和状态管理,并学会封装自己的组件。

3.1 基础组件全家福

TouchGFX的组件体系,其实就围绕几个核心类展开。我刚开始接触时也觉得组件很多,但后来发现,80%的界面都是用下面这五个组件拼出来的。

组件 类名 核心用途 我常用的场景
按钮 Button 触发交互事件 菜单、确认、翻页
文本区域 TextArea 显示静态/动态文字 标题、数值、提示
图片 Image 显示位图资源 图标、背景、产品图
矩形框 Box 纯色填充区域 背景色块、分割线
圆形 Circle 圆形/弧形绘制 仪表盘、指示灯

嗯,这里要注意:Box和Circle都是纯绘制组件,它们不处理触摸事件。如果你想做一个可点击的圆形按钮,得用Circle配合ClickListener,或者直接用自定义按钮。

3.2 组件的属性与样式定制

每个组件都有自己的一套属性。我习惯把它们分成三类:位置尺寸、视觉样式、行为配置。

3.2.1 位置与尺寸

所有组件都继承自Drawable,所以都有x、y、width、height四个基本属性。设置方式很简单:

// 创建一个按钮,放在 (10, 20) 位置,宽100,高50
Button button;
button.setXY(10, 20);
button.setWidth(100);
button.setHeight(50);

我在项目中遇到过一个问题:有些同学喜欢在构造函数里直接传参,比如Button(10, 20, 100, 50)。但TouchGFX的Button构造函数不支持这种写法,必须用set方法。这个坑我踩过一次,后来就记住了。

3.2.2 视觉样式定制

样式的定制,是让界面好看的关键。每个组件都有自己的专属样式属性:

  • Button:支持多张位图切换(按下/释放/禁用),通过setBitmaps()设置
  • TextArea:字体、颜色、对齐方式,通过TypedText和setColor()控制
  • Image:只能设置位图,通过setBitmap(),支持缩放模式
  • Box:纯色填充,通过setColor()和setAlpha()
  • Circle:填充色、描边色、描边宽度、起始角度、弧长

我的小技巧:TextArea的文字颜色不要直接用纯黑或纯白。我一般用#333333或#F5F5F5,视觉上更柔和。尤其是白色背景上的黑色文字,用#333比#000舒服很多。

3.2.3 实战:定制一个带阴影的Box

Box虽然简单,但配合透明度可以实现很多效果。比如做一个带阴影的卡片背景:

// 主背景
Box cardBackground;
cardBackground.setPosition(20, 20, 200, 150);
cardBackground.setColor(Color::getColorFrom24BitRGB(255, 255, 255));

// 阴影层(半透明黑色,偏移5像素)
Box shadow;
shadow.setPosition(25, 25, 200, 150);
shadow.setColor(Color::getColorFrom24BitRGB(0, 0, 0));
shadow.setAlpha(30);  // 透明度 30/255

// 注意:阴影要先添加,再添加主背景
// 这样阴影在底层,主背景在上层覆盖

你想想看,就两个Box叠加,一个卡片效果就出来了。很多看似复杂的UI,拆开来看都是这种基础组件的组合。

3.3 组件的状态管理(Pressed / Released)

状态管理是交互组件的灵魂。Button有四种状态:RELEASED、PRESSED、DISABLED、DISABLED_PRESSED。但最常用的就是Pressed和Released。

3.3.1 Button的状态切换机制

TouchGFX的Button内部维护了一个状态机。当手指按下时,自动切换到PRESSED状态并重绘;抬起时回到RELEASED状态。我们只需要提供不同状态的位图即可:

// 设置按钮的四种状态位图
button.setBitmaps(
    Bitmap(BITMAP_BTN_RELEASED_ID),   // 释放态
    Bitmap(BITMAP_BTN_PRESSED_ID),    // 按下态
    Bitmap(BITMAP_BTN_DISABLED_ID),   // 禁用态
    Bitmap(BITMAP_BTN_DISABLED_PRESSED_ID) // 禁用按下态
);

我曾经犯过一个低级错误:只设置了释放态和按下态的位图,没设置禁用态。结果按钮禁用时显示的是最后一张位图,画面直接崩了。所以,要么四张都配齐,要么至少保证前两张。

3.3.2 自定义组件的状态管理

如果你用Box或Circle做自定义按钮,就需要手动管理状态。我的做法是重写handleClickEvent和handleClickEventEnd:

class MyCustomButton : public Box {
public:
    MyCustomButton() : isPressed(false) {
        setColor(Color::getColorFrom24BitRGB(100, 150, 200));
    }

    virtual void handleClickEvent(const ClickEvent& event) {
        if (event.getType() == ClickEvent::PRESSED) {
            isPressed = true;
            setColor(Color::getColorFrom24BitRGB(80, 120, 180)); // 按下变暗
            invalidate(); // 触发重绘
        } else if (event.getType() == ClickEvent::RELEASED) {
            isPressed = false;
            setColor(Color::getColorFrom24BitRGB(100, 150, 200)); // 恢复
            invalidate();
            // 在这里触发点击回调
            if (clickAction) clickAction->execute(*this);
        }
        Box::handleClickEvent(event);
    }

private:
    bool isPressed;
};

注意:手动管理状态时,一定要调用invalidate()。否则界面不会刷新,你按下去啥变化都没有。我调试时经常忘了这步,盯着屏幕看了半天才发现。

3.4 自定义组件封装

当你的项目里有多个地方用到同一个UI模式时,就该封装了。比如一个带图标的菜单按钮,如果每次都重新拼,代码会变得又臭又长。

3.4.1 封装一个带图标的按钮

我习惯把自定义组件写成单独的类,继承自Container。Container可以容纳多个子组件,方便组合:

class IconButton : public Container {
public:
    IconButton() {
        // 初始化背景
        background.setPosition(0, 0, 80, 80);
        background.setColor(Color::getColorFrom24BitRGB(240, 240, 240));
        background.setRadius(10); // 圆角
        add(background);

        // 初始化图标
        icon.setXY(20, 20);
        icon.setBitmap(Bitmap(BITMAP_DEFAULT_ICON_ID));
        add(icon);

        // 设置容器大小
        setWidth(80);
        setHeight(80);
    }

    void setIcon(BitmapId newIcon) {
        icon.setBitmap(newIcon);
        icon.invalidate();
    }

    void setBackgroundColor(colortype color) {
        background.setColor(color);
        background.invalidate();
    }

private:
    Box background;
    Image icon;
};

封装好之后,在界面里用起来就一行:

IconButton myButton;
myButton.setXY(50, 100);
myButton.setIcon(BITMAP_SETTINGS_ICON_ID);
add(myButton);

你看,代码瞬间清爽了。而且如果后期要改样式,只需要改IconButton类内部,所有用到的地方自动更新。

3.4.2 封装时的注意事项

  • 记得调用add():子组件必须通过add()添加到Container中,否则不会显示
  • 设置容器大小:Container本身不自动调整大小,需要手动setWidth/setHeight
  • 暴露必要的接口:不要把所有成员都公开,只暴露需要外部调用的方法

3.5 本章知识体系

下面这张图是我自己整理的,把本章的核心知识点串了起来。你可以把它当作一个思维导图来用:

第三章 知识体系:UI组件深度解析(上) 基础组件体系 五大基础组件 Button / TextArea Image / Box / Circle 属性与样式定制 位置尺寸 (setXY) 颜色/位图/透明度 状态管理 Pressed / Released Disabled 状态 自定义组件封装 继承 Container 组合子组件 暴露接口 / 封装内部逻辑 💡 学习路径建议 先掌握单个组件 → 再理解状态切换 → 最后封装复用

3.6 避坑指南与经验总结

最后,分享几个我实际项目中踩过的坑,希望能帮你少走弯路:

坑1:位图资源没添加到资源组

我在一个项目里折腾了半天,按钮就是不显示图片。后来发现是位图没加到assets/images目录下的资源组里。TouchGFX Designer里必须把图片拖到对应的资源组,代码里才能引用到。

坑2:TextArea的文字显示不全

TextArea的宽度是固定的,如果文字太长会被截断。我习惯在设置文字后调用textArea.getTextWidth()检查一下宽度,必要时动态调整容器大小。

我的习惯:所有自定义组件都放在custom_components文件夹里,每个组件一个.h和一个.cpp。这样项目结构清晰,后期维护也方便。