flutter 动画
本文最后更新于:2022年4月22日 上午
flutter 动画
基本使用
前置条件
- 必须是 StatefulWidget且混入SingleTickerProviderStateMixin
- 创建 动画 AnimationController,传入vsync: this
- 创建 动画速率 CurvedAnimation
- 创建过渡值 Tween
- 开启动画
- 监听动画值改变
动画创建比较麻烦,如果项目中确实有用到动画,可以进行一定的封装
- animationController.forward()开启
- animationController.stop()停止
- animationController.reverse()反转
- animationController.addStatusListener()监听动画状态改变
- animationController.status动画当前状态
- animationController.dispose()销毁动画
class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);
  @override
  State<HomePage> createState() => _HomePageState();
}
/*
  动画 前置条件
  1. StatefulWidget
  2. 混入 SingleTickerProviderStateMixin
 */
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  AnimationController? animationController;
  CurvedAnimation? curvedAnimation;
  Animation<double>? tween;
  @override
  void initState() {
    super.initState();
    // 1. 创建 动画 controller
    animationController = AnimationController(vsync: this, duration: const Duration(seconds: 1));
    // 2. 创建动画速率
    curvedAnimation = CurvedAnimation(parent: animationController!, curve: Curves.linear);
    // 3. 创建动画过渡值
    tween = Tween(begin: 50.0, end: 100.0).animate(curvedAnimation);
    // 4.1 开启动画
    animationController.forward();
    // 4.2 监听动画改变
    animationController.addListener(() {
      setState(() {});
    });
    // 4.3 监听动画状态改变
    animationController.addStatusListener((status) {
      switch (status) {
        // 监听动画完成
        case AnimationStatus.completed:
          animationController.reverse();
          break;
        // 监听动画停止
        case AnimationStatus.dismissed:
          animationController.forward();
          break;
          
        default:
      }
    });
  }
  @override
  void dispose() {
    super.dispose();
    animationController.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Listener(
          onPointerUp: (e) {
            // 点击时 动画停止,再次点击动画继续
            if (animationController.isAnimating) {
              animationController.stop();
            } else {
              if (animationController.status == AnimationStatus.forward)
                animationController.forward();
              else
                animationController.reverse();
            }
          },
          child: Icon(
            Icons.favorite,
            color: Colors.red[400],
            size: tween.value, // 使用动画变化的值
          ),
        ),
      ),
    );
  }
}优化
继承 AnimatedWidget
将需要执行动画的 widget抽离且继承自 AnimatedWidget
// 抽离动画widget,当值发生改变时重新构建 widget
class IconAnimation extends AnimatedWidget {
  const IconAnimation(Animation anim) : super(listenable: anim);
  Animation<double> get _size => listenable as Animation<double>;
  @override
  Widget build(BuildContext context) {
    return Icon(
      Icons.favorite,
      color: Colors.red[400],
      size: _size.value, // 使用动画变化的值
    );
  }
}
// 使用
child: IconAnimation(tween),
AnimatedBuilder
在需要执行动画的组件外包裹一层 AnimatedBuilder,在动画改变的时候可以通过缓存 child的方式优化(该代码未展示,使用方式和其它的缓存 child的方式类似) 
child: AnimatedBuilder(
  animation: animationController,
  builder: (ctx, child) {
    return Icon(
      Icons.favorite,
      color: Colors.red[400],
      size: tween.value, // 使用动画变化的值
    );
  },
)交织动画
多个动画同时执行
如下,在动画变大变小的同时加入透明度的动画
可以在创建 Tween的时候创建多个不同的 Tween
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
main() => runApp(MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    );
  }
}
class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);
  @override
  State<HomePage> createState() => _HomePageState();
}
/*
  动画 前置条件
  1. StatefulWidget
  2. 混入 SingleTickerProviderStateMixin
 */
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  late AnimationController animationController;
  late CurvedAnimation curvedAnimation;
  late Animation<double> tween;
  late Animation<double> opcityTween; // 透明度
  @override
  void initState() {
    super.initState();
    // 1. 创建 动画 controller
    animationController = AnimationController(vsync: this, duration: Duration(seconds: 1));
    // 2. 创建动画速率
    curvedAnimation = CurvedAnimation(parent: animationController, curve: Curves.linear);
    // 3. 创建动画过渡值
    tween = Tween(begin: 50.0, end: 100.0).animate(curvedAnimation);
    // TODO: 创建透明度
    opcityTween = Tween(begin: 0.0, end: 1.0).animate(curvedAnimation);
    // 4.1 开启动画
    animationController.forward();
    // 4.2 监听动画改变
    animationController.addListener(() {
      setState(() {});
    });
    // 4.3 监听动画状态改变
    animationController.addStatusListener((status) {
      switch (status) {
      // 监听动画完成
        case AnimationStatus.completed:
          animationController.reverse();
          break;
      // 监听动画停止
        case AnimationStatus.dismissed:
          animationController.forward();
          break;
        default:
      }
    });
  }
  @override
  void dispose() {
    super.dispose();
    animationController.dispose();
  }
  @override
  Widget build(BuildContext context) {
    print('执行build 方法');
    return Scaffold(
      appBar: AppBar(title: Text('交织动画 (多个动画同时执行)')),
      body: Center(
        child: AnimatedBuilder(
          animation: animationController,
          builder: (ctx, child) {
            return Opacity(
              opacity: opcityTween.value,
              child: Icon(
                Icons.favorite,
                color: Colors.red[400],
                size: tween.value, // 使用动画变化的值
              ),
            );
          },
        ),
      ),
    );
  }
}
Hero 动画
Hero动画为动一个页面跳转到另一个页面时,同一个图片或者widget的过渡缩放动画,如:从首页的商品列表点击封面图进入详情页时的封面图过渡动画。在前端,这个过渡动画会比较难处理,但在 flutter中现起来就很简单
使用方式及其简单,在需要连接的动画外套一个 Hero且传入一致的 tag,即可保证路由跳转的时候会有个缩放效果的动画(把路由的动画同时设置为渐变效果更佳)
import 'package:flutter/material.dart';
main() => runApp(MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomePage(),
      routes: {
        '/home': (ctx) => HomePage(),
        // '/detail': (ctx) => DetailPage(),
      },
      initialRoute: '/home',
    );
  }
}
// 首页
class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Hero 动画 首页')),
      body: GridView.builder(
        itemCount: 20,
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 2, mainAxisSpacing: 5, crossAxisSpacing: 5, childAspectRatio: 16 / 9),
        itemBuilder: (ctx, index) {
          String url = 'https://picsum.photos/500/500?random=$index';
          return GestureDetector(
            onTap: () {
              Navigator.of(context).push(
                // 路由过渡动画
                PageRouteBuilder(
                  pageBuilder: (ctx, anim1, anim2) => FadeTransition(opacity: anim1, child: DetailPage(url: url)),
                ),
              );
            },
            // 加一层 Hero
            child: Hero(tag: url, child: Image.network(url, height: 50,fit: BoxFit.cover)),
          );
        },
      ),
      // body: Text('home'),
    );
  }
}
class DetailPage extends StatelessWidget {
  final String url;
  const DetailPage({Key? key, required this.url}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Hero 动画 详情页')),
      // 加一层 Hero
      body: Hero(tag: url, child: Image.network(url)),
    );
  }
}
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处。