Flutter 动画详解

362 阅读11分钟

效果如下:

分享是每个优秀的程序员所必备的品质


一、基本概念

Animation

抽象类。本身与UI渲染没有任何关系,主要的功能就是保存动画的插值和状态。 可以通过它来监听动画的每一帧并执行状态的变化 addListener():为Animation添加“动画绘制”监听器,每一帧都会被调用。常见的行为就是改变状态后调用setState()来触发UI重绘。 addStatesListener():为Animation添加“动画状态改变”监听器,如开始、结束等。

注意: 动画期间会不停调用动画widget所在的build方法。

Curve

动画的过程可以是匀速、匀加速、先加速后减速等,Flutter中通过Curve(曲线)来描述动画过程,将匀速动画称为线性的(Curves.linear),将非匀速动画称为非线性动画。 一般通过CurveAnimation来指定动画的曲线

// linear:匀速的
// decelerate:匀减速
// ease:开始加速,后面减速
// easeIn:开始慢,后面快
// easeOut:开始快,后面慢
// easeInOut:开始慢,然后加速,最后再减速
final CurveAnimation curve = CurveAnimation(parent: controller, curve: Curve.easeIn);

也可以自定义Curve,例如自定义一个正弦曲线

// 正弦曲线
class ShakeCurve extends Curve{
  @override
  double transform(double t) {
    return sin( t * pi * 2); 
  }
}

AnimationController

用于控制动画,Animation的子类,包含了 forward():正向启动(动画正在从开始处运行到结束处) reverse():反向启动 stop():停止 dismissed():动画停止在开始处 completed():动画结束(停止在结束处) AnimationController会在动画的每一帧生成一个新的值,默认情况下,会在给定的时间段内,线性生成从0.0-1.0(默认区间)的数字。代码如下:

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

其生成的数字区间可以通过lowerBound和lowerBound来指定,代码如下:

AnimationController controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 1),
      lowerBound: 10.0,
      upperBound: 20.0
    );

Ticker

从上面我们注意到,当创建一个AnimationController时,需要传递一个vsync参数,它接受一个TickerProvider的对象,主要职责是创建Ticker,代码如下:

abstract class TickerProvider{
  // 通过一个回调创建一个Ticker
  Ticker createTicker(TickerCallback onTick);
}

Flutter应用在每次启动时都会绑定一个SchedulerBinding,SchedulerBinding可以为每一次屏幕添加回调,而Ticker就是通过SchedulerBinding来添加屏幕刷新回调的,每次屏幕刷新都会调用TickerCallback。使用Ticker(而不是Timer)来驱动动画会防止屏幕外动画(动画的UI不再当前屏幕时,如锁屏)消耗不必要的资源,因为在Flutter中,屏幕刷新会通知到绑定的SchedulerBinding,而Ticker是受SchedulerBinding驱动的,由于锁屏屏幕hi停止刷新,所以Ticker将不会再触发。 通常我们会将SingleTickerProviderStateMixin添加到State中然后将State对象作为vsync的值(this)。

Tween

补间动画,默认下AnimationController对象值是[0.0,1.0],可以使用Tween来添加映射以生成不同的范围或是不同的数据类型(double、Color、EdgeInsets...)代码如下

Tween(begin: 100.0,end: 300.0)

注意:Tween继承自Animatable<T>,而不是Animation<T>,Animatable主要定义动画值的映射规则。

Tween对象不存储任何状态,提供了evaluate <Animation<double> animation>方法来获取动画当前的值(animation.value())

Tween仅仅是映射,动画的控制依然由 AnimationController 控制,因此需要 Tween.animate(controller) 将控制器传递给Tween。

如下代码:在1s时间内生成从0到255 的int值

AnimationController controller = AnimationController(
  vsync: this,
  duration: Duration(seconds: 1),
);
// 注意:animation()返回的是一个Animation,而不是Animatable
Animation<int> alpha = IntTween(begin: 0,end: 255).animate(controller);

二、示例

下面结合一些示例来深入了解下Flutter动画

1、透明度动画

透明度从1.0变为0.0,代码如下:

//需要混入 SingleTickerProviderStateMixin
class Demo1 extends StatefulWidget {
  @override
  _Demo1State createState() => _Demo1State();
}
class _Demo1State extends State<Demo1> with SingleTickerProviderStateMixin{
  AnimationController _controller;
  Animation _animation ;
  @override
  void initState() {
    super.initState();
    // AnimationController继承于Animation,可以调用addListener
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 1))..addListener(() {
      setState(() {});
    });
    // Interval : begin 参数 代表 延迟多长时间开始 动画  end 参数 代表 超过多少 直接就是 100% 即直接到动画终点
    _animation = Tween(begin: 1.0,end: 0.1).animate(CurvedAnimation(parent: _controller,curve: Interval(0.0,0.5,curve: Curves.linear)));
    // _animation有不同的构建方式
    // _animation = Tween(begin: 1.0,end: 0.2).chain(CurveTween(curve: Curves.easeIn)).animate(_controller);
    //  _animation = _controller.drive(Tween(begin: 1.0,end: 0.1)).drive(CurveTween(curve: Curves.linearToEaseOut));
  }
  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () => _controller.forward(),
      child: Opacity(
        opacity: _animation.value,
        child: Container(
          width: 100,height: 100, color: Colors.greenAccent,
          child: Center(child: Text("Demo1"),),
        ),
      ),
    );
  }
  @override
  void dispose() {
    _controller?.dispose();
    super.dispose();
  }
}

2、颜色变化 Color.lerp 两种颜色之间线性插值

红 —> 绿

class Demo2 extends StatefulWidget {
  @override
  _Demo2State createState() => _Demo2State();
}

class _Demo2State extends State<Demo2> with SingleTickerProviderStateMixin{

  AnimationController _controller;
  Color _color = Colors.red;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 1))
      ..addListener(() {
        setState(() {
           _color = Color.lerp(Colors.red, Colors.green, _controller.value);
        });
      });
  }
  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: (){
        _controller.forward();
      },
      child: Container(
        height: 100,width: 100,
        color: _color,
        child: Center(child: Text("Demo2"),),
      ),
    );
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

3、widget大小变化

class Demo3 extends StatefulWidget {
  @override
  _Demo3State createState() => _Demo3State();
}

class _Demo3State extends State<Demo3> with SingleTickerProviderStateMixin{
  double _size = 100;
  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 1),lowerBound: 100,upperBound: 200)
      ..addListener(() {
        setState(() {
          _size = _controller.value;
        });
      })..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          _controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          _controller.forward();
        }
      });
  }

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: (){
        _controller.forward();
      },
      child: Container(
        width: _size,height: _size,
        color: Colors.redAccent,
        child: Center(child: Text("Demo3"),),
      ),
    );
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

其实系统提供了大量的Tween动画,大大方便了的使用,常用到的数据类型一般都提供了,需要的时候不妨先敲敲看。

Tween.png

例如常见的位移动画使用EdgeInsetsTween,颜色变化可以使用ColorTween来实现(查看源码,其本质上也是使用 Color.lerp 实现的) 使用ColorTween代码如下:动画效果同Demo3相同。

class ColorTweenDemo extends StatefulWidget {
  @override
  _ColorTweenDemoState createState() => _ColorTweenDemoState();
}

class _ColorTweenDemoState extends State<ColorTweenDemo> with SingleTickerProviderStateMixin{
  AnimationController _controller;
  Animation <Color> _animation;

  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 1))
      ..addListener(() {
        setState(() { });
      });
    _animation = ColorTween(begin: Colors.red,end: Colors.green).animate(_controller);
  }

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: (){
        _controller.forward();
      },
      child: Container(
        height: 100,width: 100,
        color: _animation.value,
      ),
    );
  }
}

Demo1_Demo2_Demo3.gif

4、多种动画组合

混入TickerProviderStateMixin, 大小变化 + 圆角变化 + 颜色变化

class Demo4 extends StatefulWidget {
  @override
  _Demo4State createState() => _Demo4State();
}
class _Demo4State extends State<Demo4> with TickerProviderStateMixin{

  // 可以每个动画都创建各自的AnimationController来控制和监听不同的状态
  AnimationController _controller;

  Animation <double> _sizeAnimation;
  Animation <BorderRadius> _radiusAnimation;
  Animation <Color> _colorAnimation;

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

    _controller = AnimationController(vsync: this,duration: Duration(seconds: 1))..addListener(() {
      setState(() {});
    })..addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      }
    });
    _sizeAnimation = Tween(begin: 100.0,end: 200.0).chain(CurveTween(curve: Curves.linear)).animate(_controller);
    _radiusAnimation = BorderRadiusTween(begin: BorderRadius.zero,end: BorderRadius.circular(100)).animate(_controller);
    _colorAnimation = ColorTween(begin: Colors.redAccent,end: Colors.yellowAccent).chain(CurveTween(curve: Curves.linear)).animate(_controller);
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Center(
        child: InkWell(
          onTap: (){
            _controller.forward();
          },
          child: Container(
            width: _sizeAnimation.value,height: _sizeAnimation.value,
            decoration: BoxDecoration(
              border: Border.all(width: 5,color: Colors.greenAccent),
              color: _colorAnimation.value,
              borderRadius: _radiusAnimation.value
            ),
            child: Center(child: Text("Demo4"),),
          ),
        )
      )
    );
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

效果图: Demo4.gif

5、AnimatedWidget 之 位移

现在我们可以发现:

  • 每次通过addListener()和setState()来更新UI,代码显得比较冗余;
  • setState()意味着整个 State 类中的 build 方法就会被重新执行。

为了解决上面的问题,我们可以使用 AnimatedWidget将需要动画的Widget分离出来。 ``AnimatedWidget`封装调用了setState()的细节

以位移动画为例:

class Demo5 extends StatefulWidget {
  @override
  _Demo5State createState() => _Demo5State();
}

class _Demo5State extends State<Demo5> with SingleTickerProviderStateMixin{
  AnimationController _controller;
  Animation <EdgeInsets>_animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 2))..addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      }
    });
    _animation = _controller.drive(EdgeInsetsTween(begin: EdgeInsets.fromLTRB(50, 50, 0, 0),end: EdgeInsets.only(top: 200,left: 200)));
  }

  @override
  Widget build(BuildContext context) {
    Future.delayed(Duration(seconds: 1),(){
      _controller.forward();
    });
    return EdgeInsetsDemo(animation: _animation,);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}
// 使用AnimatedWidget分离
class EdgeInsetsDemo extends AnimatedWidget{

  EdgeInsetsDemo({Key key,Animation<EdgeInsets> animation}):super(key:key,listenable :animation);

  @override
  Widget build(BuildContext context) {
    final Animation<EdgeInsets> animation = listenable;
    return Container(
      margin: animation.value,
      width: 100,
      height: 100,
      color: Colors.redAccent,
    );
  }
}

效果如下: Demo5.gif

6、AnimatedBuilder 之 矩阵变化

使用AnimatedWidget可以从动画分离出Widget,但是动画的渲染过程仍在AnimatedWidget中,如果我们在添加一个Widget动画,需要再实现一个AnimatedWidget,然而这很不优雅,而AnimatedBuilder可以将渲染逻辑分离出来。

示例使用AnimatedBuilder让图片绕X轴旋转。 代码如下:

class Demo6 extends StatefulWidget {
  @override
  _Demo6State createState() => _Demo6State();
}

class _Demo6State extends State<Demo6> with SingleTickerProviderStateMixin{
  AnimationController _controller;
  Animation <Matrix4> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 2))..addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      }
    })..addListener(() {
      setState(() {
      });
    });
    // 矩阵变化,应用很广泛,例如时钟或电子书翻页动画等
    _animation = Matrix4Tween(begin: Matrix4.identity()..rotateX(0.0),end: Matrix4.identity()..rotateX(pi)).animate(_controller);
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: InkWell(
        onTap: (){
          _controller.forward();
        },
        child: AnimatedBuilderDemo(animation: _animation,)
      ),
    );
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}
// 使用AnimatedBuilder重构
class AnimatedBuilderDemo extends AnimatedWidget{

  AnimatedBuilderDemo({Key key,Animation<Matrix4> animation}):super(key :key,listenable:animation);
  // 看起来child被指定了两次,但实际上是将外部child传递给AnimationBuilder后AnimationBuilder再将其传递给匿名构造器,
  // 然后将该对象用作其子对象,最终会将AnimationBuilder返回的对象插入到Widget树中。
  @override
  Widget build(BuildContext context) {
    final Animation animation = listenable;
    return AnimatedBuilder(
      animation: animation,
      child: Image.asset("assets/g.jpg",),
      builder: (BuildContext context,Widget child){
        return  Container(
          width: 120,height: 120,
          transform: animation.value,
          child: child,
        );
      },
    );
  }
}

效果如下: Demo6.gif

好处:

  • 与AnimatedWidget一样,不用显示调用setSate();
  • 动画构建范围缩小了,setSate()会在父组件中调用,会导致父组件的build重新调用,意味着它的子 Widget 也会重新 build 。而有了builder,只会让动画的widget自身的build重新调用,避免不必要的rebuild。
  • AnimatedBuilder可以封装常见的过渡效果来复用动画,如FadeTransition、ScaleTransition、Sizetransitin等,很多时候这些预置的过渡类都可以复用。

7、AnimatedContainer :动画过渡组件

前面介绍的动画中,使用者要自己提供一个且需手动管理的AnimationController,大大增加了使用的复杂性,Flutter SDK中预置了很多动画过渡组件。

AnimatedContainer:当Container属性发生变化时候会执行过渡动画到新的状态
AnimatedPadding:在Padding发生变化会执行过渡动画到新的状态
AnimatedPositioned:配合Stack使用,当定位发生变化时,会执行过渡动画到新的状态
AnimatedOpacity:当透明度发生变化时,会执行过渡动画到新的状态
AnimatedAlign:当alignment发生变化时,会执行过渡动画到新的状态
AnimatedDefaultTextStyle:当字体样式发生变化时,会执行过渡动画到新的状态
。。。

代码如下:

class Demo7 extends StatefulWidget {
  @override
  _Demo7State createState() => _Demo7State();
}
class _Demo7State extends State<Demo7> {

  Duration _duration = Duration(seconds: 1);
  bool _animation = false;

  @override
  Widget build(BuildContext context) {
    
    return Center(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          Spacer(flex: 2,),
          AnimatedContainer(
            curve: Curves.linear,
            duration: _duration,
            width: _animation ? 200 : 100,height: _animation ? 200 : 100,
            decoration: BoxDecoration(
              borderRadius: _animation ? BorderRadius.circular(100) : BorderRadius.zero,
              color: _animation ? Colors.yellowAccent : Colors.redAccent,
              border: Border.all(width: 5,color: _animation ? Colors.greenAccent : Colors.black87)
            ),
            onEnd: (){ // 动画结束的回调
              setState(() => _animation = !_animation );
            },
            child: InkWell(
              onTap: (){
                setState(() => _animation = !_animation );
              },
            ),
          ),
          Spacer(),
          AnimatedContainer(
            alignment: Alignment.center,
            duration: _duration,
            width: _animation ? 200 : 150,height: _animation ? 100 : 80,
            decoration: BoxDecoration(
              color: Colors.greenAccent,
              borderRadius: _animation == false ? BorderRadius.circular(40) : BorderRadius.zero,
            ),

            child: AnimatedDefaultTextStyle(
              duration: _duration,
              child: Text("Hello world"),
              style: _animation ? TextStyle(color: Colors.black87,fontSize: 12,fontWeight: FontWeight.w100) : TextStyle(color: Colors.redAccent,fontSize: 20,fontWeight: FontWeight.w800),
            ),

          ),
          Spacer(),
          // AnimatedCrossFade 2个组件在切换时出现交叉渐入的效果,需要设置动画前、动画后2个子控件即可。
          AnimatedCrossFade(
            duration: _duration,
            crossFadeState: _animation ? CrossFadeState.showSecond : CrossFadeState.showFirst,
            firstChild: Container(
              alignment: Alignment.center,
              width: 100,height: 120,
              color: Colors.redAccent,
              child: Text("第一个",style: TextStyle(color: Colors.greenAccent),),
            ),
            secondChild: Container(
              alignment: Alignment.center,
              width: 100,height: 100,
              decoration: BoxDecoration(
                  color: Colors.greenAccent,
                  borderRadius: BorderRadius.circular(50)
              ),
              child: Text("第二个",style: TextStyle(color: Colors.redAccent)),
            ),
          ),
          Spacer(flex: 2,)
        ],
      ),
    );
  }
}

效果如下: Demo7.gif

再如Demo1中的透明度变化的动画代码可以简化成如下代码:

// 效果同上面的Demo1效果相同
class Demo7_1 extends StatefulWidget {
  @override
  _Demo7_1State createState() => _Demo7_1State();
}

class _Demo7_1State extends State<Demo7_1> {
  bool _animation = false;
  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedOpacity( // 透明度过渡动画组件
        duration: Duration(seconds: 1),
        opacity: _animation ? 0.0 : 1.0,
        onEnd: (){
          setState(() => _animation = !_animation );
        },
        child: Container(
          width: 100,height: 100,
          color: Colors.redAccent,
          child: InkWell(
            onTap: (){
              setState(() => _animation = !_animation );
            },
          ),
        ),
      ),
    );
  }
}

8、AnimatedIcon

再说一个比较有意思的过渡动画组件:AnimatedIcon ,这是Flutter提供的动画图标。 比如之前做过视频播放的按钮,点击按钮后 ,图标 ▶️ —> ⏸ 的变化,这些Flutter已经内置了,而且Icon在切换动画后默认指定了另一个Icon。图标 ▶️ 动画后就默认指定的Icon是 ⏸ 。

代码如下:

class Demo8 extends StatefulWidget {
  @override
  _Demo8State createState() => _Demo8State();
}

class _Demo8State extends State<Demo8> with SingleTickerProviderStateMixin{
  bool _animation = false;

  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(duration: Duration(seconds: 1),vsync: this,)
      ..addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      };
    });
  }

  @override
  Widget build(BuildContext context) {
    return GridView(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 3,
      ),
      children: <Widget>[
        setIcon(AnimatedIcons.play_pause),
        setIcon(AnimatedIcons.add_event),
        setIcon(AnimatedIcons.arrow_menu),
        setIcon(AnimatedIcons.close_menu),
        setIcon(AnimatedIcons.ellipsis_search),
        setIcon(AnimatedIcons.event_add),
        setIcon(AnimatedIcons.home_menu),
        setIcon(AnimatedIcons.list_view),
        InkWell(onTap: () => _controller.forward())
      ],
    );
  }
  Widget setIcon (AnimatedIconData iconData) {
    return Center(
      child: AnimatedIcon(
        size: 30,
        icon: iconData,
        progress: _controller,
      ),
    );
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

效果如下:Demo8.gif

9、路由转场动画

在路由跳转的时候一般会用到MaterialPageRoute或iOS风格的CupertinoPageRoute,默认的是上下滑动切换和左右滑动切换。

那么如何来自定义路由切换动画呢?答案就是使用PageRouteBuilder。例如以渐隐渐入的动画来实现路由过程:

class Demo9 extends StatefulWidget {
  @override
  _Demo9State createState() => _Demo9State();
}

class _Demo9State extends State<Demo9> {
  @override
  Widget build(BuildContext context) {
    return CupertinoButton(
      padding: EdgeInsets.zero,
      onPressed: (){
        Navigator.push(
          context,
          PageRouteBuilder(
            transitionDuration: Duration(milliseconds: 500),
            pageBuilder: (BuildContext context,Animation animation,Animation secondAnimation){
              return FadeTransition(
                opacity: animation,
                child: NewPage(),
              );
            },
          ),
        );
      },
      child: Container(
        alignment: Alignment.center,
        width: double.infinity,height: double.infinity,
        child: Text("点击进入下一页",style: TextStyle(color: Colors.black,fontSize: 30),),
      ),
    );
  }
}

效果如下: Demo9.gif

我们再将渐入转场封装一下,

class RCFadeRoute extends PageRoute{

  final WidgetBuilder builder;

  @override
  final Duration transitionDuration;

  @override
  final bool opaque;

  @override
  final bool barrierDismissible;

  @override
  final Color barrierColor;

  @override
  final String barrierLabel;

  @override
  final bool maintainState;

  RCFadeRoute({
    @required this.builder,
    this.transitionDuration = const Duration(milliseconds: 250),
    this.opaque = true,
    this.barrierDismissible = false,
    this.barrierColor,
    this.barrierLabel,
    this.maintainState = true
  });

  @override
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation)  => builder(context);

  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
    return FadeTransition(
      opacity: animation,
      child: builder(context),
    );
  }
}

那跳转动画的代码就简化成如下:

Navigator.push(context,
  RCFadeRoute(builder: (context){
    return NewPage();
   }),
 );

10、Hero动画

Hero是可以在路由之间“飞行”的Widget。由于共享的widget在新旧看路由页面上的位置、外观有所差异,所以在切换时会从旧路由逐渐过渡到新路由中指定的位置,这样就产生一个Hero动画,只需要用Hero将要共享的widget包装起来,并提供一个相同的tag即可。 具体代码如下:

参数描述
tag过渡元素组件的标记值
createRectTween位置动画
flightShuttleBuilder动画过程组件
placeholderBuilder占位符组件
transitionOnUserGestures使用手势进行专场时,是否显示动画
class Demo10 extends StatefulWidget {
  @override
  _Demo10State createState() => _Demo10State();
}

class _Demo10State extends State<Demo10> {
  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      padding: EdgeInsets.only(top: 10,left: 10,right: 10),
      itemCount: 15,
      itemBuilder: (BuildContext content,int index){
        return Container(
          child: InkWell(
            child: Hero(
              tag: "assets/g.jpg"+"$index", // 唯一的标记,前后两个路由的Hero的tag必须相同
              child: Image.asset("assets/g.jpg",fit: BoxFit.cover,),
            ),
            onTap: (){
              Navigator.push(
                context,
                PageRouteBuilder(
                  transitionDuration: Duration(milliseconds: 250),
                  pageBuilder: (BuildContext context,Animation animation,Animation secondAnimation){
                    return FadeTransition(
                      opacity: animation,
                      child: HeroAnimationRoute("assets/g.jpg","assets/g.jpg"+"$index"),
                    );
                  },
                ),
              );
            },
          ),
        );
      },
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,mainAxisSpacing: 10.0,crossAxisSpacing: 10.0,
      ),
    );
  }
}
class HeroAnimationRoute extends StatelessWidget {
  final String imageName,tag;
  HeroAnimationRoute(this.imageName,this.tag);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black54,
      body: InkWell(
        onTap: (){
          Navigator.of(context).pop();
        },
        child: Center(
          child: Hero(
            tag: tag,
            child: Image.asset(imageName,fit: BoxFit.cover,),
          ),
        ),
      ),
    );
  }
}

效果如下: Demo10.gif


RCFlutterAnimation