Flutter 仿 Hero 的动画

Flutter 模仿 Hero 动画的效果,实现逻辑比较简单,就是用 Stack 结合 AnimatedBuilder 组件实现类似 Hero 的转场的动画效果。

效果

代码

DEMO

dart 复制代码
class TWAnimationHeroApp extends StatelessWidget {
  final controller = TWAnimationHeroController();
  TWAnimationHeroApp({super.key});

  @override
  Widget build(BuildContext context) {
    Widget heroChild = GestureDetector(
      onTap: () => controller.executeAnimation(),
      child: Image.asset(
        Assets.beauty.path,
        fit: BoxFit.fitHeight,
      ),
    );

    return MaterialApp(
      theme: ThemeData(primarySwatch: Colors.grey),
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(),
        body: TWAnimationHero(
          controller: controller,
          heroChild: heroChild,
          child: Stack(
            children: [
              ListView(
                children: [
                  Container(
                    height: 100,
                    alignment: Alignment.center,
                    color: Colors.orange,
                    child: GestureDetector(
                      onTap: () => controller.reverseAnimation(),
                      child: SizedBox(
                        width: 50,
                        height: 50,
                        key: controller.targetKey,
                        child: Image.asset(
                          Assets.beauty.path,
                        ),
                      ),
                    ),
                  ),
                  Container(
                    height: 100,
                    color: Colors.black,
                  ),
                  Container(
                    height: 100,
                    color: Colors.green,
                  ),
                  Container(
                    height: 100,
                    color: Colors.red,
                  ),
                  Container(
                    height: 100,
                    color: Colors.lime,
                  ),
                  Container(
                    height: 100,
                    color: Colors.green,
                  ),
                  Container(
                    height: 100,
                    color: Colors.yellow,
                  ),
                  Container(
                    height: 100,
                    color: Colors.blueAccent,
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

TWAnimationHeroController

dart 复制代码
class TWAnimationHeroController extends ChangeNotifier {
  GlobalKey targetKey = GlobalKey();
  GlobalKey heroKey = GlobalKey();

  /// 是否可见
  bool get isHeroVisible => _isHeroVisible;

  bool _isHeroVisible = true;

  set heroVisible(bool value) {
    _isHeroVisible = value;
    notifyListeners();
  }

  /// 是否方向状态
  bool isReverse = false;
  AnimationController? controller;
  Animation? animation;

  double offTop = 0;
  double offBottom = 0;
  double offLeft = 0;
  double offRight = 0;
  TWAnimationHeroController();

  /// 执行正向动画
  executeAnimation() {
    if (isReverse) return;
    isReverse = true;
    final child1Rect = fetchChildRect(targetKey);
    final child2Rect = fetchChildRect(heroKey);
    if (child1Rect == null || child2Rect == null) return;
    offTop = child1Rect.top - child2Rect.top;
    offBottom = child2Rect.bottom - child1Rect.bottom;
    offLeft = child1Rect.left - child2Rect.left;
    offRight = child2Rect.right - child1Rect.right;
    controller?.forward();
  }

  /// 执行反向动画
  reverseAnimation() {
    if (!isReverse) return;
    heroVisible = true;
    isReverse = false;
    controller?.reverse();
  }

  Rect? fetchChildRect(GlobalKey key) {
    RenderBox? renderBox = key.currentContext?.findRenderObject() as RenderBox?;
    if (renderBox == null) return null;
    final size = renderBox.size;
    final offset = renderBox.localToGlobal(Offset.zero);
    final childRect = offset & size;
    return childRect;
  }
}

TWAnimationHero 组件

dart 复制代码
class TWAnimationHero extends StatefulWidget {
  final Widget child;
  final Widget? heroChild;

  final TWAnimationHeroController controller;
  const TWAnimationHero({
    super.key,
    required this.controller,
    required this.child,
    this.heroChild,
  });

  @override
  State<TWAnimationHero> createState() => _TWAnimationHeroState();
}

class _TWAnimationHeroState extends State<TWAnimationHero>
    with TickerProviderStateMixin {
  @override
  void initState() {
    super.initState();
    createController();
  }

  /// 创建控制器
  createController() {
    final controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    );

    //应用curve
    widget.controller.animation = CurvedAnimation(
      parent: controller,
      curve: Curves.easeIn,
    );

    controller.addListener(() {
      // 注意正向动画才会监听到 isCompleted
      if (controller.isCompleted) {
        widget.controller.heroVisible = false;
      }
    });

    widget.controller.controller = controller;
  }

  @override
  void didUpdateWidget(covariant TWAnimationHero oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.controller.controller == null) {
      widget.controller.controller?.dispose();
      createController();
    }
  }

  @override
  void dispose() {
    widget.controller.controller?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        widget.child,
        if (widget.heroChild != null &&
            widget.controller.controller != null &&
            widget.controller.animation != null)
          AnimatedBuilder(
            animation: widget.controller.controller!,
            builder: (BuildContext context, Widget? child) {
              return Positioned(
                top: widget.controller.animation!.value *
                    widget.controller.offTop,
                bottom: widget.controller.animation!.value *
                    widget.controller.offBottom,
                left: widget.controller.animation!.value *
                    widget.controller.offLeft,
                right: widget.controller.animation!.value *
                    widget.controller.offRight,
                child: child!,
              );
            },
            child: AnimatedBuilder(
              animation: widget.controller,
              builder: (BuildContext context, Widget? child) {
                return Visibility(
                  visible: widget.controller.isHeroVisible,
                  child: Container(
                    color: Colors.transparent,
                    key: widget.controller.heroKey,
                    child: widget.heroChild,
                  ),
                );
              },
            ),
          ),
      ],
    );
  }
}
相关推荐
liulian09167 小时前
Flutter for OpenHarmony 跨平台开发:颜色选择器功能实战指南
flutter
ZC跨境爬虫7 小时前
跟着 MDN 学 HTML day_9:(信件语义标记)
前端·css·笔记·ui·html
前端老石人8 小时前
HTML 字符引用完全指南
开发语言·前端·html
幼儿园技术家8 小时前
前端如何设计权限系统(RBAC / ABAC)?
前端
前端摸鱼匠10 小时前
Vue 3 的v-bind合并行为:讲解v-bind与普通属性合并的规则
前端·javascript·vue.js·前端框架·ecmascript
REDcker10 小时前
浏览器端Web程序性能分析与优化实战 DevTools指标与工程清单
开发语言·前端·javascript·vue·ecmascript·php·js
liulian091611 小时前
Flutter for OpenHarmony 跨平台开发:BMI计算器功能实战指南
flutter·华为
donecoding11 小时前
一个 sudo 引发的血案:npm 全局包权限错乱彻底修复
前端·node.js·前端工程化
风骏时光牛马11 小时前
Raku正则匹配与数据批量处理实操案例
前端
nbwenren12 小时前
2026实测:Gemini 3 镜像站视觉能力实践——拍照原型图,一键生成 HTML+CSS 代码
前端·css·html