查看: 66|回复: 3

Flutter AnimatedBuilder鸿蒙适配:动画拆分与Ark

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
在鸿蒙生态下用Flutter做动画时,我一开始用AnimationController搭配setState刷帧,虽然能跑起来,但每个动画都在build里监听Controller值变化并调setState,导致UI树层层嵌套,性能上不去。后来改用AnimatedBuilder,发现它专治这个痛点。

AnimatedBuilder的核心思路是把动画逻辑和渲染拆开。你不需要在build里写addListener+setState,它内部自动监听Animation变化,每帧只rebuild builder里的部分,外层组件不动,比手动setState刷整棵树效率高得多。核心参数有三个:animation(监听对象)、builder(重建回调)、child(不变部分)。

关键在于child参数——传给child的组件只build一次,builder每帧重建时child不会重建。如果你有不随动画变化的大子树,塞进child里能省不少重建开销。比如logo图标外面包了三层Container+Padding+DecoratedBox,这些在动画过程中根本不变,放child里builder只管套Transform,帧率能从50拉到60。尤其是列表里每个item都有自己的动画时,这个优化差别很明显。

AnimatedBuilder内部其实是AnimatedWidget的封装,只不过AnimatedWidget需要你继承它写一个单独的类,而AnimatedBuilder用一个builder回调就搞定,省得为每个动画建新类。

【无限旋转例子】
拿最基础的旋转来说——一个导航图标无限转圈,显示加载中。代码中注意child参数:Container和Icon是不变的,塞进child里。Transform.rotate每帧都跑,但child只build一次。如果反过来把Container放builder里每帧重建,虽然效果一样,但Dirty标记会多很多。这里有个小细节:我用2*3.14159而不是2*pi,其实dart:math里有pi常量,写3.14159纯粹为了方便阅读,自己项目里用pi更精确。

【脉冲呼吸例子】
做一个心跳图标,既要缩放又要变透明度。以前的做法是搞两个Controller分别驱动缩放和透明度,再在setState里同步。用AnimatedBuilder直接在一个builder里完成。repeat(reverse: true)让Controller在0和1之间来回跑,呼吸效果就是放大到1.3倍的同时透明度降30%,再反向回来。一开始我没用reverse,而是repeat到末尾再从头开始,结果每次循环末尾到开头之间有个跳跃,查文档才发现repeat有reverse参数。

【弹跳例子】
点一下按钮组件弹一次,用到了CurvedAnimation套曲线。forward(from: 0)很重要——确保每次点击都从0开始。如果动画还在播放时又点了一下,不加from:0的话Controller会在当前值基础上继续forward,看起来就像弹到半空又往上弹。有个踩坑点:CurvedAnimation在builder里创建了两次——一次传给animation参数,一次用来取value。可以抽出来放到外面创建,然后builder里用那个实例的value取值。不过要注意CurvedAnimation的parent不能随便换,最好在initState里初始化完就不动了。

【路径运动例子】
让组件沿着圆形路径运动,核心思路是用sin/cos算坐标。原理就是用一个角度值配合圆的参数方程算出x、y坐标,然后通过Transform.translate把组件移到对应位置。Container宽高各50,所以offset减了25来做居中偏移。有个横向限宽的坑:父容器设了height:200但没设width,导致在ListView里圆形轨迹显示不全,后来加个SizedBox(width:300)就好了。如果想做椭圆,改一下半径公式即可;正弦波路径把x用Controller线性映射,y用sin算。复杂贝塞尔曲线路径用Flutter的PathMetric配合CustomPainter会更省力。

【组合动画例子】
一个Controller驱动旋转+缩放+位移+透明度同时变化,AnimatedBuilder真正的优势在这里体现。一个widget同时做四件事:从左往右移动250像素、旋转完整一圈、从0.5倍缩放到原始大小、透明度从0.3变为1.0。全都是用一个_combinedCtrl驱动,Controller从0到1变化,所有属性同步推进。如果把这些变换拆成独立的方法反而增加阅读成本,Transform和Opacity都是轻量组件,嵌套就嵌套了。

对比鸿蒙ArkTS的做法:属性动画用.animation()修饰属性变化,比如缩放脉冲直接改scaleValue和opacityValue即可。显式动画用animateTo主动驱动。ArkTS的.animation()在声明式上比Flutter更直接——改个属性值动画就自动跑,不用手动管Controller的销毁和addListener。但做路径运动比如画圆这种,ArkTS里得手动算坐标然后用offset()或position()改位置,或者用Canvas的Path+animateTo实现,代码量不小。做组合动画时,多个属性用同一个animateTo闭包是同步的,但想分别控制不同属性的时长和曲线,就得拆成多个animateTo调用,时序对齐要多写不少逻辑。

主要区别:Flutter的AnimatedBuilder把动画值和渲染彻底分开,builder就是个纯函数。ArkTS的.animation()是属性维度的声明式,没有显式的builder概念。简单效果ArkTS更快,但复杂动画编排我更喜欢Flutter的Controller体系,尤其repeat、reverse、addStatusListener这些组合起来很灵活。

【踩坑记录】
1. builder里不要创建大对象:builder每帧都执行,如果在里面new一个大widget树或执行耗时操作,帧率直接崩。把Paint和Path提前创建好,builder里只做赋值和rebuild。
2. child参数的正确用法:child是传给builder的,只build一次。如果把动画变化的组件也放到child里传进去,动画不会生效。正确做法:变化的组件在builder里直接写,不变化的子组件通过child传。第一次用时把整个Container放进child,builder里只返回child,结果转圈没动静。
3. CurvedAnimation的复用:最好抽出来复用,CurvedAnimation的parent是final的,初始化后不能换Controller。
4. 多个Controller要用TickerProviderStateMixin:Demo用了5个AnimationController,如果用SingleTickerProviderStateMixin会报错。
5. 别忘了dispose Controller:忘记dispose的话页面切走后动画还在跑,浪费性能,建议创建Controller的同时写好dispose。
6. AnimatedBuilder嵌套问题:不限制嵌套层数,但嵌套多了builder回调也嵌套,debug堆栈头大。建议拆成独立widget并排而不是层层嵌套,实在需要叠加多个动画效果,一个AnimatedBuilder里搞定就够了。

验证环境:Flutter · HarmonyOS 6.0 · nova12u真机演示。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Flutter AnimatedBuilder鸿蒙适配:动画拆分与Ark

感谢分享,讲得非常清楚!尤其是AnimatedBuilder的child参数用法——把不变的部分抽出去确实能省不少重建开销,我之前也踩过整个Container塞进去动画不生效的坑。你提到的组合动画用一个Controller同步驱动多属性,也比拆多个Controller再手动对齐省心,这个思路在鸿蒙ArkTS里做类似的组合动画时确实要多写不少animateTo调用。另外关于CurvedAnimation的复用问题提醒得很及时,之前我图省事在builder里每次重建,后来发现parent不能换,一跑就崩。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Flutter AnimatedBuilder鸿蒙适配:动画拆分与Ark

感谢楼主这么详细的分享!AnimatedBuilder的child参数那个坑我也踩过,一开始以为整个widget塞进child就行,结果动画不动,看了你的解释才明白child只build一次的原理,很实用。还有那个repeat reverse参数,我之前没用reverse导致动画跳变,看完才知道有这参数,学到了。对比ArkTS的.animation()那段也很清晰,Flutter的Controller体系确实适合复杂编排,但ArkTS写简单动画确实更快。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Flutter AnimatedBuilder鸿蒙适配:动画拆分与Ark

感谢楼主的详细分享!最近正好在迁移一个 Flutter 动画比较多的页面到鸿蒙,看到你这篇真的是及时雨。 关于 `AnimatedBuilder` 的 `child` 参数那块,之前一直没太理解“只 build 一次”的用法,总以为把整个子组件扔进去就行,结果动画不跑。你举的那个把 Container 放进 child 然后只 return child 的例子,简直就是我踩的坑,这下彻底明白了——变化的组件必须写在 builder 里,不变的部分才用 child 传。 另外你提到 `CurvedAnimation` 最好抽出来复用,但 parent 是 final 的,这个细节很关键。我原来是在 builder 里每次都新建,确实没想过复用能减少哪些开销,回头试试。 `forward(from: 0)` 的弹跳例子也很有用,我之前的做法是每次点之前先 `reset` 再 `forward`,不仅多一步,而且如果动画进行中再点会有视觉跳变,用 `from: 0` 直接覆盖当前值就行,代码也更简洁。 对了,想问一下:在鸿蒙真机上,多个 `AnimationController` 同时跑的时候(比如你那 5 个 Controller),实际帧率稳定性怎么样?我在模拟器上测还好,但上真机偶尔会有掉帧卡顿,不知道是不是 `TickerProviderStateMixin` 的 ticker
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-7-23 15:54 , Processed in 0.030429 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部