Flutter 快速学会各种动画(给女票做滤镜、自定义switch不是梦)

动画从原理上可以分为两类:补间动画和基于物理动画。

补间动画顾名思义就是介于两点之间,两点也就是起点和终点。在补间动画中,定义了起点和终点以及时间轴,再定义过渡时间和速度的曲线。然后框架会计算如何从起点过渡到终点。

物理动画是基于对真实世界的行为模拟来进行建模的。像乒乓球的落地和弹起等,

在flutter中,动画又被区分隐式动画、显式动画、hexo动画、交织动画,物理动画等。下面详细解释。

隐式动画的使用

先看效果:


1.gif

实现一个盒子缩放,点击按钮放大:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("隐式动画"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              onPressed: () {
                _updateState();
              },
              child: Text('Animate'),
            ),
            Container(

              width: _bigger ? 400 : 100,
              height: _bigger ? 400 : 100,
              color: Colors.lightBlue[200],
              child: Center(
                child: Text(
                  'Animatiaon',
                  style: Theme.of(context).textTheme.subtitle1,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

没有任何动画,画面突兀生硬,下面我们用隐式动画实现一个柔和的效果:

  1. 把Container替换为AnimatedContainer;
  2. 设置动画时长为400毫秒;
  3. 设置动画曲线;
 @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("隐式动画"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              onPressed: () {
                _updateState();
              },
              child: Text('Animate'),
            ),
            AnimatedContainer(
              duration: Duration(
                milliseconds: 400,
              ),
              width: _bigger ? 400 : 100,
              height: _bigger ? 400 : 100,
              curve: Curves.bounceOut,
              color: Colors.lightBlue[200],
              child: Center(
                child: Text(
                  'Animatiaon',
                  style: Theme.of(context).textTheme.subtitle1,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
2.gif

简单的3步,就实现了一个缩放动画。能这么简单,是因为flutter帮我妈实现了动画细节。查看AnimatedContainer,可以看到它继承自ImplicitlyAnimatedWidget:
class AnimatedContainer extends ImplicitlyAnimatedWidget

ImplicitlyAnimatedWidgets:AnimatedContainer是Flutter的动画库为我们实现的管理动画的小部件。这些小部件统称为隐式动画或隐式动画小部件,它们的名称来自于ImplicitlyAnimatedWidget,也就是它们实现的父类,下面列举下常用的小部件:
ALign->AnimatedAlign
Container->AnimatedContainer
DefaultTextStyle->AnimatedDefaultTextStyle
Opacity->AnimatedOpacity
Padding->AnimatedPadding
PhysicalModel->AnimatedPhysicalModel
Positioned->AnimatedPositioned
PositionedDirectional->AnimatedPositionedDirectional
Theme->AnimatedThemeSize->AnimatedSize

这些小部件在首次添加到widget树时将不进行动画处理,也就是我们进入页面的时候,是没有动画的。但是当我们更改其属性时,它们将通过对指定持续时间内的变化自动进行动画处理来响应这些变化。怎么实现自动呢,是因为ImplicitlyAnimatedWidgetState在内部创建并管理AnimationController来为动画提供动力。

当然实现起来简单也就意味着动画效果简单,ImplicitlyAnimatedWidgets及其子类受到一些限制:除了动画属性之外,开发人员只能为动画选择持续时间和曲线。如果需要对动画进行更多控制(例如,将其停在中间的某个位置),ImplicitlyAnimatedWidgets并不能办到,这时候我们就需要使用显式动画。

Tween动画的使用

上面了解了基本的隐式动画,但是一些widget没有的属性,比如颜色变化等,我们就需要用Tween动画实现,它相当于简单自定义的隐式动画。

下面用一个案例实现P图软件的调色滤镜效果,给我的女朋友调个色。我相信你学会这一招,一定能讨得女朋友欢心,前提是你先有个女朋友。

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      body: Stack(
        children: <Widget>[
          Image.asset(
            R.bg,
            width: MediaQuery.of(context).size.width,
            height: MediaQuery.of(context).size.height,
            fit: BoxFit.fitWidth,
          ),
          Column(
            children: <Widget>[
              Center(
                child: Image.asset(
                  R.lihuili,
                ),
              ),
              Slider.adaptive(
                  value: _sliderValue,
                  onChanged: (double value) {
                    setState(() {
                      _sliderValue = value;
                      _newColor =
                          Color.lerp(Colors.white, Colors.blue, _sliderValue);
                    });
                  })
            ],
          ),
        ],
      ),
    );
  }

image.png

先实现布局,然后加入TweenAnimationBuilder:

              Center(
                child: TweenAnimationBuilder(
                  tween: ColorTween(begin: Colors.white,end: Colors.green),
                  duration: Duration(milliseconds: 300),
                  child: Image.asset(
                    R.lihuili,
                  ),
                ),
              )

因为是滤镜,所以使用ColorTween实现。然后把end颜色改为拖动手柄产生的值:

tween: ColorTween(begin: Colors.white, end: _newColor),

给图片加上颜色过滤:

ColorFiltered(
                      child: Image.asset(
                        R.lihuili,
                      ),
                      colorFilter: ColorFilter.mode(color, BlendMode.modulate),
                    );

见证奇迹的时刻来了:

33.gif

没几行代码,就实现了一个滤镜效果。嗯,加鸡腿。。。

Animation

了解了一些动画,下面介绍下动画的核心类:

  • Animation,Flutter 动画库中的核心类,插入用于指导动画的值。
    Animation 对象知道动画目前的状态(例如,是否开始,暂停,前进或倒退),但是对屏幕上显示的内容一无所知。

  • AnimationController 管理 Animation。
    AnimationController 是个特殊的 Animation 对象,每当硬件准备新帧时,他都会生成一个新值。默认情况下,AnimationController 在给定期间内会线性生成从 0.0 到 1.0 的数字。

  • CurvedAnimation 定义动画在开始值和结束值之间如何变化的路径或曲线。

  • Duration 动画花费的时间。

  • Tween 为动画对象插入一个范围值。例如,Tween 可以定义插入值由红到蓝,或从 0 到 255。
    在默认情况下,AnimationController 对象的范围是 0.0-0.1。如果需要不同的范围或者不同的数据类型,可以使用 Tween 配置动画来插入不同的范围或数据类型。

  • Tween.animate,要使用 Tween 对象,需要 Tween 调用 animate(),传入控制器对象。

  • 使用 Listeners 和 StatusListeners 监视动画状态变化。
    一个 Animation 对象可以有不止一个 Listener 和 StatusListener,用 addListener() 和 addStatusListener() 来定义。当动画值改变时调用 Listener。Listener 最常用的操作是调用 setState() 进行重建。当一个动画开始,结束,前进或后退时,会调用 StatusListener,用 AnimationStatus 来定义。

  • Ticker 动画定时器。
    AnimationController 的vsync对象会绑定一个ticker,当widget不显示时,动画定时器将会暂停,当widget再次显示时,动画定时器重新恢复执行,这样就可以避免动画相关UI不在前台显示时依然运行消耗资源。 如果要使用自定义的State对象作为vsync时,混入TickerProviderStateMixin。就不需要我们自己释放资源了。

显式动画

上面简单的动画不满足我们的时候,就需要自己控制动画了。

flutter 为我们提供的switch 不能改变大小,满足不了我们的需要,下面我们自己实现一个,首先分析都需要哪些属性:宽高、打开的颜色、关闭的颜色、按钮的颜色、打开关闭的事件。

class CustomSwitch extends StatefulWidget {
  CustomSwitch({
    Key key,
    this.width = 120,
    this.height = 50,
    this.activeColor = Colors.blue,
    this.inactiveColor = Colors.grey,
    this.buttonColor = Colors.white,
    this.onChanged,
    this.value = false,
  }) : super(key: key);

  final double width;
  final double height;

  /// 打开时的颜色
  final Color activeColor;

  /// 关闭时的颜色
  final Color inactiveColor;

  ///  按钮颜色
  final Color buttonColor;
  final ValueChanged<bool> onChanged;

  final bool value;

  @override
  _CustomSwitchState createState() {
    return _CustomSwitchState();
  }
}

class _CustomSwitchState extends State<CustomSwitch> {
  bool value;
   double paddingValue ;
   double diameter;

  @override
  void initState() {
    super.initState();
    value = widget.value;
    paddingValue=widget.height/12;
    diameter = widget.height - 2 * paddingValue;
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: widget.width,
      height: widget.height,
      decoration: BoxDecoration(
        color: value ? widget.activeColor : widget.inactiveColor,
        borderRadius: BorderRadius.circular(widget.height / 2),
      ),
      padding: EdgeInsets.all(paddingValue),
      child: Align(
        alignment: value?Alignment.centerRight:Alignment.centerLeft,
        child: Container(
          width: diameter,
          height: diameter,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: widget.buttonColor,
          ),
        ),
      ),
    );
  }
}

先实现ui,效果如下:


image.png
  1. 我们要创建一个动画,当点击的时候,滑块会从左边滑动到右边,所以首先混入 SingleTickerProviderStateMixin ,然后声明动画:

  Animation<Alignment> _animation;
  AnimationController _animationController;
  1. 初始化:
    // 设置动画取值范围和时间曲线
    _animation = Tween<Alignment>(
      begin: Alignment.centerLeft,
      end: Alignment.centerRight,
    ).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Curves.linear,
      )
    );
  1. 在我们的布局外面添加AnimatedBuilder:
    return AnimatedBuilder(
      animation: _animationController,
      builder: (animation,child){
      return  Container(
      ...
  1. 修改滑块位置为动画的值:
child: Align(
            alignment: _animation.value,
            child: Container(
            ...
  1. 万事俱备,下面就是点击滑块了,如果动画结束了,也就是从左边滑到了右边(初始为左边),或者从右边滑到了左边(初始为右),那么要反向运动,也就是reverse,否则就是开始动画,也就是forward:
 child: GestureDetector(
              onTap: () {
                if (_animationController.isCompleted) {
                  _animationController.reverse();
                } else {
                  _animationController.forward();
                }
                _value = !_value;
                widget.onChanged?.call(_value);
              },
             ...

看下效果:


4.gif

完整代码:

import 'package:flutter/material.dart';

class CustomSwitch extends StatefulWidget {
  CustomSwitch({
    Key key,
    this.width = 120,
    this.height = 50,
    this.activeColor = Colors.blue,
    this.inactiveColor = Colors.grey,
    this.buttonColor = Colors.white,
    this.onChanged,
    this.value = false,
  }) : super(key: key);

  final double width;
  final double height;

  /// 打开时的颜色
  final Color activeColor;

  /// 关闭时的颜色
  final Color inactiveColor;

  ///  按钮颜色
  final Color buttonColor;
  final ValueChanged<bool> onChanged;

  final bool value;

  @override
  _CustomSwitchState createState() {
    return _CustomSwitchState();
  }
}

class _CustomSwitchState extends State<CustomSwitch>
    with SingleTickerProviderStateMixin {
  bool _value;
  double _paddingValue;

  double _diameter;

  Animation<Alignment> _animation;
  AnimationController _animationController;

  @override
  void initState() {
    super.initState();
    _value = widget.value;
    _paddingValue = widget.height / 12;
    _diameter = widget.height - 2 * _paddingValue;
    // 初始化动画控制器,设置动画时间
    _animationController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 500),
    );

    // 设置动画取值范围和时间曲线
    _animation = Tween<Alignment>(
      begin: widget.value ? Alignment.centerRight : Alignment.centerLeft,
      end: widget.value ? Alignment.centerLeft : Alignment.centerRight,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.linear,
    ));
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animationController,
      builder: (animation, child) {
        return Container(
          width: widget.width,
          height: widget.height,
          decoration: BoxDecoration(
            color: _value ? widget.activeColor : widget.inactiveColor,
            borderRadius: BorderRadius.circular(widget.height / 2),
          ),
          padding: EdgeInsets.all(_paddingValue),
          child: Align(
            alignment: _animation.value,
            child: GestureDetector(
              onTap: () {
                if (_animationController.isCompleted) {
                  _animationController.reverse();
                } else {
                  _animationController.forward();
                }
                _value = !_value;
                widget.onChanged?.call(_value);
              },
              child: Container(
                width: _diameter,
                height: _diameter,
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: widget.buttonColor,
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

Hero动画

Hero 指的是在屏幕间转换的 widget。我们可以使用 Flutter’s Hero widget 创建 hero 动画。使 hero 从原页面过渡到新页面。
所以Flutter 中的 Hero widget 实现的动画类型也称为 共享元素过渡 或 共享元素动画。

创建 hero 的步骤

  1. 定义一个起始 Hero widget,被称为 source hero。也就是要过度的widget,通常是图片。
  2. 定义一个终点 Hero widget,被称为 destination hero。该 hero 与 source hero 使用一样的 tag 标签,hero 通过 tag 来匹配。 为了获得最佳效果,heroes 应该有几乎完全相同的 widget 树。
  3. 创建一个含有 destination hero 的页面。目标页面定义了动画结束时应有的 widget 树。
  4. 通过 Navigator 导航来触发动画。 Navigator 推送并弹出操作触发原页面和目标页面中含有配对标签 heroes 的 hero 动画。

下面我们按照套路实现一个:
第一步,定义一个起始hero;

Hero(
            tag: 'flippers',
            child: Image.asset(
              R.flippers,
            ),
          )

第二部,定义一个终点hero:

Hero(
                        tag: 'flippers',
                        child: SizedBox(
                          width: 100.0,
                          child: Image.asset(
                            R.flippers,
                          ),
                        ),
                      )

第三部,创建个页面装载终点hero:

Scaffold(
                    appBar: AppBar(
                      title: const Text('Flippers Page'),
                    ),
                    body: Container(
                      padding: const EdgeInsets.all(8.0),
                      alignment: Alignment.topLeft,
                      // Use background color to emphasize that it's a new route.
                      color: Colors.lightBlueAccent,
                      child: Hero(
                        tag: 'flippers',
                        child: SizedBox(
                          width: 100.0,
                          child: Image.asset(
                            R.flippers,
                          ),
                        ),
                      ),
                    ),
                  );

第四部,路由导航:

 Navigator.of(context).push(
              MaterialPageRoute<void>(
                builder: (BuildContext context) {
                ...

看下效果:


55.gif

页面过度动画

hero是2个widget之间的过度,页面的过度需要PageTransitionsBuilder,flutter 给我们实现了4种:


image.png
  • FadeUpwardsPageTransitionsBuilder — 淡入淡出
  • OpenUpwardsPageTransitionsBuilder — 从下往上
  • ZoomPageTransitionsBuilder — 从小到大缩放
  • CupertinoPageTransitionsBuilder — 苹果左右滑入风格

怎么使用呢?一般我们应用都是一个统一的过度风格,淡然flutter是包含安卓和ios的,所以区分不同的平台对应不同的风格。在全局设置:

MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
          pageTransitionsTheme: PageTransitionsTheme(builders: {
            TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
            TargetPlatform.android: ZoomPageTransitionsBuilder(),
          })),
      routes: Routes.routes,
      home: MyHomePage(title: '动画'),
    );

看下效果:


6.png

当然,如果你们的ui特别牛逼,要实现自己的风格,比如旋转并且淡入淡出的过度动画,我们就要自己实现了。

首先实现PageTransitionsBuilder:

class RotationFadeTransitionBuilder extends PageTransitionsBuilder {
  const RotationFadeTransitionBuilder();

  @override
  Widget buildTransitions<T>(
      PageRoute<T> route,
      BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
      Widget child,
      ) {
    return _RotationFadeTransitionBuilder(
        routeAnimation: animation, child: child);
  }
}

buildTransitions 的返回值是widget,我们创建一个widget:

class _RotationFadeTransitionBuilder extends StatelessWidget {
  _RotationFadeTransitionBuilder({
    Key key,
    @required Animation<double> routeAnimation,
    @required this.child,
  })  ;

  final Widget child;


  @override
  Widget build(BuildContext context) {

  }
}

因为我们需要一个旋转动画和一个淡入淡出,所以我们实现动画:

  final Animation<double> _turnsAnimation;
  final Animation<double> _opacityAnimation;

https://api.flutter.dev/flutter/animation/Curves-class.html

这是动画对应的curve,我们选择淡入淡出的和旋转的,并通过Animation.drive加到过渡动画上:

   _RotationFadeTransitionBuilder({
    Key key,
    @required Animation<double> routeAnimation,
    @required this.child,
  })  : _turnsAnimation = routeAnimation.drive(CurveTween(curve: Curves.linearToEaseOut)),
        _opacityAnimation = routeAnimation.drive(  CurveTween(curve: Curves.easeIn)),
        super(key: key);

实现我们的动画:

  @override
  Widget build(BuildContext context) {
    return RotationTransition(
      turns: _turnsAnimation,
      child: FadeTransition(
        opacity: _opacityAnimation,
        child: child,
      ),
    );
  }

添加到ThemeData中:

            TargetPlatform.android: RotationFadeTransitionBuilder(),

动画全部代码:


import 'package:flutter/material.dart';

class RotationFadeTransitionBuilder extends PageTransitionsBuilder {
  const RotationFadeTransitionBuilder();

  @override
  Widget buildTransitions<T>(
      PageRoute<T> route,
      BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
      Widget child,
      ) {
    return _RotationFadeTransitionBuilder(
        routeAnimation: animation, child: child);
  }
}

class _RotationFadeTransitionBuilder extends StatelessWidget {
  _RotationFadeTransitionBuilder({
    Key key,
    @required Animation<double> routeAnimation,
    @required this.child,
  })  : _turnsAnimation = routeAnimation.drive(CurveTween(curve: Curves.linearToEaseOut)),
        _opacityAnimation = routeAnimation.drive(  CurveTween(curve: Curves.easeIn)),
        super(key: key);


  final Animation<double> _turnsAnimation;
  final Animation<double> _opacityAnimation;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return RotationTransition(
      turns: _turnsAnimation,
      child: FadeTransition(
        opacity: _opacityAnimation,
        child: child,
      ),
    );
  }
}

看下效果:


99.gif

源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342