Flutter-动画过渡组件

389 阅读4分钟

在Widget属性发生变化时会执行过渡动画的组件统称为动画过渡组件,而动画过渡组件最明显的一个特征就是它会在内部自管理AnimationController。为了方便使用者可以自定义动画的曲线、执行时长、方向等,通常都需要使用者自己提供AnimationController对象来自定义这些属性值。但是如此一来,使用者就必须手动的管理AnimationController,这样会增加使用的复杂性。因此,将AnimationController进行封装,会大大提高动画组件的易用性。

自定义动画过渡组件

实现一个AnimatedDecoratedBox,它可以在decoration属性变化时,从旧状态变成新状态的过程执行一个过渡动画。

class SSLAnimatedDecoratedBox extends StatefulWidget {
  final BoxDecoration decoration;
  final Widget child;
  final Duration duration;
  final Curve curve;
  final Duration? reverseDuration;
  const SSLAnimatedDecoratedBox({
    Key? key,
    required this.decoration,
    required this.child,
    this.curve = Curves.linear,
    required this.duration,
    this.reverseDuration,
  }):super(key: key);
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return SSLAnimatedDecoratedBoxState();
  }
}

class SSLAnimatedDecoratedBoxState extends State <SSLAnimatedDecoratedBox> with SingleTickerProviderStateMixin{
  late AnimationController _controller;
  late Animation<double> _animation;
  late DecorationTween _tween;
  @protected
  AnimationController get controller => _controller;

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return AnimatedBuilder(animation: _animation, builder: (context, child){
        return DecoratedBox(
            decoration: _tween.animate(_animation).value,
          child: child,
        );
      },
      child: widget.child,
    );
  }

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _controller = AnimationController(vsync: this,duration: widget.duration, reverseDuration: widget.reverseDuration);
    _tween = DecorationTween(begin:  widget.decoration);
    updateCurve();
  }

  void updateCurve() {
    _animation = CurvedAnimation(parent: _controller, curve: widget.curve);
  }

  @override
  void didUpdateWidget(covariant SSLAnimatedDecoratedBox oldWidget) {
    // TODO: implement didUpdateWidget
    super.didUpdateWidget(oldWidget);
    if (widget.curve != oldWidget.curve){
      updateCurve();
    }

    _controller.duration = widget.duration;
    _controller.reverseDuration = widget.reverseDuration;

    if (widget.decoration != (_tween.end ?? _tween.begin)){
      _tween
          ..begin = _tween.evaluate(_animation)
          ..end = widget.decoration;
      _controller
            ..value = 0.0
            ..forward();
    }
  }
  @override
  void dispose() {
    // TODO: implement dispose
    _controller.dispose();
    super.dispose();
  }
}

为了方便开发者实现动画过渡组件的封装,Flutter提供了一个ImplicitlyAnimatedWidget抽象类,它继承自StatefulWidget,同时提供了一个对应的ImplicitlyAnimatedWidgetState类,AnimationController的管理就在ImplicitlyAnimatedWidgetState类中。封装时只需要分别继承ImplicitlyAnimatedWidget和ImplicitlyAnimatedWidgetState类即可:

class SSLCusAnimatedDecoratedBox extends ImplicitlyAnimatedWidget{
  final BoxDecoration decoration;
  final Widget child;
  const SSLCusAnimatedDecoratedBox({
    Key? key,
    required this.decoration,
    required this.child,
    Curve curve = Curves.linear,
    required Duration duration,
  }):super(key: key, curve: curve, duration: duration);

  @override
  ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState() {
    // TODO: implement createState
    return SSLCusAnimatedDecoratedBoxState();
  }
}

class SSLCusAnimatedDecoratedBoxState extends ImplicitlyAnimatedWidgetState<SSLCusAnimatedDecoratedBox>{
  late DecorationTween _decoration;
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return DecoratedBox(decoration: _decoration.evaluate(animation),child: widget.child,);
  }

  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {
    // TODO: implement forEachTween
    _decoration = visitor(
      _decoration,
      widget.decoration,
        (value) => DecorationTween(begin: value),
    ) as DecorationTween;
  }
}

其中curve、duration、reverseDuration三个属性已经定义。
可以看到State实现了build和forEachTween两个方法。在动画执行的过程中,每一帧都会调用build方法(调用逻辑在ImplicitlyAnimatedWidgetState中),所以在build中我们需要构建每一帧DecoratedBox状态,因此得算出每一帧dedecorationg状态,这个可以通过_decoration.evaluate(animation)来计算,其中animation是ImplicitlyAnimatedWidgetState积累中定义的对象,_decoration是自定义的一个DecorationTween对象。tween的主要职责就是定义动画的起始状态begin和终止状态end。对于AnimatedDecoratedBox来说,decoration的终止状态就是用户传给它的值,而起始状态是不确定的,有两种情况:

  1. AnimatedDecoratedBox首次build,此时直接将decoration值置为其实状态为_decoration.animate(animation),即_decoration值为DecorationTween(begin:_decoration).
  2. AnimatedDecoratedBox的decoration更新时,则起始状态为_decoration.animate(animation),即_decoration值为DecorationTween(begin:_decoration,animate(animation), end:decoration)。

forEachTween的作用就很明显,正是用来更新Tween的初始值,在上述来年各种功能情况下,会被调用。开发者只需要重写此方法,并在此方法中更新Tween的起始状态值即可。一些更新逻辑被屏蔽在了visitor回调中,使用时只需要传递正确的参数。看下visitor签名:

Tween<T> visitor(
   Tween<T> tween, //当前的tween,第一次调用为null
   T targetValue, // 终止状态
   TweenConstructor<T> constructor,//Tween构造器,在上述三种情况下会被调用以更新tween
 );

可以看到通过继承ImplicitlyAnimatedWidget和ImplicitlyAnimatedWidgetState类可以更快速的实现动画过渡组件的封装。

Flutter预置的动画过渡组件

组件名功能
AnimatedPadding在padding发生变化时会执行过渡动画到新状态
AnimatedPositioned配合Stack一起使用,当定位状态发生变化时会执行过渡动画到新的状态
AnimatedOpacity在透明opacity发生变化时执行过渡动画更新状态
AnimatedAlign当alignment发生变化时会执行过渡动画到新的状态
AnimatedContainer当Container属性发生变化时会执行过渡动画到新的状态
AnimatedDefaultTextStyle当字体样式发生变化时,子组件中继承了该样式的文本组件会动态过渡到新样式
import 'package:flutter/material.dart';

class AnimatedWidgetsTest extends StatefulWidget {
  const AnimatedWidgetsTest({Key? key}) : super(key: key);

  @override
  _AnimatedWidgetsTestState createState() => _AnimatedWidgetsTestState();
}

class _AnimatedWidgetsTestState extends State<AnimatedWidgetsTest> {
  double _padding = 10;
  var _align = Alignment.topRight;
  double _height = 100;
  double _left = 0;
  Color _color = Colors.red;
  TextStyle _style = const TextStyle(color: Colors.black);
  Color _decorationColor = Colors.blue;
  double _opacity = 1;

  @override
  Widget build(BuildContext context) {
    var duration = const Duration(milliseconds: 400);
    return SingleChildScrollView(
      child: Column(
        children: <Widget>[
          ElevatedButton(
            onPressed: () {
              setState(() {
                _padding = 20;
              });
            },
            child: AnimatedPadding(
              duration: duration,
              padding: EdgeInsets.all(_padding),
              child: const Text("AnimatedPadding"),
            ),
          ),
          SizedBox(
            height: 50,
            child: Stack(
              children: <Widget>[
                AnimatedPositioned(
                  duration: duration,
                  left: _left,
                  child: ElevatedButton(
                    onPressed: () {
                      setState(() {
                        _left = 100;
                      });
                    },
                    child: const Text("AnimatedPositioned"),
                  ),
                )
              ],
            ),
          ),
          Container(
            height: 100,
            color: Colors.grey,
            child: AnimatedAlign(
              duration: duration,
              alignment: _align,
              child: ElevatedButton(
                onPressed: () {
                  setState(() {
                    _align = Alignment.center;
                  });
                },
                child: const Text("AnimatedAlign"),
              ),
            ),
          ),
          AnimatedContainer(
            duration: duration,
            height: _height,
            color: _color,
            child: TextButton(
              onPressed: () {
                setState(() {
                  _height = 150;
                  _color = Colors.blue;
                });
              },
              child: const Text(
                "AnimatedContainer",
                style: TextStyle(color: Colors.white),
              ),
            ),
          ),
          AnimatedDefaultTextStyle(
            child: GestureDetector(
              child: const Text("hello world"),
              onTap: () {
                setState(() {
                  _style = const TextStyle(
                    color: Colors.blue,
                    decorationStyle: TextDecorationStyle.solid,
                    decorationColor: Colors.blue,
                  );
                });
              },
            ),
            style: _style,
            duration: duration,
          ),
          AnimatedOpacity(
            opacity: _opacity,
            duration: duration,
            child: TextButton(
              style: ButtonStyle(
                  backgroundColor: MaterialStateProperty.all(Colors.blue)),
              onPressed: () {
                setState(() {
                  _opacity = 0.2;
                });
              },
              child: const Text(
                "AnimatedOpacity",
                style: TextStyle(color: Colors.white),
              ),
            ),
          ),
          AnimatedDecoratedBox1(
            duration: Duration(
                milliseconds: _decorationColor == Colors.red ? 400 : 2000),
            decoration: BoxDecoration(color: _decorationColor),
            child: Builder(builder: (context) {
              return TextButton(
                onPressed: () {
                  setState(() {
                    _decorationColor = _decorationColor == Colors.blue
                        ? Colors.red
                        : Colors.blue;
                  });
                },
                child: const Text(
                  "AnimatedDecoratedBox toggle",
                  style: TextStyle(color: Colors.white),
                ),
              );
            }),
          )
        ].map((e) {
          return Padding(
            padding: const EdgeInsets.symmetric(vertical: 16),
            child: e,
          );
        }).toList(),
      ),
    );
  }
}