一个令你颤抖的flutter动画:Basic Animations

效果

这个包含一系列的动画实例和动画控制:

  • Swipe It 透明度从1到0的变换

  • 根据黑色区域的宽度改变贝塞尔曲线的大小

  • Tap Here 文案从左到右出现

  • Easy 区域从屏幕外移动到屏幕内

  • Easy 区域反转动画

从 Swipe It 的透明度动画开始

class _HomeState extends State<Home>
    with TickerProviderStateMixin {
   ...
   AnimationController _opacityController;
   ...
   Animation<double> _opacity;
   bool _showHint = false;
   ...

   @override
   void initState() {
     super.initState();
     ...
     _opacityController = new AnimationController(vsync: this,duration: const Duration(milliseconds: 800));
     _opacity = new CurvedAnimation(parent: _opacityController, curve: Curves.easeInOut)..addStatusListener((status) {
       if (status == AnimationStatus.completed) {
         _opacityController.reverse();
       } else if (status == AnimationStatus.dismissed) {
         _opacityController.forward();
       }
     });
     _opacityController.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return  new Container(
        child: new Stack(
          alignment: Alignment.center,
          children: <Widget>[
            !_showHint?new FadeTransition(
              opacity: _opacity,
              child: new Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  new Icon(Icons.arrow_back),
                  new Padding(padding: const EdgeInsets.only(left: 8.0)),
                  new Text("Swipe it",
                    style: new TextStyle(
                        fontSize: 16.0,
                        color: Colors.blueGrey
                    ),
                  )
                ],
              ),
            ):new Container(),
       ...
      )
    );
 }

flutter 中动画基于 Animation对象,我们这里使用Animation是因为大多数动画都是使用的double类型属性。

我们使用AnimationController来控制动画。创建Animationcontroller的时候需要传递一个vsync对象,这个对象可以防止离开屏幕的动画消耗不必要的资源。

Stateful widget的state添加TickerProviderStateMixin之后就可以当作vsync使用。如果你的state生命周期内只有一个Ticker(例如:一个AnimationController)。 使用更高效的SingleTickerProviderStateMixin。

现在我们开始实现Animation的协议,这里使用CurvedAnimation来控制透明度opacity,因为
CurvedAnimation的值改变范围是 0.0 到 1.0,用在这里刚好合适。

接着,添加动画状态监听。当动画结束的时候再次启动。

然后启动动画,forward()。

flutter为我们提供了FadeTransition控件,可以接受一个opacity对象,可以包裹任意的widget。


实现第2和第3步

接下来我们实现控制贝塞尔曲线和Tap next文案的显示动画:

class _HomeState extends State<Home>
    with TickerProviderStateMixin {
   ...
   AnimationController _controllerMenu;
   AnimationController _controllerText;
   ...
   Animation<double> _animationMenu;
   Animation<double> _animationText;
   bool _menuOpened = false;
   bool _showeasy = false;
   ...

   @override
   void initState() {
     super.initState();
     ...
     _controllerMenu = new AnimationController(vsync: this, duration: const Duration(milliseconds: 500));
     _controllerText = new AnimationController(vsync: this, duration: const Duration(milliseconds: 500));
     ...
     _animationMenu = new Tween(begin: 0.3, end: 1.0).animate(
      new CurvedAnimation(
        parent: _controllerMenu,
        curve:Curves.ease,
      ),
    );
    _animationMenu.addListener((){
      setState((){});
    });
    _animationMenu.addStatusListener((status){
      if(status == AnimationStatus.completed ){
        _menuOpened = !_menuOpened;
        _controllerText.forward();
      }
      if(status == AnimationStatus.dismissed ){
        if (_showeasy) {
          _showEasyController.forward();
          setState(()=>_showeasy = false);
        }
        else{
          setState(()=>_showHint = false);
        }
      }
    });
    _animationText = new Tween(begin: -100.0, end: 16.0).animate(
      new CurvedAnimation(
        parent: _controllerText,
        curve:Curves.ease,
      ),
    );
    _animationText.addListener((){
      setState((){});
    });

    _animationText.addStatusListener((status){
      if(status == AnimationStatus.dismissed){
      _menuOpened = !_menuOpened;
      _controllerMenu.reverse();
      }

    });
  }

  @override
  void dispose() {
    super.dispose();
    ...
    _controllerMenu.dispose();
    _controllerText.dispose();
  }
}
}

与前面对比唯一的不同是我们用Tween包裹了CurvedAnimation,它代表了可以自定义开始和结束的值。

贝塞尔曲线开始值从全部宽度的0.3开始。

文案移动从x轴的 -100 px 到 16 px。

class _HomeState extends State<Home>
    with TickerProviderStateMixin {
  ...
  @override
  Widget build(BuildContext context) {
    return  new Container(
        child: new Stack(
          alignment: Alignment.center,
          children: <Widget>[
            ...
            new Align(
              child: new GestureDetector(
                child: new Container(
                  child: new ClipPath(
                    child: new Container(
                      color: Colors.black,
                      width: 250.0 * _animationMenu.value,
                    ),
                    clipper: new HillClipper(),
                  ),
                ),
                onHorizontalDragEnd: (details) {
                  if(!_isMenuAvailable) return;
                  if (details.velocity.pixelsPerSecond.dx > 0 && !_menuOpened) {
                    print("Open");
                    setState(()=>_showHint = true);
                    _controllerMenu.forward();
                  }
                  else if (details.velocity.pixelsPerSecond.dx < 0 && _menuOpened) {
                    print("Close");
                    _controllerText.reverse();
                  }
                },
              ),
              alignment: Alignment.centerLeft,
            ),
            new Align(
              child: new Transform(
                transform: new Matrix4.translationValues(
                    _animationText.value, 0.0, 0.0),
                child: new GestureDetector(
                  child: const Text("Tap Here",
                    style: const TextStyle(
                        fontSize: 16.0,
                        fontWeight: FontWeight.bold,
                        color: Colors.white
                    ),
                  ),
                  onTap: (){
                    _controllerText.reverse();
                    setState((){
                      _showeasy = true;
                      _isMenuAvailable = false;
                    });
                  },
                ),
              ),
              alignment: Alignment.centerLeft,
            ),
            ...
          ],
        )
    );
  }
}

class HillClipper extends CustomClipper<Path>{

  @override
  Path getClip(Size size) {
    var path = new Path();
    path.lineTo(0.0, size.height/5);

    var medianControlPoint = new Offset(size.width, size.height/2);
    var medianPoint = new Offset(0.0, size.height - size.height/5);
    path.quadraticBezierTo(medianControlPoint.dx, medianControlPoint.dy,
        medianPoint.dx, medianPoint.dy);

    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}

我们使用的动画小部件是一个黑色容器。它被包裹在GestureDetector中以捕获水平滑动。为了实现贝塞尔曲线,我还在容器限幅器属性中添加了一个CustomClipper。你可以在上面的代码片段的底部找到一个简单的CustomClipper作为HillClipper类实现。

这里我们使用的是widget的宽度来控制贝塞尔曲线的绘制。

Tap here 移出屏幕使用了transform widget。使用了Matrix4变换属性。当然,Matrix4还可以旋转、缩放等。

实现 easy 方框

这里可以看到我是如何使用Matrix4 的 translationValue方法的。

class _HomeState extends State<Home>
    with TickerProviderStateMixin {
   ...
   AnimationController _scaleController;
   AnimationController _showEasyController;
   ...
   Animation<double> _frontScale;
   Animation<double> _backScale;
   Animation<Offset> _showEasy;
   bool _showeasy = false;
   ...

   @override
   void initState() {
     super.initState();
     ...
     _scaleController = new AnimationController(vsync: this, duration: const Duration(milliseconds: 800));
     _showEasyController = new AnimationController(vsync: this, duration: const Duration(milliseconds: 2000));
     _showEasy = new Tween(
      begin: new Offset(0.0,3.0),
      end: new Offset(0.0,0.0),
    ).animate(new CurvedAnimation(
      parent: _showEasyController,
      curve: new Interval(0.0, 0.5, curve: Curves.easeInOut),
    ));
    _showEasy.addStatusListener((status){
      if(status == AnimationStatus.completed ){
        _scaleController.forward();
      }
      if(status == AnimationStatus.dismissed){
        _scaleController.reverse();
        setState((){
          _showHint = false;
          _isMenuAvailable = true;
        });
      }
    });

    _frontScale = new Tween(
      begin: 1.0,
      end: 0.0,
    ).animate(new CurvedAnimation(
      parent: _scaleController,
      curve: new Interval(0.0, 0.5, curve: Curves.easeIn),
    ));
    _backScale = new CurvedAnimation(
      parent: _scaleController,
      curve: new Interval(0.5, 1.0, curve: Curves.easeOut),
    );

    _frontScale.addListener((){
      setState((){});
    });
    _backScale.addListener((){
      setState((){});
    });

    _backScale.addStatusListener((status){
      if(status == AnimationStatus.completed){
        _showEasyController.reverse();
      }
    });
    ...
  }

  @override
  Widget build(BuildContext context) {
    return  new Container(
        child: new Stack(
          alignment: Alignment.center,
          children: <Widget>[
            ...
            new SlideTransition(position: _showEasy,
              child: new Stack(
                children: <Widget>[
                  new Transform(transform:  new Matrix4.identity()
                ..scale(1.0, _backScale.value, 1.0),
                    alignment: FractionalOffset.center,
                    child: new EasyCard("images/too_easy.jpeg"),
                  ),
                  new Transform(transform:  new Matrix4.identity()
                    ..scale(1.0, _frontScale.value, 1.0),
                    alignment: FractionalOffset.center,
                    child: new EasyCard(null),
                  ),
                ],
              )
            ),
          ],
        )
    );
  }
}

class EasyCard extends StatelessWidget {
  final String image;

  EasyCard(this.image);

  @override
  Widget build(BuildContext context) {
    return new Container(
        alignment: FractionalOffset.center,
        height: 250.0,
        width: 250.0,
        decoration: new BoxDecoration(
          border: new Border.all(color: new Color(0xFF9E9E9E)),
        ),
        child: image != null? new Image.asset(image): new Text(
            "Animations in flutter?",
            style: new TextStyle(
              fontSize: 18.0,
              color: Colors.black,
              fontWeight: FontWeight.bold
            ),
        ),
    );
  }
}

即使这个片段开起来很长,但是并不复杂。这里没有使用 Animation让卡片从屏幕底部移动到屏幕上。相反的使用了Animation对象。

希望你能喜欢。请多多支持我的网站 www.tryenough.com

关闭菜单