4、自定义按钮:从Button类继承、自定义皮肤、状态管理
按钮,是嵌入式GUI里最常用的控件。没有之一。
TouchGFX自带的Button类能满足大部分常规需求——点击、弹起、触发回调。但实际项目里,你总会遇到一些「特殊要求」:比如按钮要显示动态图标、按下时要有呼吸灯效果、或者需要同时响应长按和短按。这时候,就得自己动手了。
我个人习惯,只要按钮行为超出标准控件的范围,就毫不犹豫地写子类。别怕继承,TouchGFX的控件体系设计得挺清爽,扩展起来并不复杂。
4.1 为什么需要自定义按钮?
先看几个我在项目中遇到的真实场景:
- 场景一:设备需要一个「录音按钮」,按下时显示红色圆点+波纹扩散动画,松开后恢复静态图标。标准Button做不到。
- 场景二:工业平板上有「模式切换键」,需要根据当前模式显示不同颜色的皮肤(绿色=运行、黄色=待机、红色=报警)。标准Button只能换图片,不能联动状态机。
- 场景三:需要检测「长按超过2秒」触发特殊功能,短按触发普通功能。标准Button只提供clicked回调。
说白了,标准Button是「通用件」,而你的产品需要「专用件」。自己写一个,一劳永逸。
4.2 从Button类继承
TouchGFX里所有控件都继承自Drawable。Button本身继承自ButtonBase,而ButtonBase又继承自Widget。我们自定义时,直接继承Button是最省力的方式——它已经帮你处理了触摸检测、回调触发等底层逻辑。
来看一个最简单的自定义按钮头文件:
#ifndef MY_CUSTOM_BUTTON_HPP
#define MY_CUSTOM_BUTTON_HPP
#include <touchgfx/widgets/Button.hpp>
class MyCustomButton : public touchgfx::Button
{
public:
MyCustomButton();
virtual ~MyCustomButton() {}
// 重写绘制方法
virtual void draw(const touchgfx::Rect& invalidatedArea) const override;
// 重写触摸处理
virtual void handleClickEvent(const touchgfx::ClickEvent& event) override;
// 新增接口:设置自定义数据
void setCustomData(uint8_t data);
private:
uint8_t customData;
bool isLongPress;
};
#endif
嗯,这里要注意:重写draw()方法时,一定要调用父类的draw(),除非你打算完全自己画。我刚开始做的时候忘了调,结果按钮背景一片黑,排查了半天才发现。
draw()里先调用Button::draw(),再用Canvas或Renderer画额外内容。这样既保留了原有皮肤,又实现了扩展。
4.3 自定义皮肤:不只是换图片
标准Button的皮肤通过setBitmaps()设置,本质上就是两张图片(按下/弹起)。但自定义按钮可以玩得更花哨。
我习惯把皮肤拆成三层:
| 层次 | 内容 | 控制方式 |
|---|---|---|
| 底层 | 背景色/背景图 | 状态机控制 |
| 中层 | 图标/文字 | 业务逻辑控制 |
| 顶层 | 特效(阴影、高光、动画) | 定时器控制 |
举个例子,实现一个「按下时背景变暗、图标变亮」的按钮:
void MyCustomButton::draw(const touchgfx::Rect& invalidatedArea) const
{
// 先画背景
if (getPressedState())
{
// 按下状态:深色背景
touchgfx::Renderer::getInstance().fillRect(
getRect(), touchgfx::Color::getColorFrom24BitRGB(0x33, 0x33, 0x33));
}
else
{
// 弹起状态:浅色背景
touchgfx::Renderer::getInstance().fillRect(
getRect(), touchgfx::Color::getColorFrom24BitRGB(0x66, 0x66, 0x66));
}
// 再画图标(假设图标是24x24的白色位图)
// 这里可以按状态切换不同颜色的图标
touchgfx::Bitmap icon = getPressedState() ?
touchgfx::Bitmap(BITMAP_ICON_PRESSED_ID) :
touchgfx::Bitmap(BITMAP_ICON_RELEASED_ID);
touchgfx::Renderer::getInstance().drawBitmap(
icon,
getX() + (getWidth() - 24) / 2,
getY() + (getHeight() - 24) / 2,
24, 24,
0, // alpha
touchgfx::Bitmap::VS_NORMAL);
}
draw()里直接调用Renderer的绘图API时,一定要确保坐标是相对于屏幕的绝对坐标。我踩过坑——直接用getX()/getY()没问题,但如果按钮被嵌套在容器里,需要加上容器的偏移量。建议用getAbsoluteRect()获取绝对位置。
4.4 状态管理:从两态到多态
标准Button只有两种状态:pressed和released。但实际项目中,按钮往往需要更多状态:
- 禁用态:灰色显示,不可点击
- 选中态:高亮边框,表示当前选项
- 加载态:显示转圈动画,表示操作进行中
- 错误态:红色闪烁,表示操作失败
我设计了一个通用的状态枚举:
enum class ButtonState : uint8_t
{
NORMAL = 0,
PRESSED = 1,
DISABLED = 2,
SELECTED = 3,
LOADING = 4,
ERROR = 5
};
然后在自定义按钮里维护一个currentState变量。每次状态变化时,调用invalidate()触发重绘。你想想看,这样设计的好处是什么?——皮肤和状态完全解耦了。你想加一个新状态,只需要在枚举里加一项,然后在draw()里加一个case分支就行。
我曾经在一个医疗设备项目里,给按钮设计了7种状态,每种状态对应不同的颜色和图标组合。后期客户要求增加「待确认」状态,我只花了10分钟就改完了。这就是状态管理设计得好的回报。
4.5 完整示例:带状态管理的自定义按钮
下面是一个完整的实现片段,包含了状态切换和皮肤更新:
void MyCustomButton::setState(ButtonState newState)
{
if (currentState == newState)
return; // 状态没变,不处理
currentState = newState;
// 根据状态更新外观
switch (currentState)
{
case ButtonState::NORMAL:
setTouchable(true);
setBitmaps(Bitmap(BITMAP_BTN_NORMAL_ID), Bitmap(BITMAP_BTN_PRESSED_ID));
break;
case ButtonState::DISABLED:
setTouchable(false);
setBitmaps(Bitmap(BITMAP_BTN_DISABLED_ID), Bitmap(BITMAP_BTN_DISABLED_ID));
break;
case ButtonState::SELECTED:
setTouchable(true);
setBitmaps(Bitmap(BITMAP_BTN_SELECTED_ID), Bitmap(BITMAP_BTN_SELECTED_PRESSED_ID));
break;
case ButtonState::LOADING:
setTouchable(false);
// 加载态使用动画位图,由定时器驱动
startLoadingAnimation();
break;
default:
break;
}
// 触发重绘
invalidate();
}
setState()里启动一个200ms的渐变动画,让颜色平滑过渡。TouchGFX的EasingEquations类提供了现成的缓动函数,用起来很方便。
4.6 知识体系总览
下面这张图概括了本章的核心逻辑:
4.7 避坑指南
最后分享几个我踩过的坑,希望能帮你省点时间:
- 坑一:忘记调用父类构造函数。自定义按钮的构造函数里,一定要先调用
Button(),否则内部的状态机没初始化,点击没反应。 - 坑二:在draw()里做耗时操作。
draw()是在渲染循环里调用的,频率很高。千万别在里面做图片解码、内存分配之类的操作。我曾经在draw()里加载了一张PNG图片,结果帧率直接掉到10fps。 - 坑三:状态切换后忘记invalidate()。改了状态、换了皮肤,但屏幕没刷新——这种问题排查起来特别隐蔽。我建议在
setState()末尾强制调用invalidate(),养成习惯。 - 坑四:触摸事件处理不当。重写
handleClickEvent()时,如果你拦截了事件但没有调用父类方法,按钮的clicked回调就不会触发。要么自己处理完整的事件链,要么在最后调用Button::handleClickEvent(event)。