在卡片选中态切换的开发中,背景色、边框、圆角往往会分开写多套动画。AnimatedContainer 虽然方便,但透明度与颜色变化混在一起,调参时很难分清影响因素。后来通过 DecoratedBoxTransition 发现,它能把 BoxDecoration 整体做动画过渡,颜色、圆角、渐变、边框、阴影一次性搞定。这次将 Flutter 组件移植到鸿蒙系统时,我深入研究了几个典型场景的用法,在此分享经验。
---- 组件定位 ----
DecoratedBoxTransition 本质上是一个盒子加上 Tween 动画。它的 child 始终存在,visible 不变,但盒子本身的装饰属性(背景色、圆角、边框、阴影、渐变)会跟着动画过渡。使用时必须创建 BoxDecoration 之间的 Tween,Flutter 会自动计算中间帧插值。核心代码如下:- DecoratedBoxTransition(
- decoration: DecorationTween(
- begin: BoxDecoration(
- color: Colors.blue.shade100,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: Colors.blue.shade300, width: 2),
- ),
- end: BoxDecoration(
- color: Colors.orange.shade100,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: Colors.orange.shade300, width: 2),
- ),
- ).animate(controller),
- child: Container(
- width: double.infinity,
- height: 100,
- alignment: Alignment.center,
- child: Text('背景色过渡'),
- ),
- )
复制代码 Controller 变化时,DecoratedBoxTransition 内部根据 Animation 的值 interpolate 两个 BoxDecoration——颜色从蓝渐变到橙,边框也从蓝过渡到橙。child 不参与动画绘制,只负责展示内容区域。与 AnimatedContainer 的隐式动画不同,DecoratedBoxTransition 是显式动画,控制粒度更细,可配合 Curve 和 Interval 实现复杂节奏。
---- 颜色过渡 ----
建一个 AnimationController,forward 时变到 end,reverse 时回到 begin。Flutter 会自动计算中间值。注意:两边相同的属性可以省略,如 borderRadius 均为 12 可只写一次;但 border 等复杂对象最好两边都写,否则可能出现边框突然消失的问题。- DecoratedBoxTransition(
- decoration: DecorationTween(
- begin: BoxDecoration(
- color: Colors.blue.shade100,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: Colors.blue.shade300, width: 2),
- ),
- end: BoxDecoration(
- color: Colors.orange.shade100,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: Colors.orange.shade300, width: 2),
- ),
- ).animate(_colorCtrl),
- child: Container(
- width: double.infinity,
- height: 100,
- alignment: Alignment.center,
- child: Text(
- '背景色过渡',
- style: TextStyle(fontSize: 18, color: Colors.grey.shade600, fontWeight: FontWeight.w500),
- ),
- ),
- ),
复制代码
---- 圆角变化 ----
从直角变成圆角,Flutter 的 BorderRadius.lerp 函数会自动线性插值,效果顺滑。注意:如果 color 两边一样,可以只写一个,但 begin 和 end 的类型必须一致——不能一边有 color 一边有 gradient,否则 DecorationTween 会报错。遇到这种情况需自行实现 lerp,或拆成两个 DecoratedBoxTransition 切换。- DecoratedBoxTransition(
- decoration: DecorationTween(
- begin: BoxDecoration(
- color: Colors.red.shade100,
- borderRadius: BorderRadius.circular(4),
- border: Border.all(color: Colors.red.shade300),
- ),
- end: BoxDecoration(
- color: Colors.red.shade100,
- borderRadius: BorderRadius.circular(50),
- border: Border.all(color: Colors.red.shade300),
- ),
- ).animate(_radiusCtrl),
- child: Container(
- width: 100,
- height: 100,
- alignment: Alignment.center,
- child: Text('圆角'),
- ),
- ),
复制代码
---- 渐变切换 ----
LinearGradient 本身可 interpolate,Flutter 会把两个渐变的 colors 数组按位置一一对应插值。注意:colors 数组长度必须一致,否则多出的颜色会被截掉,出现异常色带。渐变方向(begin/end alignment)最好两边一致,否则插值会导致方向旋转,结果可能不符合预期。- DecoratedBoxTransition(
- decoration: DecorationTween(
- begin: BoxDecoration(
- gradient: LinearGradient(
- colors: [Colors.blue.shade300, Colors.blue.shade100],
- begin: Alignment.topLeft,
- end: Alignment.bottomRight,
- ),
- borderRadius: BorderRadius.circular(12),
- ),
- end: BoxDecoration(
- gradient: LinearGradient(
- colors: [Colors.purple.shade300, Colors.orange.shade300],
- begin: Alignment.topLeft,
- end: Alignment.bottomRight,
- ),
- borderRadius: BorderRadius.circular(12),
- ),
- ).animate(_gradientCtrl),
- child: Container(
- width: double.infinity,
- height: 100,
- alignment: Alignment.center,
- child: const Text('渐变背景过渡', style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.w500)),
- ),
- ),
复制代码
---- 组合切换 ----
实际开发中经常需要颜色、圆角、边框、阴影同时变化。DecoratedBoxTransition 用一个 DecorationTween 搞定,无需每个属性配一个 Controller。而鸿蒙 ArkTS 中做组合动画,需给每个属性分别配 animateTo,若插值曲线不同还需拆成链式调用,代码较长。Flutter 这边所有属性过渡节奏统一,跟随同一个 Animation。- DecoratedBoxTransition(
- decoration: DecorationTween(
- begin: BoxDecoration(
- color: Colors.teal.shade50,
- borderRadius: BorderRadius.circular(8),
- border: Border.all(color: Colors.teal.shade200, width: 1),
- boxShadow: [BoxShadow(color: Colors.teal.shade100.withValues(alpha: 0.3), blurRadius: 4, offset: const Offset(0, 2))],
- ),
- end: BoxDecoration(
- color: Colors.pink.shade50,
- borderRadius: BorderRadius.circular(20),
- border: Border.all(color: Colors.pink.shade300, width: 3),
- boxShadow: [BoxShadow(color: Colors.pink.shade100.withValues(alpha: 0.5), blurRadius: 12, offset: const Offset(0, 4))],
- ),
- ).animate(_combinedCtrl),
- child: Container(
- width: double.infinity,
- height: 100,
- alignment: Alignment.center,
- child: Text('颜色 + 圆角 + 边框 + 阴影', style: TextStyle(fontSize: 16, color: Colors.grey.shade600, fontWeight: FontWeight.w500)),
- ),
- ),
复制代码
---- 状态切换卡片的实际场景 ----
项目里常需要“选中态”和“默认态”切换。之前用 AnimatedContainer 时每次选中刷新整个 widget tree,动画会卡顿。改用 DecoratedBoxTransition,每个卡片持有自己的 Controller,点击时 forward/reverse,动画独立不干扰。Controller 时长建议 400ms,easeInOut 曲线效果自然。- Widget _buildStateSwitchCard(int index, String label) {
- final ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 400));
- return GestureDetector(
- onTap: () {
- if (ctrl.isCompleted) {
- ctrl.reverse();
- } else {
- ctrl.forward();
- }
- },
- child: DecoratedBoxTransition(
- decoration: DecorationTween(
- begin: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(8),
- border: Border.all(color: Colors.grey.shade300),
- ),
- end: BoxDecoration(
- color: Colors.blue.shade50,
- borderRadius: BorderRadius.circular(8),
- border: Border.all(color: Colors.blue.shade300, width: 2),
- ),
- ).animate(CurvedAnimation(parent: ctrl, curve: Curves.easeInOut)),
- child: Container(
- width: double.infinity,
- height: 50,
- padding: const EdgeInsets.symmetric(horizontal: 16),
- alignment: Alignment.centerLeft,
- child: Text(label, style: const TextStyle(fontSize: 15)),
- ),
- ),
- );
- }
复制代码
---- 与鸿蒙 ArkTS 属性动画对比 ----
ArkTS 中装饰属性(borderColor、borderRadius、backgroundColor)可直接做属性动画,写法更直接,无需创建 Tween 和 Controller。但 DecoratedBoxTransition 的优势在于 BoxDecoration 作为整体参与动画,多个属性的插值曲线天然同步;而 ArkTS 的属性动画即使放在同一个 animateTo 回调里,每个属性的插值逻辑仍是独立计算,可能出现“颜色变完了圆角还在跑”的情况。此外,ArkTS 的阴影动画不能直接用 animateTo 改 shadow,需用 shadow API 配合自定义 Shadow 对象或 .stateStyles 做状态切换;而 Flutter 的 boxShadow 是 BoxDecoration 的一部分,天然支持 Tween 插值。
不过 ArkTS 的 TransitionEffect 支持入场合退场动画(如卡片从列表中移除时边框缩小再消失),Flutter 实现类似效果需用 AnimatedList + DecoratedBoxTransition 嵌套组合。
---- 踩坑点 ----
1. DecorationTween 只认同类型的 Decoration:begin 和 end 必须同为 BoxDecoration,不可一个纯 color 一个 gradient,否则报错。需确保类型一致。
2. child 不参与动画绘制:DecoratedBoxTransition 只动画 decoration 参数,child 内容不受影响。如需 child 透明度或缩放变化,需外层再加 FadeTransition 或 ScaleTransition。
3. 某些属性插值效果不明显:border 的 style(solid/dashed)不能插值,只能突变;gradient 的 tileMode、transform 也不参与插值。避免在动画过程中切换这些属性。
4. Controller 生命周期:所有 Controller 需在 initState 初始化、dispose 释放。若每个列表条目持有自己的 Controller,条目回收时务必 dispose,否则 OOM。
---- 插值细节 ----
DecorationTween 实际调用 Decoration.lerp 静态方法。检查 begin 和 end 是否都是 BoxDecoration,若是则逐字段插值:color、borderRadius、border、boxShadow、gradient、shape、backgroundBlendMode。每个字段有自己的 lerp 逻辑,如 Color.lerp、BorderRadius.lerp、逐个 BoxShadow.lerp。本质是将零散属性插值统一封装为组件。
---- 总结 ----
DecoratedBoxTransition 的最大价值在于将装饰属性动画统一管理。Flutter 组件化与 ArkTS 声明式各有优势:简单动画用 ArkTS 的 animateTo 更快;复杂多组件协调、交错入场场景下,Flutter 显式动画系统更灵活。验证环境:Flutter · HarmonyOS 6.0 · nova12u 真机。 |