Flutter动画的使用

吐槽君 分类:javascript

动画能提高用户的使用体验,使APP更流畅。那么在Flutter中如何实现动画以及选择使用什么样的动画呢?

开门见山,我们直接上图:

动画概览

绘制依赖的动画

绘制依赖的动画是指我们用动画库没法直接实现的动画,这时候有两种实现方式:

  1. Canvas不断绘制形成动画(CustomPainter或者自定义RenderObjectWidget);
  2. 使用设计师提供的动画文件,然后结合三方库来使用(Lottie或者Flare等)。

Lottie

lottie-flutter借鉴自Lottie,使用方法很也很简单。

  • 添加依赖
dependencies:
  lottie: ^1.0.1
 
  • 引入库文件
import 'package:lottie/lottie.dart';
 
  • 使用
Lottie.network('https://raw.githubusercontent.com/xvrh/lottie-flutter/master/example/assets/Mobilo/A.json')
 

lottie

CustomPainter

CustomPainter是系统提供的一个能够绘制内容的底层API。

  • CustomPainter绘制
class SquarePainter extends CustomPainter {
  final double radians;
  SquarePainter(this.radians);

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.pink
      ..strokeWidth = 5
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    var path = Path();

    var angle = (math.pi * 2) / 4.0;

    Offset center = Offset(size.width / 2, size.height / 2);
    Offset startPoint =
        Offset(100 * math.cos(radians), 100 * math.sin(radians));

    path.moveTo(startPoint.dx + center.dx, startPoint.dy + center.dy);

    for (int i = 1; i <= 4; i++) {
      double x = 100 * math.cos(radians + angle * i) + center.dx;
      double y = 100 * math.sin(radians + angle * i) + center.dy;
      path.lineTo(x, y);
    }
    path.close();
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
  
}
 
  • CustomPaint这个Widget封装CustomPainter的绘制内容
CustomPaint(
    painter: SquarePainter(animation.value),
    child: Container(),
);
 
  • AnimationController驱动动画

全部相关代码

提示: CustomPainter可以实现很复杂的绘制,本案例仅仅用CustomPainter简单绘制了一个正方形,然后进行不断的旋转动画。

CustomPainter

隐性动画

隐性动画的WidgetImplicitlyAnimatedWidget,它的特点是在属性发生改变后会自动进行动画,不需对动画进行控制,所以叫做隐性动画。

Flutter中的隐性动画和iOS中的隐性动画的概念类似。

abstract class ImplicitlyAnimatedWidget extends StatefulWidget {
    const ImplicitlyAnimatedWidget({
        Key? key,
        this.curve = Curves.linear,
        required this.duration,
        this.onEnd,
    })
}
 

ImplicitlyAnimatedWidget可以设置动画的速率曲线curve(可以设置的类型),动画时间duration和动画结束后的回调函数onEnd。属性发生变化后,动画就依据这些参数自动进行。

官方提供了很多的隐性动画Widget,他们被命名为Animated**。接下来我们就来一个个看下这些Widget的动画效果。

AnimatedAlign

Align的可动画版本,alignment的发生变化引发动画效果。

AnimatedAlign

class Body extends StatefulWidget {
  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> with TickerProviderStateMixin {
  bool selected = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('CustomPainter'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            selected = !selected;
          });
        },
        child: Center(
          child: Container(
            width: 250.0,
            height: 250.0,
            color: Colors.red,
            child: AnimatedAlign(
              alignment: selected ? Alignment.topRight : Alignment.bottomLeft,
              duration: const Duration(seconds: 1),
              curve: Curves.fastOutSlowIn,
              child: Logo(),
            ),
          ),
        ),
      ),
    );
  }
}
 

AnimatedContainer

Container的可动画版本,AnimatedContainer的各种属性发生变化后有动画效果。

AnimatedContainer

AnimatedContainer(
    width: selected ? 300.0 : 150.0,
    height: selected ? 300.0 : 150.0,
    decoration: BoxDecoration(
        color: selected ? Colors.amber[500] : Colors.amber[200],
        borderRadius: BorderRadius.circular(selected ? 20 : 0)),
        alignment: selected
            ? AlignmentDirectional.bottomCenter
            : AlignmentDirectional.topCenter,
    duration: Duration(seconds: 1),
    curve: Curves.fastOutSlowIn,
    child: Logo(),
)
 

AnimatedDefaultTextStyle

TextStyle的可动画版本,TextStyle发生变化引发动画效果。

AnimatedDefaultTextStyle

AnimatedDefaultTextStyle(
    duration: Duration(seconds: 1),
    curve: Curves.bounceOut,
    style: TextStyle(
        fontSize: selected ? 20 : 16,
        color: selected ? Colors.amber : Colors.red,
        fontWeight: FontWeight.bold,
    ),
    child: Text("Animated DefaultTextStyle"),
)
 

AnimatedOpacity

Opacity的可动画版本,透明度改变引发动画效果。

AnimatedOpacity

AnimatedOpacity(
    opacity: selected ? 1.0 : 0.0,
    duration: Duration(seconds: 1),
    curve: Curves.linear,
    child: Logo(),
)
 

AnimatedPadding

Padding的可动画版本,Padding改变引发动画效果。

AnimatedPadding

AnimatedPadding(
    curve: Curves.easeInOut,
    duration: Duration(seconds: 1),
    child: Container(
        child: Logo(),
    ),
    padding: EdgeInsets.symmetric(
        horizontal: selected ? 10 : 40,
        vertical: selected ? 10 : 30),
)
 

AnimatedPhysicalModel

PhysicalModel的可动画版本,borderRadiuselevation改变引发动画效果。

AnimatedPhysicalModel

AnimatedPhysicalModel(
    duration: const Duration(milliseconds: 500),
    curve: Curves.fastOutSlowIn,
    elevation: selected ? 0 : 10.0,
    shape: BoxShape.rectangle,
    shadowColor: Colors.red,
    color: Colors.white,
    borderRadius: selected
        ? BorderRadius.all(Radius.circular(0))
        : BorderRadius.all(Radius.circular(10)),
    child: Container(
        color: Colors.blue[100],
        child: Logo(),
    ),
)
 

AnimatedPositioned

Position的可动画版本,Position改变引发动画效果。

AnimatedPositioned

Stack(
    children: [
        AnimatedPositioned(
            width: selected ? 100.0 : 100.0,
            height: selected ? 100.0 : 100.0,
            top: selected ? 150.0 : 50.0,
            left: selected ? 150.0 : 100.0,
            duration: Duration(seconds: 1),
            curve: Curves.fastOutSlowIn,
                child: Container(
                color: Colors.blue,
            ),
        ),
    ],
),
 

AnimatedCrossFade

两个子Widget相互切换的动画。

AnimatedCrossFade

AnimatedCrossFade(
    firstChild: Logo(),
    secondChild: FlutterLogo(
        size: 120,
    ),
    crossFadeState: selected
        ? CrossFadeState.showSecond
        : CrossFadeState.showFirst,
    duration: Duration(seconds: 1),
    firstCurve: Curves.easeIn,
    secondCurve: Curves.easeIn,
)
 

AnimatedSize

子Widget大小发生变化后会发生动画。AnimatedContainer是自身大小发生变化引发动画,这是它们的主要区别。

AnimatedSize

解释:子Widget(Colors.amber)的大小改变没有动画,子Widget的大小改变后AnimatedSize(Colors.red)开始执行动画。

AnimatedSize(
    duration: Duration(seconds: 1),
    reverseDuration: Duration(seconds: 1),
    curve: Curves.fastOutSlowIn,
    child: Container(
        width: selected ? 150 : 100,
        height: selected ? 150 : 100,
    color: Colors.amber,
    ),
    vsync: this,
)
 

AnimatedSwitcher

这个Widget功能比较全,可以实现

  • 添加、删除Widget的动画;
  • 切换Widget的动画;
  • Widget属性变化的动画;

AnimatedSwitcher

AnimatedSwitcher(
    child: Text(
        "$elapsed",
        key: ValueKey(elapsed),
            style: TextStyle(fontSize: 34),
        ),
        duration: Duration(seconds: 2),
        transitionBuilder:
            (Widget child, Animation<double> animation) {
                final offsetAnimation = TweenSequence([
                      TweenSequenceItem(
                          tween: Tween<Offset>(
                                  begin: Offset(0.0, 1.0),
                                  end: Offset(0.0, 0.0))
                              .chain(CurveTween(curve: Curves.easeInOut)),
                          weight: 1),
                      TweenSequenceItem(
                          tween: ConstantTween(Offset(0.0, 0.0)), weight: 4),
                    ]).animate(animation);
                    return ClipRRect(
                      child: SlideTransition(
                        position: offsetAnimation,
                        child: child,
                ),
            );
        },
        layoutBuilder: (currentChild, previousChildren) {
            return currentChild;
    },
)
 

前面介绍了一系列官方提供的隐性动画Widget,除了AnimatedCrossFade,AnimatedSizeAnimatedSwitcher外都是ImplicitlyAnimatedWidget的子类。使用这些类能够方便的实现动画,当遇到没法实现的功能又想用隐性动画的需求时,TweenAnimationBuilder就是我们很好的选择。

TweenAnimationBuilder

实现自定义隐性动画。

TweenAnimationBuilder

TweenAnimationBuilder(
    duration: Duration(seconds: 2),
    curve: Curves.easeInOut,
    tween: Tween<double>(begin: 0, end: selected ? 180 : 0),
    builder: (context, value, child) {
        return RotationWidget(rotationY: value);
    },
)

class RotationWidget extends StatelessWidget {
  static const double degrees2Radians = pi / 180;
  final double rotationY;

  const RotationWidget({Key key, this.rotationY = 0}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Transform(
        alignment: FractionalOffset.center,
        transform: Matrix4.identity()
          ..setEntry(3, 2, 0.001)
          ..rotateY(rotationY * degrees2Radians),
        child: rotationY <= 90
            ? Logo(size: 250)
            : Transform(
                alignment: FractionalOffset.center,
                transform: Matrix4.identity()
                  ..setEntry(3, 2, 0.001)
                  ..rotateY(180 * degrees2Radians),
                child: FlutterLogo(size: 200),
              ));
  }
}
 

显性动画

显性动画是指需要开发者去控制动画,也就是说需要开发者去使用动画Animation相关的类去控制动画过程。

动画(Animation)相关类

  • Animation
abstract class Animation<T> extends Listenable implements ValueListenable<T> {

  void addListener(VoidCallback listener);
  void removeListener(VoidCallback listener);
    
  void addStatusListener(AnimationStatusListener listener);
  void removeStatusListener(AnimationStatusListener listener);

  AnimationStatus get status;
    
  T get value;

  bool get isDismissed => status == AnimationStatus.dismissed;
  bool get isCompleted => status == AnimationStatus.completed;
}
 

Animation这个类代表了动画的当前值和状态,以及告知监听者这两个值的改变

  1. value代表动画当前的值,addListener()removeListener()可以添加和移除监听者,当value变化后,会通知监听者值的变化;
  2. status代表动画的状态,有初始状态dismissed,正向动画状态forward,反向动画状态reverse动画已完成状态completed四种,addStatusListener()removeStatusListener()可以添加和移除监听者,当status变化后,会通知监听者状态的变化。
  • AnimationController
class AnimationController extends Animation<double> {

    void reset() {}

    TickerFuture forward({ double? from }) {}
    
    TickerFuture reverse({ double? from }) {}
    
    TickerFuture animateTo(double target, { Duration? duration, Curve curve = Curves.linear }) {}
    
    TickerFuture animateBack(double target, { Duration? duration, Curve curve = Curves.linear }) {}
    
    TickerFuture repeat({ double? min, double? max, bool reverse = false, Duration? period }) {}
    
    void stop({ bool canceled = true }) {}
}
 

Animation只能代表当前动画值和动画的状态,没法控制动画。AnimationController继承自Animation是对动画进行控制的类。譬如它能重置动画reset(),正向进行动画forward(),反向进行动画reverse(),重复进行动画repeat()和停止动画stop()等。

  • CurvedAnimation
class CurvedAnimation extends Animation<double> {
    
    final Animation<double> parent;
    
    Curve curve;
    
    Curve? reverseCurve;
}
 

CurvedAnimation的功能是给parent设置一个动画速率变化的曲线。也就是说动画值的变化率可以不是固定的。curve是正向动画的曲线,reverseCurve是反向动画的曲线。可以设置的类型和隐性动画一样

  • Tween
class Tween<T extends dynamic> extends Animatable<T> {

  T? begin;


  T? end;
 

Tween主要是给``设置一个动画的开始值begin和动画的结束值end

系统提供了一些列的Tween封装类供我们使用:

ColorTween

SizeTween

RectTween

IntTween

StepTween

ConstantTween

CurveTween

  • TweenSequence
class TweenSequence<T> extends Animatable<T> {

  final List<TweenSequenceItem<T>> _items = <TweenSequenceItem<T>>[];
  final List<_Interval> _intervals = <_Interval>[];

}
 

TweenSequence可以设置一系列的TweenSequenceItem,相当于设置一些关键帧,可以实现类似关键帧动画

动画实现的案例

我们来用上面提到的一些类来实现一个图片放大然后缩小的循环动画,效果如下:

animation

  • 1.创建AnimationController
// vsync参数值是同步信号,duration参数值是动画的时间
AnimationController _controller = AnimationController(vsync: this, duration: Duration(seconds: 2));
 
  • 2.给_controller设置Curve
// parent参数值是需要设置Curve的Animation,curve参数值是正向动画的Curve,reverseCurve参数值是反向动画的Curve
Animation _animation = CurvedAnimation(
                        parent: _controller,
                        curve: Curves.bounceOut,
                        reverseCurve: Curves.bounceIn);
 
  • 3.给_animation设置动画的开始值和结束值
Animation_sizeAnim = Tween(begin: 100.0, end: 200.0).animate(_animation);
 
  • 4.监听_controller动画值的改变,然后进行界面更新
_controller.addListener(() {
    setState(() {});
});
 
  • 5.监听听_controller动画的状态改变,然后进行循环动画
_controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      }
});
 
  • 6.界面的展示逻辑
Logo(
    size: _sizeAnim.value,
)
 

通过上面几个步骤,动画的代码就写完了。所有代码如下:

class Body extends StatefulWidget {
  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> with SingleTickerProviderStateMixin {
  // 创建AnimationController
  AnimationController _controller;
  Animation _animation;
  Animation _sizeAnim;

  @override
  void initState() {
    super.initState();

    // 1.创建AnimationController
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 2));

    // 2.设置Curve的值
    _animation = CurvedAnimation(
        parent: _controller,
        curve: Curves.bounceOut,
        reverseCurve: Curves.bounceIn);

    // 3. 设置动画的开始值和结束值
    _sizeAnim = Tween(begin: 100.0, end: 200.0).animate(_animation);

    // 4. 监听动画值的改变
    _controller.addListener(() {
      setState(() {});
    });

    // 5. 监听动画的状态改变
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Animation"),
      ),
      body: Center(
        child: Logo(
          size: _sizeAnim.value,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          _controller.forward();
        },
      ),
    );
  }

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

AnimatedWidget

上面的代码我们实现了动画,但是有两个问题:

  1. 我们需要监听_controller的值的变化,然后调用setState(() {});这个方法;
  2. 动画的绘制涉及到了Body的重构ReBuild,其实我们只需要Logo进行重构ReBuild就行。

我们可以将Logo重构为一个AnimatedWidget,这样就可以解决上面两问题了:

class LogoAnimatedWidget extends AnimatedWidget {
  LogoAnimatedWidget(Animation anim) : super(listenable: anim);

  @override
  Widget build(BuildContext context) {
    Animation anim = listenable;
    return Logo(size: anim.value);
  }
}
 

解释下代码逻辑:

  1. 继承AnimatedWidget的子类的构造函数需要传入一个Animation;
  2. 重写build方法,返回Widget,这里就是我们的Logo

接下来删除_controller.addListener(), 然后使用LogoAnimatedWidget

class Body extends StatefulWidget {
  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> with SingleTickerProviderStateMixin {
  // 创建AnimationController
  AnimationController _controller;
  Animation _animation;
  Animation _sizeAnim;

  @override
  void initState() {
    super.initState();

    // 1.创建AnimationController
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 2));

    // 2.设置Curve的值
    _animation = CurvedAnimation(
        parent: _controller,
        curve: Curves.bounceOut,
        reverseCurve: Curves.bounceIn);

    // 3. 设置动画的开始值和结束值
    _sizeAnim = Tween(begin: 100.0, end: 200.0).animate(_animation);

    // 4. 监听动画值的改变
    // _controller.addListener(() {
    //   setState(() {});
    // });

    // 5. 监听动画的状态改变
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Animation"),
      ),
      body: Center(
        child: LogoAnimatedWidget(_sizeAnim),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          _controller.forward();
        },
      ),
    );
  }

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

class LogoAnimatedWidget extends AnimatedWidget {
  LogoAnimatedWidget(Animation anim) : super(listenable: anim);

  @override
  Widget build(BuildContext context) {
    Animation anim = listenable;
    return Logo(size: anim.value);
  }
}
 

AnimatedBuilder

AnimatedWidget其实也有一些问题:

  1. 需要新建一个AnimatedWidget,代码量增加了;
  2. AnimatedWidget如果动画的Widget含有子Widget,那子Widget也会重构ReBuild

AnimatedBuilder可以解决上面两问题。代码如下:

  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Animation"),
      ),
      body: Center(
        child: AnimatedBuilder(
            animation: _sizeAnim,
            builder: (ctx, child) {
              return Logo(
                size: _sizeAnim.value,
              );
            }),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          _controller.forward();
        },
      ),
    );
  }
 

AnimatedBuilder有两个参数,animation参数值是Animation, builder参数值是一个函数,会提供BuildContext子Widget,一个提供Widget的构造环境,一个提供child可以复用,无需重构子Widget

官方提供的AnimatedWidget

官方提供了一些AnimatedWidget,使用方式和使用自定义的AnimatedWidget类似。

由于和隐式动画的版本类似,这里就不一一贴出效果了,只是列出来供大家参阅:

AlignTransition

DecoratedBoxTransition

DefaultTextStyleTransition

PositionedTransition

RelativePositionedTransition

RotationTransition

ScaleTransition

SizeTransition

SlideTransition

FadeTransition

总结

至此,我们将Flutter中的动画实现方式总结完了。

我们知道动画的逻辑就是不断的重绘,那Animation相关的类是如何引发重绘的呢?隐式动画又是如何对开发者屏蔽了Animation类实现动画的呢?

相关的问题,我们在下一节将深入底层去为你揭开面纱!

回复

我来回复
  • 暂无回复内容