You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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),
      ),
    };
  }
}

方案优势

  1. 保留状态模式:各状态UI类仍独立负责自身布局与业务逻辑,关注点分离清晰。
  2. 无额外动画代码:新增/修改状态时,仅需实现抽象协议的共享元素声明,无需编写动画逻辑。
  3. 自动匹配转场:协调容器通过GlobalKey自动匹配前后状态的共享元素,计算位置与尺寸实现平滑转场。
  4. 兼容基础动画:非共享元素通过AnimatedSwitcher实现淡入淡出,与共享元素转场互不干扰。

内容的提问来源于stack exchange,提问作者Nerdy Bunz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 09:13:21