ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TouchGFX Mixin机制深入解析:实现控件动画与手势交互的优雅方案

TouchGFX Mixin机制深入解析:实现控件动画与手势交互的优雅方案 如果你在嵌入式 GUI 开发里用过 TouchGFX大概率会碰到这样一个场景界面上的某个控件既想让它能拖动又希望它在拖动结束之后自己弹回原位或者想让一个图片按钮按下时有缩放反馈同时又得响应点击业务。一开始我的做法很粗暴直接在子类里重写事件回调或者用定时器轮询位置。代码写多了之后会发现两个问题第一是大量重复逻辑第二是 TouchGFX Designer 每次重新生成代码手写部分容易和生成代码打架。后来仔细翻了一遍 ST 的应用笔记才把 Mixin 这套机制真正玩明白。这篇笔记就以 LAT1206TouchGFX 控件附加 Mixin 功能为核心完整梳理 Mixin 的设计思路、类型、实际操作步骤以及我踩过的几个坑。适合刚接触 TouchGFX 的开发者也适合已经在用但只停留在 Designer 自动生成层面的朋友读完你至少能把混入动画和手势交互这件事做得干净利落。1. 从控件到 Mixin重新理解 TouchGFX 的扩展机制1.1 Mixin 到底是什么Mixin 这个名字最早来自面向对象里的混入mix-in概念意思是不通过继承而是把一组现成的功能“混入”到另一个类中让目标类立即拥有这些能力。TouchGFX 里的 Mixin 也是这个思路它本质上是封装好的一组模板类由 TouchGFX 代码生成器在生成界面代码时把指定的 Mixin 作为父类之一插入到控件的继承链里。具体到代码层面比如你往一个 Button 上加MoveAnimator代码生成器不会去改动你的 ScreenView 子类而是在生成的ScreenNameViewBase里将这个 Button 的类型从Button变成MoveAnimatorButton。这样你的按钮就同时拥有了 Button 的绘制、点击能力以及 MoveAnimator 提供的动画移动接口。看到这里你可能会问为什么不直接自己写一个子类继承 Button再自己实现动画原因有两点。第一TouchGFX Designer 生成的代码中控件成员变量的类型是固定的如果你手改生成文件里的类型下次重新生成会被静默覆盖项目直接编译失败或者行为异常。Mixin 的好处在于你只需要在 Designer 里勾选生成器会替你解决类型包装的问题。第二多个 Mixin 可以叠加比如MoveAnimatorFadeAnimatorButton这种多层包装如果用继承来做你得写一堆中间类而 Mixin 是模板组合几层包装都不需要额外手写代码。1.2 Mixin 与普通继承到底怎么选做一个控件的扩展通常有三条路直接改控件子类重写draw()和handleXXXEvent()这适合对单个控件做深度定制但没法复用。用 Widget 子类继承把通用逻辑放到基类这是传统的面向对象思路但遇到 TouchGFX Designer 的代码生成机制容易和生成代码冲突。用 Mixin 混合这是 TouchGFX Designer 推荐的方案适合大多数“想给标准控件加点通用行为”的场景。以我自己的项目为例早期我做一个系统设置页面里面的滑块需要支持拖动调亮度我直接写了个SliderWidget子类在handleDragEvent里更新内部数值再调用invalidate()。第一版很顺利后来产品经理要求所有滑块不仅支持拖动还要在触摸松开后有一个平滑回弹动画我老老实实改了五六个页面代码改到第三处就开始意识到方案有问题。如果用 Mixin 来做DraggableSlider直接给滑块加拖动能力MoveAnimatorSlider提供回弹动画就不用每个页面各自实现一遍。所以我的结论是控件本身的绘制逻辑有较大改动时用继承控件的行为扩展尽量用 Mixin后者的维护成本低很多尤其适合界面迭代频繁的产品。1.3 代码生成器怎么处理 Mixin 的继承链要理解 Mixin 的生成机制可以看看 Scree 生成后的继承结构。假设你在 Designer 里给screenMain添加了一个Box并且给它加了MoveAnimator和ClickListener那么在生成的ScreenMainViewBase.cpp里大概是这个样子的MoveAnimatorClickListenerBox box1;ClickListenerBox先让 Box 能够识别点击事件MoveAnimator再在它的基础上扩展移动动画。TouchGFX 的模板设计保证了这些 Mixin 类的接口会“穿透”。什么意思也就是说你在代码里调用box1.setXY()、box1.setPosition()这些 Box 原生接口依然可以直接调用因为MoveAnimator内部隐式继承了ClickListenerBox的所有接口并外露给上层。注意TouchGFX Designer 在 4.15 版本之后更新了 Mixins 的添加方式界面更简洁但本质上没有变。新版里你选中控件后右边 Properties 面板就能看到 Mixins 区域点加号即可添加旧版4.14 及以前则通常需要右键控件选择添加 Mixin。后面实操部分我会把两种流程都讲一下。2. Mixin 有哪些类型分别用在什么场合2.1 交互类 Mixin让控件“活”起来交互类 Mixin 负责处理触摸和手势最常用的是下面这几个Mixin 名称能力典型应用场景ClickListener响应点击事件提供clicked信号点击一个图标触发页面跳转Draggable让控件可以跟随手指移动拖动滑块、拖动弹窗标题栏DragListener监听拖动事件但不处理移动做滑动操作时的方向判断GestureSupport处理手势识别如滑动、缩放图片查看器双指缩放这里要特别提醒一下ClickListener和Draggable如果同时加到同一个控件上是有冲突风险的。原因是触摸事件的判定机制里TouchGFX 在发送点击信号之前会先判断这个控件是否“可拖动”如果可拖动且手指发生了位移事件会被当作拖动处理点击信号可能丢失。所以我自己的经验是需要同时响应点击和拖动的控件比如列表项滑动删除不要直接叠加ClickListener和Draggable而是用DragListener自己判断是 tap 还是 drag。2.2 动画类 Mixin让界面过渡顺滑动画类 Mixin 是我用得最多的一类它解决的核心问题是“让控件的状态变化看起来自然”。常用成员包括MoveAnimator让控件平滑移动到新位置配合startMoveAnimation()等接口。FadeAnimator让控件透明度渐变实现淡入淡出。MoveAndFadeAnimator同时移动和改变透明度适合做界面切换时旧元素退场。ZoomAnimator缩放控件适合做弹窗弹出效果。AlphaAnimator单独控制 Alpha 值变化和FadeAnimator类似但更细粒度控制。这些动画类 Mixin 都要求控件的draw()实现支持对应属性。比如FadeAnimator依赖控件的 alpha 值但如果你控件使用的图片格式不支持 alpha 混合动画效果就会变成瞬间消失而不是渐变。后面第 4 章我会专门讲这种坑。拿MoveAndFadeAnimator举例实际项目中我用它做通知横幅的弹出和消失通知从顶部滑下来同时透明度从 0 变到 1点击关闭后透明度从 1 变到 0同时向上滑出。以前用AnimationManager自己管理代码要写好几个回调用 Mixin 之后代码量减少了一大半。2.3 特殊用途 MixinDrawable、Container 等除了上面两大类TouchGFX 还提供一些特殊用途的 Mixin我见过很多初学者一头雾水Drawable把任何控件变成可绘制对象最常见的是配合Canvas和Shape做自定义绘制。Container把一个控件包装成容器可以装子控件。这听起来简单但很实用比如你想把一个图片和一个文字打包成整体拖动直接给这个整体加Draggable就需要先把它们包进一个容器里再给容器加 Mixin。这类 Mixin 的使用场景比较窄但一旦用到就非常关键。我最深的一个体会是当界面层级复杂时直接给控件数组整体加动画不如先包一个Container再加MoveAnimator这样只需调一个对象的动画接口不用遍历数组逐个设置。3. 实操演示在 Designer 中附加 Mixin 并跑通交互3.1 新版 Designer 添加 Mixin 的完整步骤我当前项目用的 TouchGFX Designer 是 4.19界面变化已经和早期文档里的截图不太一样了但核心入口还在。以给一个 Button 添加“拖动 松手回弹”功能为例步骤如下。第一步新建一个 Screen比如叫ScreenDemo。从控件面板拖一个Button到画布上设置好图片资源。选中这个按钮右边属性面板往下翻找到 Mixins 区域点击加号选择Draggable然后再点一次加号选择MoveAnimator。第二步你会发现按钮在画布上依然显示为普通按钮这很正常Designer 的预览不一定能完全体现 Mixin 的效果。生成代码之后在工程里打开generated/screens/ScreenDemoViewBase.hpp找到按钮的声明你会看到类似这样的内容MoveAnimatorDraggableButton button1;这就说明 Mixin 已经正确附加了。第三步进入你的用户代码ScreenDemoView.cpp现在你的button1已经可以直接调用button1.startMoveAnimation(100, 100, 200, EasingEquations::cubicEaseOut);这会在 200 毫秒内把按钮从当前位置移动到坐标 (100, 100)缓动函数用的三次缓出运动末端比较柔和。3.2 旧版 Designer 的操作方式对比如果你的项目还在用 4.14 或更早的 Designer右击控件会有一个 “Add Mixin” 的菜单项点击之后会弹出一个小对话框里面列出所有可用的 Mixin勾选后点确定即可。旧版有个好处它会在画布上把 mixin 的图标显示在控件角上方便你一眼看出哪些控件加过 mixin。新版 4.15 之后这个图标没了所以很多老用户觉得新版反而不直观。实际上新版你可以在左下角的 Widget Tree 视图里看到每个控件名旁边多出来的一串模板前缀。比如MoveAnimatorDraggableButton会在树里显示成一个长名字按钮刚开始看不习惯但用惯之后反而觉得信息更清晰。3.3 代码侧怎么知道控件加了哪些 Mixin有些时候你需要在代码里动态判断一个控件是否支持某种能力尤其是界面元素是在代码里动态创建的时候。Mixin 类提供了对应的转换接口比如MoveAnimator提供了getMoveAnimator()方法返回一个接口指针。ClickListener提供了getClickListener()。Draggable提供了getDraggable()。实际用法是if (auto animator container.getMoveAnimator()) { animator-startMoveAnimation(newX, newY, duration); }这个判断接口在 C 里用的是dynamic_cast或者模板特化来实现好处是同一段代码可以兼容“是否加了 Mixin”两种控件类型写通用工具函数时非常顺手。举个例子我自己写过一个工具函数输入任意Widget引用自动尝试执行“移动到指定点并淡出”代码长这样bool fadeOutAndMove(Widget widget, int16_t x, int16_t y, uint16_t duration) { auto fade widget.getFadeAnimator(); auto move widget.getMoveAnimator(); if (fade move) { fade-startFadeAnimation(0, duration); move-startMoveAnimation(x, y, duration); return true; } return false; }注意getFadeAnimator()是FadeAnimator提供的接口不是FadeAnimator内部直接继承的调用时要用auto接收。如果你对模板代码不熟悉刚开始看到一长串类型名可能有点懵其实只要记住Mixin 都提供了对应的getXXX()接口返回的指针如果为空说明没加该 Mixin。3.4 业务联动Mixin 事件如何传到业务层Mixin 解决了控件能力的问题但业务逻辑最终还是得传到 Presenter / Model 这一层。以ClickListener为例加了 Mixin 后你可以在 View 里这样绑定点击事件button1.setAction(clickedCallback);或者用更自然一点的方式直接重写 View 的响应void ScreenDemoView::handleClickEvent(const ClickEvent event) { if (event.getType() ClickEvent::CLICKED) { // 业务处理 } }更常见的做法是点击按钮后把数据变化同步到 Model再通过presenter传到业务逻辑。例如button1.setAction([this]() { presenter-onButtonClicked(button1.getX(), button1.getY()); });这里presenter-onButtonClicked()是你需要在ScreenDemoPresenter里自己定义的方法和普通控件的事件处理没有区别Mixin 只是给了你更多触达事件的入口不会改变既有架构。4. 常见问题与排查技巧实录4.1 Mixin 加上了但代码没效果这是我在论坛里看到最多的问题。最常见的几个原因没有重新生成代码。Mixin 是通过代码生成器进入工程代码的你在 Designer 里加了之后一定要点一下右上角的 “Generate Code”然后再去 IDE 里编译。很多人在 Designer 里加完 mixin 直接切到 IDE 编译编译的还是上一次生成的代码。生成后被generated目录缓存。TouchGFX 的代码生成器有时因为文件时间戳问题没有重新生成最新代码稳妥做法是删除generated/gui_generated目录下的对应文件重新 Generate Code。如果还不行就直接把整个generated目录删掉重新生成。代码放在用户文件被覆盖。很多人误以为 Mixin 加完之后要在生成的ViewBase里手动改代码结果改了generated下的文件下次生成被覆盖就以为没有生效。正确做法是只在View.cpp和View.hpp用户文件里写逻辑。4.2 ClickListener 和 Draggable 同时加导致点击失灵前文提到过这个冲突这里详细展开。TouchGFX 的触摸事件传递链条是Screen收到触摸事件后遍历子控件把事件交给命中的控件。如果控件同时具备ClickListener和Draggable它会先尝试作为 draggable 处理事件当手指移动超过一定阈值时就标记为拖动导致ClickListener的 click 事件不会被触发。我的排查经验是先确定需求优先级。如果必须同时支持点击和拖动改用DragListener自己实现 tap 阈值判断。如果只是拖动结束时有一个“松手回弹”效果那不需要ClickListener只需要在DragListener的回调里检测拖动的终点并启动MoveAnimator。具体代码模板如下void ScreenDemoView::handleDragEvent(const DragEvent event) { if (event.getType() DragEvent::RELEASED) { // 松手回弹回到初始位置 button1.startMoveAnimation(initialX, initialY, 150, EasingEquations::cubicEaseOut); } }这样既保证了拖动又不牺牲点击。关键点是按压后拖动阈值对比判断要交给DragListener而不是完全依赖事件类型毕竟手指轻微抖动也会产生小幅拖动事件。4.3 动画和拖动同时进行时的相互打断问题产品经理经常提这样的需求弹窗可以拖动但松手后自动吸边拖动过程中如果再点关闭按钮弹窗还要能平滑收起。这个需求看似平淡做起来却有一个很微妙的问题在MoveAnimator动画进行过程中用户手指触摸弹窗动画还在持续更新位置而拖拽事件也在设置新位置两个动画系统会对同一个坐标属性写值导致弹窗抖动。我用的处理方案是把动画和拖动互斥。具体做法在开始拖动时调用button1.cancelMoveAnimation()把动画先取消掉。在松手启动吸边动画之前先把控件当前的触摸状态标记为可拖动再启动动画。如果你用的 Mixin 版本比较旧可能会发现cancelMoveAnimation()不存在。旧版本里你需要用AnimationManager::stopAnimation()来手动停止。TouchGFX 4.18 之后Mixin 接口日趋完善cancelMoveAnimation()已经比较稳定了。实测下来动画与拖动互斥逻辑最好放在 View 统一管理不要散落到各种回调里否则很难排查。4.4 图片透明度动画不生效的显示问题FadeAnimator用得好不好和图片格式关系非常大。TouchGFX 里如果图片是 L88 位索引色且调色板没有 alpha 通道做透明度动画时整个图片的 alpha 值会作用于整张图但这张图本身的透明像素会先和背景混合视觉上要么显得生硬要么出现边缘发黑。ARGB8888 格式是最稳的支持逐像素 alpha但代价是 Flash 占用更大。我的建议是需要做淡入淡出效果的重要 UI 元素一律用 ARGB8888 或 A4 格式并关闭调色板透明优化。另外如果目标是实现整个页面级联淡入淡出不要给每个控件单独加FadeAnimator而是给容器加一个然后容器内所有子控件统一受影响。这样性能开销小动画效果也更统一。4.5 Mixin 和目标类不匹配导致的编译错误MoveAnimator要求目标类是Widget的子类Container这个 Mixin 则需要你的目标控件能够作为容器承载子控件。如果你给一个TextArea强行加Container生成代码后编译会报错原因是TextArea不满足模板类的接口要求。这类错误在编译信息里通常会指向模板定义那一大段初学者看到容易懵。我的排查方法是先看编译错误里是否指向touchgfx/Mixin.hpp或具体控件头文件如果是先去 Designer 里把 Mixin 的勾选取消确认编译一旦恢复说明就是 Mixin 类型和控件类型不匹配。这种情况下解决方案通常是先包一层Container再给 Container 加 Mixin。5. 我的一些补充心得与建议5.1 用 Mixin 之前先想清楚逻辑分层Mixin 再好也不能滥用。我见过有的项目把一个按钮同时加了Draggable、MoveAnimator、FadeAnimator、ClickListener、ZoomAnimator结果整个按钮的交互行为非常混乱拖动、点击、松手动画、缩放动画同时触发几乎没法调。我的个人原则是一个控件最多叠加两个交互类 Mixin 一个动画类 Mixin。超过这个数量建议你拆成多个子控件组合。比如说一个卡片要支持拖动、点击、删除时缩小动画那就拆成卡片本身只加Draggable卡片内的删除按钮单独加ClickListener和ZoomAnimator而不是让卡片一个对象同时对所有事件负责。5.2 版本升级后旧工程的处理如果是 TouchGFX 4.14 升级到 4.18老工程里直接通过代码方式添加的 Mixin 在新版本中接口有些变化主要体现在getMoveAnimator()等辅助接口的返回类型更严格了。我升级完第一个遇到的就是setTouchable(true)的调用上旧代码里很多控件靠这个来开启控制交互新版如果用了ContainerMixin需要额外调用setTouchable的地方会更多。建议升级前先把工程里所有手动new MoveAnimator...的地方列出来统一评估是否迁移到 Designer 的 Mixins 面板。如果只是临时使用升级后保留代码方式也可以但长期看还是集中到 Designer 里维护比较省心。5.3 从社区和官方资料中筛选有用信息ST 官方有一个 LAT 系列应用笔记LAT1206 这份是专门讲 TouchGFX Mixin 的英文版本更全中文翻译版可能滞后。如果想系统学官方还有一个专门讲Mixins的博客文章图文并茂非常推荐。社区里关于 Mixin 的讨论比较零散搜的时候可以用TouchGFX Mixin Draggable、TouchGFX FadeAnimator这类关键词精确查找。我自己的习惯是先通过一个小 Demo 把各个 Mixin 都试一遍再去看源码里对应模板类是怎么实现的这样理解最深刻。TouchGFX 的源码全公开只看不练容易眼高手低。5.4 未来扩展方向Mixin 不仅仅是给现有控件加能力也可以自己写模板类做自定义 Mixin只要你定义好接口和 TouchGFX 的模板机制匹配就能在 Designer 中作为自定义 Mixin 使用需要额外配置 XML 描述文件。这个玩法比较深适合团队中已经有了一定 TouchGFX 积累后去尝试。我目前还在探索阶段等跑通了再单独写一篇。6. 最后想说的话回过头来看 LAT1206 这份应用笔记它本质上讲的不只是几个模板类的用法而是 TouchGFX 里一种可复用的控件能力组合思想。以前写界面代码总觉得控件行为应该是类继承树的一部分后来真正用熟了 Mixin才意识到组合往往比继承更灵活也更好维护。我踩过最大的坑就是图省事总想一个控件搞定所有事最后被各种事件冲突折磨得够呛。调试 Mixin 相关的问题最有效的办法是先从 Designer 出发确认生成代码确实包含了预期的 Mixin 类型再去代码侧排查事件和动画逻辑。千万不要一上来就怀疑是 TouchGFX 生成器的 bug绝大多数情况都是我自己逻辑没理清。如果你正在做 TouchGFX 项目建议花一个下午的时间单纯搭一个测试页把Draggable、MoveAnimator、FadeAnimator、ClickListener几个最常用的 Mixin 都拖进去过一遍看看生成的代码长什么样再试试事件组合。这样真正做产品界面时心里就有底了。这个测试页我到现在还留着每次新版本 TouchGFX 发布我都会打开重新生成一遍验证相关行为没有变化也算是一个低成本的高效实践。
返回列表