如何玩转 Flutter 动画

1. 基本动画概念和相关类

  • Animation: flutter 动画库中的一个核心类,它生成指导动画的值;
  • AnimationController: Animation 的管理类。
  • CurvedAnimation: 用于定义非线性曲线动画.
  • Tween: 补间对象,用于计算动画使用的数据范围之间的插值 (例:Tween 可能会生成从红到蓝之间的色值,或者从0到255)
  • Listeners 和 StatusListeners: 监听动画状态改变。

Flutter 中的动画系统基于 Animation 对象的,widget 可以在 build 函数中读取 Animation 对象的当前值,并且可以监听动画的状态改变。

1.1. Animation

  1. Animation 是一个抽象类,它拥有其当前值和状态(完成或停止)。其中一个比较常见的Animation 类是 Animation< double >;
  2. Flutter 中的 Animation 对象是一个在一段时间内依次生成一个区间之间值的类。 Animation 对象的输出可以是线性的、曲线的、一个步进函数或者任何其他可以设计的映射。 根据 Animation 对象的控制方式,动画可以反向运行,甚至可以在中间切换方向;
  3. Animation 还可以生成除 < double > 之外的其他类型值,如:Animation< Color > 或 Animation< Size >;
  4. Animation 对象有状态。可以通过访问其 value 属性获取动画的当前值。
  5. Animation 对象本身和UI渲染没有任何关系。

1.2. AnimationController

AnimationController 是 Animation< double > 的子类:

  • AnimationController 会生成一系列的值,数字的产生与屏幕刷新有关,因此每秒钟通常会产生60个数字,(默认情况下,AnimationController 在给定的时间段内会线性的生成从0.0到1.0之间的数字)
  • 在生成每个数字后,每个 Animation 对象调用添加的 Listener 对象。

代码示例:

final AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 2000), 
    vsync: this
);

当创建一个 AnimationController 时,需要传递一个 vsync 必传参数,vsync 对象会绑定动画的定时器到一个可视的 widget,它的作用是避免动画相关UI不在当前屏幕时消耗资源:

  • 当 widget 不显示时,动画定时器将会暂停;
  • 当 widget 再次显示时,动画定时器重新恢复执行,
  • 如果要使用自定义的 State 对象作为 vsync 时,需要包含 TickerProviderStateMixin;

AnimationController 控制动画的方法:

  • forward:向前执行动画;
  • reverse:反向执行动画;
  • stop:停止动画;

1.3. CurvedAnimation

CurvedAnimation 是 Animation< double > 的子类:

  • 它可以将 AnimationController 定义为一个非线性曲线动画。

代码示例:

final CurvedAnimation curvedAnimation = CurvedAnimation(
    parent: controller, 
    curve: Curves.linear
);

curve 参数对象的有一些常量Curves(和Color类型有一些Colors是一样的)可以供我们直接使用,例如:linear、easeIn、easeOut、easeInToLinear等等。

1.4. Tween

Tween 继承自 Animatable< T >:

  • AnimationController 动画生成的值所在区间默认是是0.0到1.0,如果我们需要不同的范围或不同的数据类型,则可以使用 Tween 来配置动画以生成不同的范围或数据类型的值。

代码示例:

final Tween tweenAnim = Tween(
    begin: 50.0, 
    end: 150.0
).animate(curvedAnimation);
  • Tween 是一个无状态 stateless 对象,需要 begin 和 end 值,Tween 的唯一职责就是定义从输入范围到输出范围的映射。
  • 要使用 Tween 对象,需要调用 animate() 方法,传入一个 Animation<double> 对象,返回的是 Animation< T > 对象。

1.5. Listeners 和 StatusListeners

一个 Animation 对象可以拥有 Listeners 和 StatusListeners 监听器,可以用 addListener() 和 addStatusListener() 来添加:

  • addListener:

    • 只要动画的值发生变化,就会调用所有通过 addListener 添加的监听器;
    • Listener 最常见的行为是调用 setState() 来触发UI重建。
  • addStatusListener

    • 当动画的状态发生变化时,例如:开始、结束、向前移动或向后移动,都会通知所有通过 addStatusListener 添加的监听器;
    • 通常情况下,动画会从 dismissed 状态开始,表示它处于变化区间的开始点;
    • 举例来说,从 0.0 到 1.0 的动画在 dismissed 状态时的值应该是 0.0;
    • 动画进行的下一状态可能是 forward(比如从 0.0 到 1.0)或者 reverse(比如从 1.0 到 0.0);
    • 最终,如果动画到达其区间的结束点,则动画会变成 completed 状态。

接下来,我们结合一个个的例子熟悉它们的用法:

2. 动画示例

2.1 动画示例-1

image

主要代码如下:

class LookPage extends StatefulWidget {
  @override
  _CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}

class _CJAnimationWidgetState extends State<LookPage> with SingleTickerProviderStateMixin {

  AnimationController _controller;
  Animation<double> _curvedAnimation;
  Animation<double> _tweenAnimation;

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

    // 1. 创建 controller
    _controller = AnimationController(
      duration: Duration(milliseconds: 1000),
      vsync: this,
    );

    // 2. 创建 curvedAnimation
    _curvedAnimation = CurvedAnimation(
        parent: _controller,
        curve: Curves.linear
    );

    // 3. 创建 tween 配置动画值的范围
    _tweenAnimation = Tween(
        begin: 1.0,
        end: 2.0
    ).animate(_curvedAnimation);

    // 4. 添加值监听
    _controller.addListener(() {
      setState(() {});
    });

    // 4. 监听状态
    _controller.addStatusListener((status) {
      print(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("动画"),
      ),
      body: Container(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            SizedBox(height: 200,),
            // 长度变化
            Container(
              color: Colors.blueAccent,
              width: 100 * _tweenAnimation.value,
              height: 60,
            ),
            SizedBox(height: 20,),
            // 透明度变化
            Opacity(
              opacity: 2.0 - _tweenAnimation.value,
              child: Container(
                color: Colors.blueAccent,
                width: 100,
                height: 100,
              ),
            ),
            SizedBox(height: 20,),
            // 字体大小变化
            Text("窗外风好大",
              style: TextStyle(
                fontSize: 20 * _tweenAnimation.value,
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.done_outline),
        onPressed: (){
          _controller.forward();
        },
      ),
    );
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

根据动画值的变化修改 widget 宽度、透明度和字体大小,虽然动画效果做到了,但是我们必须监听动画值的改变,并且改变后需要调用 setState(),这会带来两个问题:

  • 执行动画必须包含被部分代码,代码比较冗余;
  • 调用 setState() 意味着整个 State 类中的 build 方法就会被重新执行。

2.1.1 AnimatedWidget

为了解决上面的问题,我们可以使用 AnimatedWidget(而不是 addListener() 和setState() )来给 widget 添加动画:

  • AnimatedWidget 从 setState() 调用中的动画代码中分离出 widget 代码,创建一个可重用动画的 widget;
  • AnimatedWidget 不需要维护一个 State 对象来保存动画;
  • AnimatedWidget 中会自动调用 addListener() 和 setState()。

所以上面的代码可以优化成下面这样:

// 使用处
...
body: CJAnimatedWidget(_tweenAnimation),
...

class CJAnimatedWidget extends AnimatedWidget {

  final Animation<double> _tweenAnimation;

  CJAnimatedWidget(this._tweenAnimation): super(listenable: _tweenAnimation);

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          SizedBox(height: 200,),
          // 长度变化
          Container(
            color: Colors.blueAccent,
            width: 100 * _tweenAnimation.value,
            height: 60,
          ),
          SizedBox(height: 20,),
          // 透明度变化
          Opacity(
            opacity: 2.0 - _tweenAnimation.value,
            child: Container(
              color: Colors.blueAccent,
              width: 100,
              height: 100,
            ),
          ),
          SizedBox(height: 20,),
          // 字体大小变化
          Text("窗外风好大",
            style: TextStyle(
              fontSize: 20 * _tweenAnimation.value,
            ),
          ),
        ],
      ),
    );
  }
}

Flutter 提供了很多封装完成的 AnimatedWidget 子类给我们使用:

  • FadeTransition
  • ScaleTransition
  • RotationTransition
  • SizeTransition
  • SlideTransition
  • RelativePositionedTransition
  • DecoratedBoxTransition
  • AlignTransition
  • DefaultTextStyleTransition
  • PositionedTransition

AnimatedWidget 虽然解决了一些问题,但是它也有一些弊端:

  • 我们大部分情况都要新建一个类来继承自 AnimatedWidget;
  • 如果我们的动画 Widget 有子 Widget,那么意味着它的子 Widget 也会重新 build。

2.1.2 AnimatedBuilder

为了优化上述问题,我们可以使用 AnimatedBuilder,它可以从 widget 中分离出动画过渡:

  • AnimatedBuilder 是渲染树中的一个独立的类。 与 AnimatedWidget 类似, AnimatedBuilder 自动监听来自 Animation 对象的通知,并根据需要将该控件树标记为脏(dirty),因此不需要手动调用 addListener()。

优化后的代码如下:

...
body: Container(
    child: AnimatedBuilder(
      animation: _tweenAnimation,
      builder: (ctx, child) {
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            SizedBox(height: 200,),
            // 长度变化
            Container(
              color: Colors.blueAccent,
              width: 100 * _tweenAnimation.value,
              height: 60,
            ),
            SizedBox(height: 20,),
            // 透明度变化
            Opacity(
              opacity: 2.0 - _tweenAnimation.value,
              child: Container(
                color: Colors.blueAccent,
                width: 100,
                height: 100,
              ),
            ),
            SizedBox(height: 20,),
            // 字体大小变化
            Text("窗外风好大",
              style: TextStyle(
                fontSize: 20 * _tweenAnimation.value,
              ),
            ),
          ],
        );
      },
    ),
  ),
...

2.2 动画示例-2

image

主要代码如下:

class LookPage extends StatefulWidget {
  @override
  _CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}

class _CJAnimationWidgetState extends State<LookPage> with SingleTickerProviderStateMixin {

  AnimationController _controller;
  Animation<double> _curvedAnimation;
  Animation<double> _glassLocationAnim;
  Animation<double> _glassRotationAnim;
  Animation<double> _necklaceOpacityAnim;
  Animation<double> _necklaceLocationAnim;

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

    // 1. 创建 controller
    _controller = AnimationController(
      duration: Duration(milliseconds: 2000),
      vsync: this,
    );

    // 2. 创建 curvedAnimation
    _curvedAnimation = CurvedAnimation(
        parent: _controller,
        curve: Curves.linear
    );

    // 3. 创建 tween 配置动画值的范围
    _glassLocationAnim = Tween(
        begin: 0.0,
        end: 252.0
    ).animate(_curvedAnimation);

    _glassRotationAnim = Tween(
        begin: 0.0,
        end: 2.1*pi
    ).animate(_curvedAnimation);

    _necklaceLocationAnim = Tween(
        begin: 500.0,
        end: 370.0
    ).animate(_curvedAnimation);

    _necklaceOpacityAnim = Tween(
        begin: 0.0,
        end: 1.0
    ).animate(_curvedAnimation);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("动画"),
      ),
      body: AnimatedBuilder(
        animation: _controller,
        builder: (ctx, child) {
          return Stack(
            overflow: Overflow.clip,
            children: <Widget>[
              Positioned(
                  left: 0,
                  top: 200,
                  width: 414,
                  child: Image.asset(
                    "assets/images/dog.jpg",
                    width: 414,
                  )
              ),
              Positioned(
                  left: _glassLocationAnim.value - 130,
                  top: 207,
                  child: Transform(
                    alignment: Alignment.center,
                    transform: Matrix4.rotationZ(_glassRotationAnim.value),
                    child: Image.asset(
                      "assets/images/glasses.png",
                      width: 130,
                      height: 130,
                    ),
                  )
              ),
              Positioned(
                  left: 130,
                  top: _necklaceLocationAnim.value,
                  child: Opacity(
                    opacity: 1 * _necklaceOpacityAnim.value,
                    child: Image.asset(
                      "assets/images/necklace.png",
                      width: 160,
                      height: 110,
                    ),
                  )
              ),
            ],
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.done_outline),
        onPressed: (){
          if (_controller.status == AnimationStatus.completed) {
            _controller.reverse();
          } else if (_controller.status == AnimationStatus.dismissed) {
            _controller.forward();
          }
        },
      ),
    );
  }

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

主要思路:

  • 给墨镜添加一个位置变化动画和一个旋转动画,给墨镜一个初始位置,开启动画之后,墨镜在旋转的同时移动的目标位置;
  • 大金链子则添加一个位置变化动画,开启动画之后,直接移动到目标位置。

3. 系统动画组件

3.1 AnimatedContainer

我们可以理解 AnimatedContainer 是带动画功能的 Container:

  • AnimatedContainer 只需要提供动画开始值和结束值,它就会动起来并不需要我们主动调用 setState() 方法;
  • 动画不仅可以作用在宽高上,还可以作用在颜色、边界、边界圆角半径、背景图片、形状等;
  • AnimatedContainer 有2个必须的参数,一个时长 duration,即动画执行的时长,另一个是动画曲线 curve,默认是线性,系统为我们提供了很多动画曲线(加速、减速等),例:curve: Curves.bounceIn

AnimatedContainer 动画示例:

image

主要代码如下:

class LookPage extends StatefulWidget {
  @override
  _CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}

class _CJAnimationWidgetState extends State<LookPage> with SingleTickerProviderStateMixin {

  bool _click = false;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        onTap: () {
          setState(() {
            _click = !_click;
          });
        },
        child: AnimatedContainer(
          height: _click ? 200 : 100,
          width: _click ? 200 : 100,
          duration: Duration(milliseconds: 2000),
          curve: Curves.easeInOutCirc,
          transform: Matrix4.rotationX(_click ? pi : 0),
          decoration: BoxDecoration(
              image: DecorationImage(
                image: AssetImage("assets/images/girl.jpg"),
                fit: BoxFit.cover,
              ),
              borderRadius: BorderRadius.all(Radius.circular(
                _click ? 200 : 100,
              ))
          ),
          onEnd: () {
            setState(() {
              _click = !_click;
            });
          },
        ),
      ),
    );
  }
}

主要思路:

  • 根据点击的状态变化,修改图片的尺寸以及图片围绕X轴旋转的角度。

3.2 AnimatedCrossFade

AnimatedCrossFade 组件让2个组件在切换时出现交叉渐入的效果,因此 AnimatedCrossFade 需要设置2个子控件、动画时间和显示第几个子控件。

AnimatedCrossFade 动画示例:

image

主要代码如下:

class LookPage extends StatefulWidget {
  @override
  _CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}

class _CJAnimationWidgetState extends State<LookPage> with SingleTickerProviderStateMixin {

  bool _click = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("动画"),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              AnimatedContainer(
                duration: Duration(seconds: 2),
                width: 200,
                height: _click ? 200 : 100,
                decoration: BoxDecoration(
                    color: _click ? Colors.blueAccent : Colors.green,
                    borderRadius: BorderRadius.all(
                        Radius.circular(_click ? 0 : 50,)
                    )
                ),
              ),
              SizedBox(height: 20,),
              AnimatedCrossFade(
                duration: Duration(seconds: 2),
                crossFadeState: _click ? CrossFadeState.showSecond : CrossFadeState
                    .showFirst,
                firstChild: Container(
                  height: 100,
                  width: 200,
                  alignment: Alignment.center,
                  decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(50),
                      color: Colors.green
                  ),
                  child: Text('1',
                    style: TextStyle(
                        color: Colors.white,
                        fontSize: 40
                    ),
                  ),
                ),
                secondChild: Container(
                  height: 200,
                  width: 200,
                  alignment: Alignment.center,
                  decoration: BoxDecoration(
                      color: Colors.blueAccent,
                  ),
                  child: Text('2',
                    style: TextStyle(
                        color: Colors.white,
                        fontSize: 40
                    ),
                  ),
                ),
              ),
              SizedBox(height: 20,),
              AnimatedContainer(
                duration: Duration(seconds: 2),
                width: 200,
                height: _click ? 200 : 100,
                decoration: BoxDecoration(
                    color: _click ? Colors.blueAccent : Colors.green,
                    borderRadius: BorderRadius.all(
                        Radius.circular(_click ? 0 : 50,)
                    )
                ),
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          child: Icon(Icons.done_outline),
          onPressed: () {
            setState(() {
              _click = !_click;
            });
          },
        )
    );
  }
}

主要思路:

  • 根据点击的状态变化,改变 AnimatedCrossFade 显示的子控件,同时让上下的两个 AnimatedContainer 变化形状。

3.3 AnimatedIcon

Flutter还提供了很多动画图标,想要使用这些动画图标需要使用 AnimatedIcon 控件:

  • 第一步设置图标;
  • 第二步设置 progress,progress 用于图标的动画。

AnimatedIcon 动画示例:

image

主要代码如下:

class LookPage extends StatefulWidget {
  @override
  _CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}

class _CJAnimationWidgetState extends State<LookPage> with SingleTickerProviderStateMixin {

  AnimationController _controller;

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

    // 1. 创建 controller
    _controller = AnimationController(
      duration: Duration(milliseconds: 2000),
      vsync: this,
    );

    // 2. 监听状态
    _controller.addStatusListener((status) {
      print(status);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("动画"),
        ),
        body: GridView(
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
          ),
          children: <Widget>[
            createAnimatedIcon(AnimatedIcons.add_event),
            createAnimatedIcon(AnimatedIcons.arrow_menu),
            createAnimatedIcon(AnimatedIcons.close_menu),
            createAnimatedIcon(AnimatedIcons.ellipsis_search),
            createAnimatedIcon(AnimatedIcons.event_add),
            createAnimatedIcon(AnimatedIcons.home_menu),
            createAnimatedIcon(AnimatedIcons.list_view),
            createAnimatedIcon(AnimatedIcons.menu_arrow),
            createAnimatedIcon(AnimatedIcons.menu_close),
            createAnimatedIcon(AnimatedIcons.menu_home),
            createAnimatedIcon(AnimatedIcons.pause_play),
            createAnimatedIcon(AnimatedIcons.play_pause),
            createAnimatedIcon(AnimatedIcons.search_ellipsis),
            createAnimatedIcon(AnimatedIcons.view_list),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          child: Icon(Icons.done_outline),
          onPressed: () {
            if (_controller.status == AnimationStatus.completed) {
              _controller.reverse();
            } else if (_controller.status == AnimationStatus.dismissed) {
              _controller.forward();
            }
          },
        )
    );
  }

  Widget createAnimatedIcon (AnimatedIconData animIconData) {
    return Container(
      width: 138,
      height: 138,
      child: Center(
        child: AnimatedIcon(
          icon: animIconData,
          progress: _controller,
        ),
      ),
    );
  }
}

这个不需要思路...

系统动画组件还有很多,但是有些功能是有重叠的,在这里就不一一陈述了。

其他系统动画组件如下:

  • AnimatedAlign
  • AnimatedDefaultTextStyle
  • AnimatedModalBarrier
  • AnimatedOpacity
  • AnimatedPadding
  • AnimatedPhysicalModel
  • AnimatedPositioned
  • AnimatedPositionedDirectional
  • AnimatedSize

4. 转场动画

如果我们要导航到一个新页面,一般会使用 MaterialPageRoute,在页面切换的时候,会有默认的自适应平台的过渡动画,如果想自定义页面的进场和出场动画,那么需要使用 PageRouteBuilder 来创建路由,PageRouteBuilder 主要的部分:

  • 一个是“pageBuilder”,用来创建所要跳转到的页面;
  • 另一个是“transitionsBuilder”,也就是我们可以自定义的转场效果。

PageRouteBuilder 转场动画示例:

image

主要代码如下:

class LookPage extends StatefulWidget {
  @override
  _CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}

class _CJAnimationWidgetState extends State<LookPage> with SingleTickerProviderStateMixin {

  String _imageURL = "assets/images/cj3.png";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("第一页"),
          backgroundColor: Color.fromARGB(255, 24,45, 105),
        ),
        body: Center(
          child: GestureDetector(
            onTap: () {
              Navigator.of(context).push(PageRouteBuilder(
                  pageBuilder: (context, animation, secondaryAnimation) {
                    return CJNextPage("assets/images/cj2.png");
                  },
                  transitionsBuilder: (context, animation, secondaryAnimation,
                      child) {
                    return CJRotationTransition(
                      turns: Tween<double>(
                          begin: 1.0,
                          end: 0.0
                      ).animate(animation),
                      child: child,
                    );
                  }
              )
              );
            },
            child: Image.asset(_imageURL, height: 896, fit: BoxFit.fitHeight,)
          ),
        ),
    );
  }
}

class CJNextPage extends StatelessWidget {
  final String imageURL;

  CJNextPage(this.imageURL);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二页"),
        backgroundColor: Colors.lightBlueAccent,
      ),
      backgroundColor: Colors.white,
      body: Center(
        child: GestureDetector(
          onTap: () {
            Navigator.of(context).pop();
          },
          child: Image.asset(imageURL, height: 896, fit: BoxFit.fitHeight),
        ),
      ),
    );
  }
}

class CJRotationTransition extends AnimatedWidget {
  const CJRotationTransition({
    Key key,
    @required Animation<double> turns,
    this.alignment = Alignment.center,
    this.child,
  }) : assert(turns != null),
        super(key: key, listenable: turns);
  Animation<double> get turns => listenable;
  final Alignment alignment;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final double turnsValue = turns.value;
    final Matrix4 transform = Matrix4.rotationY(turnsValue * pi/2.0);
    return Transform(
      transform: transform,
      alignment: alignment,
      child: child,
    );
  }
}

主要思路:

  • 第二个页面出现的时候,围绕 Y 轴旋转90度。

4.1 Hero

Hero 是我们常用的过渡动画,当用户点击一张图片,切换到另一个页面时,这个页面也有此图,那么我们可以使用 Flutter 给我们提供的 Hero 组件来完成这个效果:

  • 2个页面都要有 Hero 控件,且保证 tag 参数一致。

Hero 动画示例:

image

主要代码如下:

class LookPage extends StatefulWidget {
  @override
  _CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}

class _CJAnimationWidgetState extends State<LookPage> with SingleTickerProviderStateMixin {

  String _imageURL = "assets/images/shoes.JPG";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第一页"),
        backgroundColor: Color.fromARGB(255, 24, 45, 105),
      ),
      body: GridView(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          crossAxisSpacing: 10,
          mainAxisSpacing: 10,
        ),
        children: List.generate(20, (index) {
          return GestureDetector(
              onTap: () {
                Navigator.of(context).push(PageRouteBuilder(
                    pageBuilder: (context, animation, secondaryAnimation) {
                      return CJHeroPage(_imageURL, "$_imageURL-$index");
                    },
                    transitionsBuilder: (context, animation, secondaryAnimation,
                        child) {
                      return FadeTransition(
                        opacity: animation,
                        child: child,
                      );
                    }
                )
                );
              },
              child: Hero(
                tag: "$_imageURL-$index",
                child: Image.asset(_imageURL, width: 125, height: 125,),
              )
          );
        }),
      ),
    );
  }
}

class CJHeroPage extends StatelessWidget {
  final String imageURL;
  final String heroTag;

  CJHeroPage(this.imageURL, this.heroTag);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: GestureDetector(
          onTap: () {
            Navigator.of(context).pop();
          },
          child: Hero(
            tag: heroTag,
            child: Image.asset(imageURL, width: double.infinity, fit: BoxFit.cover,
            ),
          ),
        ),
      ),
    );
  }
}

如有不对的地方,还请指出,感谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349