Flutter - 变换动画

4,394 阅读11分钟

android 里面我们就可以根据 layout 的变化设计自己的 latyout 切换动画,甚至矢量动画还可以进行 path 方面的无缝切换,比如从圆形自然过度到矩形

Flutter 这里自然也响应提供了相关动画,但是区别肯定还是有的,首先 Flutter 这里名字就改了叫做:隐式动画,我想说何必给 coder 找不自在呢,延续 android 的传统不好嘛~

  • AnimatedSwitcher - widget 内容改变时可以播放自己指定的动画
  • AnimatedContainer - 带动画的 Container,像 Container 一眼使用,在其中 color、width、height、圆角改变时会触发过度动画,动画不能控制,有些类似与 path 动画
  • AnimatedCrossFade - 切换不同布局时可以显示动画,但是不能自己设置动画,默认就是淡入淡出,并且在大小不通切换时显示不好
  • DecoratedBoxTransition - 边框动画,核心是通过圆角角度的改变实现形状上的变化,这个变化是自然过度的,这点和 path 动画是一样了
  • AnimatedDefaultTextStyle - 文字样式改变时的切换动画,主要呈现的大小变换方面的动画,颜色的渐变过度不明显,但是体验不好的地方在于,大小字切换时字体粗细的变化真实有点辣眼,有点卡顿
  • AnimatedModalBarrier - 颜色改变的变换动画,特殊的地方在于其必须放到所操的 widget 的 child 中,有明确的应用场景,就是点击时改变背景色,比如 dialog 弹出时,背景变灰色
  • AnimatedOpacity - 透明度的变化动画
  • AnimatedPhysicalModel - 阴影变换动画,设置有些复杂
  • AnimatedPositioned - stack 中 widget 位置,大小变换动画
  • AnimatedSize - widget 大小变换动画

AnimatedContainer

AnimatedContainer 顾名思义就是带动画的 Container,属性设置使用和 Container 时一模一样的,区别就是可以设置动画时间和插值器

动画效果这块和 矢量动画类似,都可以实现前后状态间的无缝切换,由系统完成动画每帧的数值输出。但是能做到 矢量动画 那种效果的属性只有:colorwidthheight圆角,其他都不行,比如图片切换就是一下就切换了,shape 形状的切换,比如放大从圆形到矩形,圆形在达到最大时一瞬间会切换到矩形,矩形再慢慢放大

AnimatedContainer 的使用套路就是,把属性值写在外部,通过 setState 改变属性值就可以实现切换动画了

这方面看我写的例子:

class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {
  double width = 50;
  double height = 50;
  Color color = Colors.blueAccent;
  BoxShape shape = BoxShape.circle;

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        AnimatedContainer(
          duration: Duration(seconds: 1),
          width: width,
          height: height,
          decoration: BoxDecoration(
            color: color,
            shape: shape,
          ),
        ),
        RaisedButton(
          child: Text("AAA"),
          onPressed: () {
            setState(() {
              width = 300;
              height = 300;
              color = Colors.pink;
              shape = BoxShape.rectangle;
            });
          },
        ),
      ],
    );
  }
}

官方文档这里给出了一个非常好的例子,AnimatedContainer 能实现的极限都在这里面了,其中形状的改变是通过改变圆角矩形的角度实现的:BorderRadius.circular(8);

class TestWidgetState extends State<TestWidget>
    with SingleTickerProviderStateMixin {
  double _width = 50;
  double _height = 50;
  Color _color = Colors.green;
  BorderRadiusGeometry _borderRadius = BorderRadius.circular(8);

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Center(
          child: AnimatedContainer(
            // Use the properties stored in the State class.
            width: _width,
            height: _height,
            decoration: BoxDecoration(
              color: _color,
              borderRadius: _borderRadius,
            ),
            // Define how long the animation should take.
            duration: Duration(seconds: 1),
            // Provide an optional curve to make the animation feel smoother.
            curve: Curves.fastOutSlowIn,
          ),
        ),
        RaisedButton(
          child: Icon(Icons.play_arrow),
          // When the user taps the button
          onPressed: () {
            // Use setState to rebuild the widget with new values.
            setState(() {
              // Create a random number generator.
              final random = Random();

              // Generate a random width and height.
              _width = random.nextInt(300).toDouble();
              _height = random.nextInt(300).toDouble();

              // Generate a random color.
              _color = Color.fromRGBO(
                random.nextInt(256),
                random.nextInt(256),
                random.nextInt(256),
                1,
              );

              // Generate a random border radius.
              _borderRadius =
                  BorderRadius.circular(random.nextInt(100).toDouble());
            });
          },
        ),
      ],
    );
  }
}

最后大家注意啊,AnimatedContainer 动画只能直接操作 Container 本身的属性,child 里的子 widget 就管不了了


AnimatedSwitcher

基本使用

AnimatedSwitcher 是 Flutter 中提供的用于 widget 切换内容时得动画样式,目前看到只能支持同一个 widget 得内容变化,切换不同类型的 widget 还在研究

AnimatedSwitcher 属性有几个:

  • child - 内容切换动画作用于得 widget
  • duration - 动画从 A -> B 的时间
  • reverseDuration - 动画反过来从 B -> A 的时间
  • switchInCurve - 动画从 A -> B 的动画插值器,Curves.linear
  • switchOutCurve - 反过来得插值器
  • transitionBuilder - 动画
  • layoutBuilder - 包装新旧 Widget 的组件,默认是一个 Stack

其中注意点是 key,flutter widget 树自身有缓存的,同一个 widget 只是内容更新的话是不会重建该 widget 的,但是 AnimatedSwitcher 想要有动画出来,必须是2个 widget 才行的,所以我们要手动设置 key 以规避 Flutter 中的 widget 缓存机制

网上这部分内容基本都出自官方文档:9.6 通用"切换动画"组件(AnimatedSwitcher)

官方文档没 gif,看不出效果,这里我帖一下效果和代码:

class TestWidgetState extends State<TestWidget> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          AnimatedSwitcher(
            duration: const Duration(milliseconds: 500),
            transitionBuilder: (Widget child, Animation<double> animation) {
              //执行缩放动画
              return ScaleTransition(child: child, scale: animation);
            },
            child: Text(
              '$_count',
              //显示指定key,不同的key会被认为是不同的Text,这样才能执行动画
              key: ValueKey<int>(_count),
              style: Theme.of(context).textTheme.display1,
            ),
          ),
          RaisedButton(
            child: const Text('+1',),
            onPressed: () {
              setState(() {
                _count += 1;
              });
            },
          ),
        ],
      ),
    );
  }
}

然后我们再来一个 icon 切换的例子

class TestWidgetState extends State<TestWidget> {
  IconData _actionIcon = Icons.delete;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          AnimatedSwitcher(
            transitionBuilder: (child, anim) {
              return ScaleTransition(child: child, scale: anim);
            },
            duration: Duration(milliseconds: 200),
            child: IconButton(
              key: ValueKey(_actionIcon),
              icon: Icon(_actionIcon),
            ),
          ),
          RaisedButton(
            child: Text("切换图标"),
            onPressed: () {
              setState(() {
                if (_actionIcon == Icons.delete)
                  _actionIcon = Icons.done;
                else
                  _actionIcon = Icons.delete;
              });
            },
          ),
        ],
      ),
    );
  }
}

AnimatedSwitcher 原理

其实原理很简单,一说就明白。因为 AnimatedSwitcher 其中的 child widget 的key 不一样,那么每次 child widget 内容变化时都会被认为是有新的 widget 出现,会重新 build,我们在 didUpdateWidget 中拿到新久 widget,对旧 widget 执行反向动画,对新 widget 执行正向动画,就是这样,下面是源码截取:

void didUpdateWidget(AnimatedSwitcher oldWidget) {
  super.didUpdateWidget(oldWidget);
  // 检查新旧child是否发生变化(key和类型同时相等则返回true,认为没变化)
  if (Widget.canUpdate(widget.child, oldWidget.child)) {
    // child没变化,...
  } else {
    //child发生了变化,构建一个Stack来分别给新旧child执行动画
   _widget= Stack(
      alignment: Alignment.center,
      children:[
        //旧child应用FadeTransition
        FadeTransition(
         opacity: _controllerOldAnimation,
         child : oldWidget.child,
        ),
        //新child应用FadeTransition
        FadeTransition(
         opacity: _controllerNewAnimation,
         child : widget.child,
        ),
      ]
    );
    // 给旧child执行反向退场动画
    _controllerOldAnimation.reverse();
    //给新child执行正向入场动画
    _controllerNewAnimation.forward();
  }
}

AnimatedSwitcher 高级使用

AnimatedSwitcher 的特征大家应该都明白了,新旧 widget 之间会执行一个动画的前进和反转,奇特证就是从哪里来旧回到哪里,比如新文字从右边进来,那么老的文字就从右边出去,总体上动画的执行必须是顺序的

那么我们可以实现自己想要的效果嘛,比如右边进,左边出。其实这样是可以的,AnimatedSwitcher 我们也不用改,我们可以改一改动画API FlideTransition,所有的动画都是在 AnimationWidget 基础上写的

针对这个需求,我们仿照 FlideTransition 内部实现,把动画在反转时把 X 轴的值加个-号就是我们要的效果啦,大多数时候我们都是用这种思路实现的

这个例子是官方文档上面的,代码上我多少改了一下,主要是在使用上更方便一点,封装度高了一点,针对 X/Y 轴做了封装

  • 这是对 SlideTransition 的改造,内部试用了 SlideTransition 的原始实现,核心就是在动画执行反转时对 Offset 坐标数据进行响应的处理,记住这个套路,其他的也一样

enum FreeSlideTransitionMode {
  reverseX,
  reverseY,
}

class FreeSlideTransition extends AnimatedWidget {
  
  Animation<Offset> animation;
  var child;
  Offset begin;
  Offset end;
  FreeSlideTransitionMode type;

  // x,y 轴反转播放时不同的数据处理,用 map 承载
  Map<FreeSlideTransitionMode, Function(Animation animation, Offset offset)> worksMap = {
    // x 轴反转操作,典型应用,右进左出
    FreeSlideTransitionMode.reverseX: (Animation animation, Offset offset) {
      if (animation.status == AnimationStatus.reverse) {
        return offset = Offset(-offset.dx, offset.dy);
      }
      return offset;
    },
    FreeSlideTransitionMode.reverseY: (Animation animation, Offset offset) {
      if (animation.status == AnimationStatus.reverse) {
        return offset = Offset(offset.dx, -offset.dy);
      }
      return offset;
    },
  };

  // 构造方法的多态,写着有点麻烦,看起来也不省事
  FreeSlideTransition(this.type, this.child,
      {Key key, Animation<double> animation, Offset begin, Offset end})
      : super(key: key, listenable: animation) {
    this.animation = Tween<Offset>(begin: begin, end: end).animate(animation);
  }

  FreeSlideTransition.reverseX(
      {Widget child,
      Animation<double> animation,
      Key key,
      Offset begin,
      Offset end})
      : this(FreeSlideTransitionMode.reverseX, child,
            key: key, animation: animation, begin: begin, end: end);

  FreeSlideTransition.reverseY(
      {Widget child,
      Animation<double> animation,
      Key key,
      Offset begin,
      Offset end})
      : this(FreeSlideTransitionMode.reverseY, child,
            key: key, animation: animation, begin: begin, end: end);

  @override
  Widget build(BuildContext context) {
    var offset = animation.value;
    offset = worksMap[type](animation, offset);

    // SlideTranslation 内部就是这么实现的
    return FractionalTranslation(
      translation: offset,
      child: child,
    );
  }
}
  • 具体试用:在 Column 中有时候位置会失效,中间加上一个 padding 过度就好了
class Test3 extends State<TestWidget> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          AnimatedSwitcher(
            duration: const Duration(milliseconds: 500),
            transitionBuilder: (Widget child, Animation<double> animation) {
              //执行缩放动画
              return FreeSlideTransition.reverseY(
                animation: animation,
                begin: Offset(0, 1),
                end: Offset(0, 0),
                child: child,
              );
            },
            child: Text(
              '$_count',
              //显示指定key,不同的key会被认为是不同的Text,这样才能执行动画
              key: ValueKey<int>(_count),
              style: Theme.of(context).textTheme.display1,
            ),
          ),

//          Padding(
//            padding: EdgeInsets.all(20.0),
//          ),
//          Text("AAA"),

          RaisedButton(
            child: Text(
              '+1',
            ),
            onPressed: () {
              setState(() {
                _count += 1;
              });
            },
          ),
        ],
      ),
    );
  }
}

好了就是这样,有的朋友可能看官方文档时挺简单的,但是还是推荐大家自己写一下,文档的代码封装度不够,也有些繁琐,自己试试也能练习一下功能封装不是,至少我这个 FreeSlideTransition 是可以放到 lib 库里面的

有的朋友不理解为什么还要在 AnimatedSwitcher 里面自己写 tween 呢。其实一开始我也是不理解的,后来一想啊,AnimationControl 的数值默认是 [0-1] 的,我们要是想用自己的数据设置,可不就得自己写 Tween 啊


AnimatedCrossFade

AnimatedCrossFade 不同布局切换时可以显示动画,但是不能自己设置动画,默认就是淡入淡出,并且在大小不通切换时显示不好

我走一个 gif,让大家看看 widget 在大小不同切换时那种强烈的突兀感,小变大还行,但是大变小就不行了

AnimatedCrossFade 可惜不能自己设置动画,默认也就是个渐变动画,限制挺大的

代码上呢,需要我们指定显示 frist widget 还是 second widget,我们在 widget 外部写一个标记,setState 改变这个标记就能触发动画了

class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {
  var isFristShow = true;

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        AnimatedCrossFade(
          firstChild: Container(
            alignment: Alignment.center,
            width: 200,
            height: 200,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: Colors.blueAccent,
            ),
            child: Text("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
          ),
          secondChild: Container(
            alignment: Alignment.center,
            width: 300,
            height: 300,
            child: Text("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
            decoration:
                BoxDecoration(shape: BoxShape.rectangle, color: Colors.pinkAccent),
          ),
          crossFadeState: isFristShow
              ? CrossFadeState.showFirst
              : CrossFadeState.showSecond,
          duration: Duration(milliseconds: 300),
          reverseDuration: Duration(milliseconds: 300),
        ),
        RaisedButton(
          child: Text("AAA"),
          onPressed: () {
            setState(() {
              isFristShow = !isFristShow;
            });
          },
        ),
      ],
    );
  }
}

DecoratedBoxTransition

DecoratedBoxTransition 是边框变化动画,只能进行边框变化的动画,但他是我们一直所追求的,他能实现 widget 形状变化的自然过度,当然和我们说 AnimatedContainer 时一样,起形状自然过度依然还是依靠圆角矩形的圆角度数实现的

DecoratedBoxTransition 属性:

  • child -
  • decoration - 表示由外传递进来的动画属性值的变化,通过获取其值,填充到child的边框上 position - 表示边框动画的位置,可以是前景位置或者是背景位置,前景位置会盖住child元素

DecoratedBoxTransition 的 child 不用设置背景,DecorationTween 数值生成器会自动给 child 加上设定的 shape 背景的

    _animation = DecorationTween(
      begin: BoxDecoration(
        borderRadius: BorderRadius.all(Radius.circular(0.0)),
        color: Colors.red,
      ),
      end: BoxDecoration(
        borderRadius: BorderRadius.all(Radius.circular(100.0)),
        color: Colors.green,
      ),
    )

class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {
  Animation<Decoration> _animation;
  AnimationController _controller;
  Animation _curve;

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

    _controller = AnimationController(
      duration: const Duration(milliseconds: 1000),
      vsync: this,
    );
    _curve = CurvedAnimation(parent: _controller, curve: Curves.fastOutSlowIn);
    _animation = DecorationTween(
      begin: BoxDecoration(
        borderRadius: BorderRadius.all(Radius.circular(0.0)),
        color: Colors.red,
      ),
      end: BoxDecoration(
        borderRadius: BorderRadius.all(Radius.circular(100.0)),
        color: Colors.green,
      ),
    ).animate(_curve)
      ..addStatusListener((AnimationStatus state) {
        if (state == AnimationStatus.completed) {
          _controller.reverse();
        } else if (state == AnimationStatus.dismissed) {
          _controller.forward();
        }
      });
    _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Column(
      children: <Widget>[
        DecoratedBoxTransition(
          position: DecorationPosition.background,
          decoration: _animation,
          child: Container(
              child: Container(
            padding: EdgeInsets.all(50),
            child: Text("AAAAAA"),
          )),
        )
      ],
    );
  }
}

AnimatedDefaultTextStyle

AnimatedDefaultTextStyle 文字样式改变时的切换动画,主要呈现的大小变换方面的动画,颜色的渐变过度不明显,但是体验不好的地方在于,大小字切换时字体粗细的变化真实有点辣眼,尤其是文字字号大的时候

吐槽一下,Google 团队的代码质量也在下降啊,在前有 AnimatedContainer 的前提下,AnimatedDefaultTextStyleAnimatedContainer 的设计,使用,命名套路完全不一样,Google 你是要闹哪样,有代码质量审查吗?AnimatedDefaultTextStyleAnimatedContainer 他们两者其实是一种东西,但是开发者之间没有沟通,搞出2种东西,真是用着蛋疼啊

AnimatedDefaultTextStyle 使用上作为 text 的外层 widget 来用的,其实搞成 AnimatedContainer 那样直接代替 text 多好,其实 text 那些属性大多数 AnimatedDefaultTextStyle 也有,搞得不乱不类的,真让人火大

动画的触发一样还是通过外层变量控制,通过 staState 来触发

class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {

  var _isSelected = true;
  var info1 = "Flutter !!!";
  var info2 = "is not you !!!";

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: <Widget>[
        AnimatedDefaultTextStyle(
          softWrap: false,
          textAlign: TextAlign.right,
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
          curve: Curves.linear,
          duration: Duration(milliseconds: 300),
          child: Text( info2),
          style: _isSelected
              ? TextStyle(
                  fontSize: 10.0,
                  color: Colors.red,
                  fontWeight: FontWeight.bold,
                )
              : TextStyle(
                  fontSize: 30.0,
                  color: Colors.black,
                  fontWeight: FontWeight.w300,
                ),
        ),
        RaisedButton(
          child: Text("AA"),
          onPressed: () {
            setState(() {
              _isSelected = !_isSelected;
            });
          },
        ),
      ],
    );
  }
}

文字若是切换前后行数不一样的话,动画就比较难看了,大家看下面这个例子,在用的时候大家切记一行变多行


AnimatedModalBarrier

  • AnimatedModalBarrier 颜色改变的变换动画,特殊的地方在于其必须放到所操的 widget 的 child 中,有明确的应用场景,就是点击时改变背景色,比如 dialog 弹出时,背景变灰色

AnimatedModalBarrier 有几个参数,除了 color 外具体有啥用我也不知道:

  • color - 颜色值动画变化
  • dismissible - 是否触摸当前ModalBarrier将弹出当前路由,配合点击事件弹出路由使用
  • semanticsLabel - 语义化标签
  • barrierSemanticsDismissible - 语义树中是否包括ModalBarrier语义

color 这里接收一个 animation 动画对象,这样的话我们可以自己设置前后颜色值,时长,添加控制等,API 自由度比其他同类变换动画 API 强多了

下面的例子里我设置了一个循环播放

class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {

  AnimationController _controller;
  Animation _curve;

  Animation<Color> animation;

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

    _controller = AnimationController(
      duration: const Duration(milliseconds: 1000),
      vsync: this,
    )
      ..addStatusListener((AnimationStatus state) {
        if (state == AnimationStatus.completed) {
          _controller.reverse();
        } else if (state == AnimationStatus.dismissed) {
          _controller.forward();
        }
      });

    animation = ColorTween(
      begin: Colors.blue,
      end: Colors.pinkAccent,
    ).animate(_controller);
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      fit: StackFit.expand,
      children: <Widget>[
        Container(
          width: 300,
          height: 300,
          child: AnimatedModalBarrier(
            semanticsLabel: "StackBarrier",
            barrierSemanticsDismissible: true,
            dismissible: true,
            color: animation,
          ),
        ),
        Positioned(
          left: 20,
          top: 20,
          child: RaisedButton(
            child: Text("AA"),
            onPressed: () {
              _controller.forward();
            },
          ),
        ),
      ],
    );
  }
}

AnimatedOpacity

AnimatedOpacity 透明度的变化动画,没什么可说的,看代码就是,下面我把颜色变换的动画一起加进来

class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {

  AnimationController _controller;
  Animation<Color> animation;
  double opacity = 1.0;

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

    _controller = AnimationController(
      duration: const Duration(seconds: 1),
      vsync: this,
    );

    animation = ColorTween(
      begin: Colors.blue,
      end: Colors.pinkAccent,
    ).animate(_controller);
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      fit: StackFit.expand,
      children: <Widget>[
        AnimatedOpacity(
          curve: Curves.fastOutSlowIn,
          opacity: opacity,
          duration: Duration(seconds: 1),
          child: Container(
            width: 300,
            height: 300,
            child: AnimatedModalBarrier(
              semanticsLabel: "StackBarrier",
              barrierSemanticsDismissible: true,
              dismissible: true,
              color: animation,
            ),
          ),
        ),
        Positioned(
          left: 20,
          top: 20,
          child: RaisedButton(
            child: Text("AA"),
            onPressed: () {
              setState(() {
                opacity = 0.3;
                _controller.forward();
              });
            },
          ),
        ),
      ],
    );
  }
}

AnimatedPhysicalModel

AnimatedPhysicalModel 属性如下,有点多,大家仔细看,看不懂的看下面 demo 就行:

  • shape:阴影的形状
  • clipBehavior:阴影的裁剪方式
    • Clip.none:无模式
    • Clip.hardEdge:裁剪速度稍快,但容易失真,有锯齿
    • Clip.antiAlias:裁剪边缘抗锯齿,使得裁剪更平滑,这种模式裁剪速度比antiAliasWithSaveLayer快,但是比hardEdge慢
    • Clip.antiAliasWithSaveLayer:裁剪后具有抗锯齿特性并分配屏幕缓冲区,所有后续操作在缓冲区进行
  • borderRadius:背景的边框
  • elevation:阴影颜色值的深度
  • color:背景色
  • animateColor:背景色是否用动画形式展示
  • shadowColor:阴影的动画值
  • animateShadowColor:阴影是否用动画形式展示

看着很多,但是大家不要懵,其实就一个有用,就是 shadowColor,我们改 shadowColor 就会触发动画,不过 color 这个属性必须设置,要不报错

整体动画来说,我是真不知道这个动画应用在哪里,是不是像上面那个一样,做点击后背景色的变化呢,谁知道呢...

class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {
 
  var isShadow = true;

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      fit: StackFit.loose,
      children: <Widget>[
        AnimatedPhysicalModel(
          curve: Curves.fastOutSlowIn,
          color: Colors.grey.withOpacity(0.2),
          clipBehavior: Clip.antiAliasWithSaveLayer,
          borderRadius: BorderRadius.circular(12.0),
          animateColor: true,
          animateShadowColor: true,
          shape: BoxShape.rectangle,
          shadowColor: isShadow ? _shadowColor1 : _shadowColor2,
          elevation: 5.0,
          duration: Duration(milliseconds: 300),
          child: Container(
            width: 200,
            height: 200,
            child: Text("AA"),
          ),
        ),
        Positioned(
          left: 20,
          top: 20,
          child: RaisedButton(
            child: Text("AA"),
            onPressed: () {
              setState(() {
                isShadow = !isShadow;
              });
            },
          ),
        ),
      ],
    );
  }
}

AnimatedPositioned

AnimatedPositioned 这个大家看名字就知道了,就是严格按照之前的 API 命名的,这样才好嘛,这样才会一看就知道是干啥的,这里再次 diss 一下其他垃圾的起名和设计

这个我不想说了,大家看代码都清楚

class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {
 
  var isPosition = true;

  var top1 = 20.0;
  var left1 = 20.0;
  var width1 = 200.0;
  var height1 = 200.0;

  var top2 = 100.0;
  var left2 = 100.0;
  var width2 = 300.0;
  var height2 = 300.0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      fit: StackFit.expand,
      children: <Widget>[
        AnimatedPositioned(
          top: isPosition ? top1 : top2,
          left: isPosition ? left1 : left2,
          width: isPosition ? width1 : width2,
          height: isPosition ? height1 : height2,
          child: Container(
            color: Colors.blueAccent,
          ),
          duration: Duration(milliseconds: 300),
        ),
        Positioned(
          left: 20,
          top: 20,
          child: RaisedButton(
            child: Text("AA"),
            onPressed: () {
              setState(() {
                isPosition = !isPosition;
              });
            },
          ),
        ),
      ],
    );
  }
}

AnimatedSize

上面看了这么多了,到这大家啥套路都知道了吧,就是效果不是很满意,看下面的 gif,大家能看到若是因为 widget 的大小变化而造成 widget 有位移的话,那么会进行位移动画,而大小就没动画了,这点挺不爽的

class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {
  double size1 = 200;
  double size2 = 300;

  var isSize = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        RaisedButton(
          child: Text("AA"),
          onPressed: () {
            setState(() {
              isSize = !isSize;
            });
          },
        ),
        AnimatedSize(
          alignment: Alignment.center,
          curve: Curves.fastOutSlowIn,
          vsync: this,
          duration: Duration(seconds: 1),
          reverseDuration: Duration(seconds: 2),
          child: Container(
            width: isSize ? size1 : size2,
            height: isSize ? size1 : size2,
            color: Colors.blueAccent,
          ),
        ),
      ],
    );
  }
}