flutter 动画

本文最后更新于:2022年4月22日 上午

flutter 动画

基本使用

前置条件

  1. 必须是 StatefulWidget且混入SingleTickerProviderStateMixin
  2. 创建 动画 AnimationController,传入 vsync: this
  3. 创建 动画速率 CurvedAnimation
  4. 创建过渡值 Tween
  5. 开启动画
  6. 监听动画值改变

动画创建比较麻烦,如果项目中确实有用到动画,可以进行一定的封装

  • 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 协议,转载请注明出处。