Flutter 仿 Hero 的动画

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

效果

Simulator Screen Recording - iPhone 14 Pro - 2023-08-08 at 22.32.59.gif

代码

DEMO

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

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

}
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 组件

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

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY05Tt8k' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片