9.6 通用“动画切换”组件(AnimatedSwitcher)

实际开发中,经常会遇到很多过度动画,比如tab切换,路由切换。为了让用户感觉更丝滑流畅,我们可以给过度指定一个动画。

Flutter提供了很多组件,但是这些很多场景不能使用,为此,FlutterSDK提供了一个AnimatedSwitcher.

9.6.1 AnimatedSwitcher

AnimatedSwitcher可以对旧组件与新组建分别实现隐藏和显示动画,首先看下定义:

const AnimatedSwitcher({
  Key key,
  this.child,
  @required this.duration, // 新child显示动画时长
  this.reverseDuration,// 旧child隐藏的动画时长
  this.switchInCurve = Curves.linear, // 新child显示的动画曲线
  this.switchOutCurve = Curves.linear,// 旧child隐藏的动画曲线
  this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder, // 动画构建器
  this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, //布局构建器
})

当AnimatedSwitcher的child发生变化时(类型或Key不同),旧child会执行隐藏动画,新child会执行执行显示动画。究竟执行何种动画效果则由transitionBuilder参数决定,该参数接受一个AnimatedSwitcherTransitionBuilder类型的builder,定义如下:

typedef AnimatedSwitcherTransitionBuilder =
  Widget Function(Widget child, Animation<double> animation);

builderAnimatedSwitcherchild切换时会分别对新、旧child绑定动画:

对旧child,绑定的动画会反向执行(reverse) 对新child,绑定的动画会正向指向(forward) 这样一下,便实现了对新、旧child的动画绑定。AnimatedSwitcher的默认值是AnimatedSwitcher.defaultTransitionBuilder

Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
  return FadeTransition(
    opacity: animation,
    child: child,
  );
}

可以看到返回了FadeTransition对象,也就是说默认,AnimatedSwitcher会对旧child执行渐隐和渐显动画。

例子

我们实现一个计数器,每次自增/减小都执行缩小动画。

ClipRRect(
child: AnimatedSwitcher(
  child: ClipRRect(
    borderRadius: BorderRadius.all(Radius.circular(50)),
    key: Key(_count.toString()),
    child: Container(
      width: 100,
      height: 100,
      color: Colors.orange,
      alignment: Alignment.center,
      child: Text(
        '$_count',
      ),
    ),
  ),
  duration: Duration(milliseconds: 300),
  transitionBuilder: (Widget child, Animation<double> animation) {
    return ScaleTransition(
      scale: animation,
      child: child,
    );
  },
),
)

每次key都要变化,而且是在child的顶级部件上,所以使用count最合适不过了。

在使用渐入渐隐下过看下,只需要将关键代码改成下面即可:

return FadeTransition(
                  opacity: animation,
                  child: child,
                );

要实现一个+向上翻滚,新的widget从底部出来该怎么做呢?

关键代码改成

SlideTransition(
  position:
      Tween<Offset>(begin: Offset(0, -1), end: Offset(0, 0))
          .animate(animation),
  child: child,
)

效果:

效果不是很理想,旧widget消失了,新的weiget从消失的地方出现了,需要改成新的widget从底部出现才行。

那么需要在就动画消失的时候讲坐标重置。

class MySliderTransitiion extends AnimatedWidget {
  final Widget child;
  final Ax ax;
  Tween<Offset> _tween;
  MySliderTransitiion(
      {Key key,
      @required Animation<double> position,
      this.child,
      this.ax = Ax.btt})
      : assert(position != null),
        super(key: key, listenable: position) {
   _tween = Tween(begin: Offset(0, 1), end: Offset(0, 0));
  }
  Animation<double> get position => listenable;
  @override
  Widget build(BuildContext context) {
    Offset offset = _tween.evaluate(position);

    /// 根据 动画的执行正向和反向来调节 坐标位置
    if (position.status == AnimationStatus.reverse) {
        offset = Offset(offset.dx, -offset.dy);
    }
    return FractionalTranslation(
      translation: offset,
      child: child,
    );
  }
}

最后将上边的SlideTransition换成MySliderTransitiion即可。

看下效果:

AnimatedSwitcher(
  child: ClipRRect(
    borderRadius: BorderRadius.all(Radius.circular(50)),
    key: Key(_count.toString()),
    child: Container(
      width: 100,
      height: 100,
      color: Colors.orange,
      alignment: Alignment.center,
      child: Text(
        '$_count',
      ),
    ),
  ),
  duration: Duration(milliseconds: 300),
  transitionBuilder: (Widget child, Animation<double> animation) {
    return MySliderTransitiion(
      position: animation,
      child: child,
    );
  },
),

高级应用

前边我们展示了从下到上,那么如果实现从下到上,从左到右,从右到左呢,当然我们可以修改上边的 代码,定义一个枚举,实现一个方向,这样子每次只需要穿传进去方向即可。

enum Ax {
  ltr,
  rtl,
  ttb,
  btt,
}

// ignore: must_be_immutable
class MySliderTransitiion extends AnimatedWidget {
  final Widget child;
  final Ax ax;
  Tween<Offset> _tween;
  MySliderTransitiion(
      {Key key,
      @required Animation<double> position,
      this.child,
      this.ax = Ax.btt})
      : assert(position != null),
        super(key: key, listenable: position) {
    /// 偏移在内部处理 每个方向的偏移量都不一样,
    /// 需要根据方向来指定偏移量
    switch (this.ax) {
      case Ax.btt:
        _tween = Tween(begin: Offset(0, 1), end: Offset(0, 0));
        break;
      case Ax.ltr:
        _tween = Tween(begin: Offset(-1, 0), end: Offset(0, 0));
        break;
      case Ax.ttb:
        _tween = Tween(begin: Offset(0, -1), end: Offset(0, 0));
        break;
      case Ax.rtl:
        _tween = Tween(begin: Offset(1, 0), end: Offset(0, 0));
        break;
    }
  }
  Animation<double> get position => listenable;
  @override
  Widget build(BuildContext context) {
    Offset offset = _tween.evaluate(position);

    /// 根据 动画的执行正向和反向来调节 坐标位置
    if (position.status == AnimationStatus.reverse) {
      switch (this.ax) {
        case Ax.ltr:
          offset = Offset(-offset.dx, offset.dy);
          break;
        case Ax.rtl:
          offset = Offset(-offset.dx, offset.dy);
          break;
        case Ax.ttb:
          offset = Offset(offset.dx, -offset.dy);
          break;
        case Ax.btt:
          offset = Offset(offset.dx, -offset.dy);
          break;
      }
    }
    return FractionalTranslation(
      translation: offset,
      child: child,
    );
  }
}

现在要实现从左到右只需要传入Ax.ztl即可。

AnimatedSwitcher(
  child: ClipRRect(
    borderRadius: BorderRadius.all(Radius.circular(50)),
    key: Key(_count.toString()),
    child: Container(
      width: 100,
      height: 100,
      color: Colors.orange,
      alignment: Alignment.center,
      child: Text(
        '$_count',
      ),
    ),
  ),
  duration: Duration(milliseconds: 300),
  transitionBuilder: (Widget child, Animation<double> animation) {
    return MySliderTransitiion(
      position: animation,
      child: child,
      ax: _ax,
    );
  },
)

效果:

总结

本节学习了AnimatedSwitcher的详细用法,同事介绍了AnimatedSwitcher的对称性方法,自己在做动画的时候需要手动切换新旧场景的动画,AnimatedSwitcher很有用。

results matching ""

    No results matching ""