Flutter 动画入门篇

2020-06-19  本文已影响0人  IAMCJ

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

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 > 的子类:

代码示例:

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

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

AnimationController 控制动画的方法:

1.3. CurvedAnimation

CurvedAnimation 是 Animation< double > 的子类:

代码示例:

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

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

1.4. Tween

Tween 继承自 Animatable< T >:

代码示例:

final Tween tweenAnim = Tween(
    begin: 50.0, 
    end: 150.0
).animate(curvedAnimation);

1.5. Listeners 和 StatusListeners

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

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

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(),这会带来两个问题:

2.1.1 AnimatedWidget

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

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

// 使用处
...
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 子类给我们使用:

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

2.1.2 AnimatedBuilder

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

优化后的代码如下:

...
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 动画示例:

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;
            });
          },
        ),
      ),
    );
  }
}

主要思路:

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;
            });
          },
        )
    );
  }
}

主要思路:

3.3 AnimatedIcon

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

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,
        ),
      ),
    );
  }
}

这个不需要思路...

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

其他系统动画组件如下:

4. 转场动画

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

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,
    );
  }
}

主要思路:

4.1 Hero

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

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,
            ),
          ),
        ),
      ),
    );
  }
}

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

上一篇 下一篇

猜你喜欢

热点阅读