Flutter中如何结合状态模式实现共享元素转场动画?
基于状态模式的Flutter音频组件共享元素转场实现方案
核心思路
在保留状态模式的前提下,通过独立的动画协调容器抽离转场逻辑,让各状态UI类仅负责自身布局与共享元素声明,无需关心动画实现。协调容器统一管理状态切换时的动画生命周期,自动匹配前后状态的共享元素并执行转场。
步骤1:定义状态UI抽象协议
扩展原有状态UI抽象类,新增共享元素声明方法,让每个状态明确哪些元素需要参与转场:
abstract class AudioStateUI { // 构建当前状态完整UI Widget build(BuildContext context); // 返回需参与共享转场的元素(Key作为唯一标识) Map<GlobalKey, Widget> getSharedElements(); }
步骤2:实现动画协调容器
创建一个StatefulWidget作为状态UI的容器,负责检测状态切换、管理动画控制器、处理共享元素转场:
class AudioStateTransitionContainer extends StatefulWidget { final AudioStateUI currentState; const AudioStateTransitionContainer({ super.key, required this.currentState, }); @override State<AudioStateTransitionContainer> createState() => _AudioStateTransitionContainerState(); } class _AudioStateTransitionContainerState extends State<AudioStateTransitionContainer> with SingleTickerProviderStateMixin { late AnimationController _animController; AudioStateUI? _prevState; final _containerKey = GlobalKey(); @override void initState() { super.initState(); _animController = AnimationController( vsync: this, duration: const Duration(milliseconds: 350), curve: Curves.easeInOut, ); } @override void didUpdateWidget(covariant AudioStateTransitionContainer oldWidget) { super.didUpdateWidget(oldWidget); // 检测状态类型变化,触发动画 if (widget.currentState.runtimeType != oldWidget.currentState.runtimeType) { _prevState = oldWidget.currentState; _animController.reset(); _animController.forward(); } } @override Widget build(BuildContext context) { return Stack( key: _containerKey, children: [ // 非共享元素:用AnimatedSwitcher实现基础切换动画 AnimatedSwitcher( duration: const Duration(milliseconds: 350), child: _buildNonSharedUI(widget.currentState), ), // 共享元素:执行转场动画 if (_prevState != null) AnimatedBuilder( animation: _animController, builder: (context, child) { return Stack( children: [ ..._buildSharedElementLayer(_prevState!, 1 - _animController.value), ..._buildSharedElementLayer(widget.currentState, _animController.value), ], ); }, ), ], ); } // 过滤掉共享元素,构建非共享UI部分 Widget _buildNonSharedUI(AudioStateUI state) { final sharedKeys = state.getSharedElements().keys.toSet(); return RepaintBoundary( child: Builder( builder: (context) { // 需改造状态UI的build方法,支持过滤共享元素 // 或新增buildNonSharedElements方法替代原build return state.build(context); }, ), ); } // 构建单个状态的共享元素层,应用动画插值 List<Widget> _buildSharedElementLayer(AudioStateUI state, double animValue) { return state.getSharedElements().entries.map((entry) { final key = entry.key; final widget = entry.value; final renderBox = key.currentContext?.findRenderObject() as RenderBox?; if (renderBox == null) return const SizedBox.shrink(); // 获取元素相对于容器的位置与尺寸 final containerBox = _containerKey.currentContext?.findRenderObject() as RenderBox; final offset = renderBox.localToGlobal(Offset.zero, ancestor: containerBox); final size = renderBox.size; return Positioned( left: offset.dx, top: offset.dy, width: size.width, height: size.height, child: Opacity( opacity: animValue, child: Transform.scale( scale: animValue, child: widget, ), ), ); }).toList(); } }
步骤3:改造具体状态UI类
每个状态UI实现抽象协议,声明共享元素,无需关心动画:
// 全局唯一Key,用于匹配跨状态的共享元素 final recordPlayButtonKey = GlobalKey(); class RecordingState implements AudioStateUI { @override Widget build(BuildContext context) { return Column( children: [ // 非共享元素:录音时长 const Text("00:15", style: TextStyle(fontSize: 20)), // 共享元素:录音/播放按钮 Align( alignment: Alignment.bottomCenter, child: ElevatedButton( key: recordPlayButtonKey, onPressed: () => audioModel.stopRecording(), child: const Icon(Icons.stop, size: 32), ), ), ], ); } @override Map<GlobalKey, Widget> getSharedElements() { return { recordPlayButtonKey: ElevatedButton( key: recordPlayButtonKey, onPressed: () {}, child: const Icon(Icons.stop, size: 32), ), }; } } class PlayingState implements AudioStateUI { @override Widget build(BuildContext context) { return Column( children: [ // 非共享元素:进度条 const LinearProgressIndicator(value: 0.4), // 共享元素:录音/播放按钮 Align( alignment: Alignment.bottomCenter, child: ElevatedButton( key: recordPlayButtonKey, onPressed: () => audioModel.pausePlaying(), child: const Icon(Icons.pause, size: 32), ), ), ], ); } @override Map<GlobalKey, Widget> getSharedElements() { return { recordPlayButtonKey: ElevatedButton( key: recordPlayButtonKey, onPressed: () {}, child: const Icon(Icons.pause, size: 32), ), }; } }
方案优势
- 保留状态模式:各状态UI类仍独立负责自身布局与业务逻辑,关注点分离清晰。
- 无额外动画代码:新增/修改状态时,仅需实现抽象协议的共享元素声明,无需编写动画逻辑。
- 自动匹配转场:协调容器通过GlobalKey自动匹配前后状态的共享元素,计算位置与尺寸实现平滑转场。
- 兼容基础动画:非共享元素通过AnimatedSwitcher实现淡入淡出,与共享元素转场互不干扰。
内容的提问来源于stack exchange,提问作者Nerdy Bunz
相关产品推荐
相关产品推荐

