All in FlutterAndroid进阶之路Flutter圈子

Flutter 快速学会各种动画

2020-07-12  本文已影响0人  A_si

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

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

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

在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

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

显式动画

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

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

怎么使用呢?一般我们应用都是一个统一的过度风格,淡然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

源码

上一篇 下一篇

猜你喜欢

热点阅读