Flutter动画:用Flutter来实现一个拍手动画

文章地址:mrw.so/4VPxov 译者:依然范特稀西html

在本文中,咱们将经过在Flutter中建立一个拍手动画的模型,来学习一些有关动画的核心概念。git

就像标题中所说的那样,本文将更多地关注动画,而不会关注Flutter的基础知识。github

正文开始

咱们将从建立一个新的Flutter项目生成的代码开始,建立一个新的Flutter项目,你就会获得下面的代码:api

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'You have pushed the button this many times:',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: new Icon(Icons.add),
      ),
    );
  }
}
复制代码

Flutter为咱们提供了一些免费的入门代码,它为咱们建立了一个浮动操做按钮,而且自动帮咱们管理计数的状态。数组

下图是咱们最终要实现的效果:app

在添加动画以前,让咱们快速解决一些简单的问题:less

  • 更改按钮图标和背景。dom

  • 按住按钮时,按钮应继续增长计数。ide

让咱们快速解决上面2个问题,而后开始实现动画:函数

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  final duration = new Duration(milliseconds: 300);
  Timer timer;


  initState() {
    super.initState();
  }

  dispose() {
   super.dispose();
  }

  void increment(Timer t) {
    setState(() {
      _counter++;
    });
  }

  void onTapDown(TapDownDetails tap) {
    // User pressed the button. This can be a tap or a hold.
    increment(null); // Take care of tap
    timer = new Timer.periodic(duration, increment); // Takes care of hold
  }

  void onTapUp(TapUpDetails tap) {
    // User removed his finger from button.
    timer.cancel();
  }

  Widget getScoreButton() {

    return new Positioned(
        child: new Opacity(opacity: 1.0, child: new Container(
            height: 50.0 ,
            width: 50.0 ,
            decoration: new ShapeDecoration(
              shape: new CircleBorder(
                  side: BorderSide.none
              ),
              color: Colors.pink,
            ),
            child: new Center(child:
            new Text("+" + _counter.toString(),
              style: new TextStyle(color: Colors.white,
                  fontWeight: FontWeight.bold,
                  fontSize: 15.0),))
        )),
        bottom: 100.0
    );
  }

  Widget getClapButton() {
    // Using custom gesture detector because we want to keep increasing the claps
    // when user holds the button.
    return new GestureDetector(
        onTapUp: onTapUp,
        onTapDown: onTapDown,
        child: new Container(
          height: 60.0 ,
          width: 60.0 ,
          padding: new EdgeInsets.all(10.0),
          decoration: new BoxDecoration(
              border: new Border.all(color: Colors.pink, width: 1.0),
              borderRadius: new BorderRadius.circular(50.0),
              color: Colors.white,
              boxShadow: [
                new BoxShadow(color: Colors.pink, blurRadius: 8.0)
              ]
          ),
          child: new ImageIcon(
              new AssetImage("images/clap.png"), color: Colors.pink,
              size: 40.0),
        )
    );
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'You have pushed the button this many times:',
            ),
            new Text(
              '$_counter',
              style: Theme
                  .of(context)
                  .textTheme
                  .display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new Padding(
          padding: new EdgeInsets.only(right: 20.0),
          child: new Stack(
            alignment: FractionalOffset.center,
            overflow: Overflow.visible,
            children: <Widget>[
              getScoreButton(),
              getClapButton(),
            ],
          )
      ),
    );
  }
}
复制代码

看了上面最终的效果图,咱们须要作2件事:

  • 更改widgets的大小。
  • 按下按钮时显示分数widget,释放按钮时将其隐藏。
  • 添加这些小巧的widget并为其设置动画。

让咱们一个接一个地慢慢增长学习曲线。首先,咱们须要了解有关Flutter动画的一些基本知识。

了解Flutter中基本动画的组件

动画不过是随着时间变化的一些值,例如,当咱们点击按钮时,咱们但愿用动画来让显示分数widget 从底部升起,而当手指离开按钮时,继续上升而后隐藏。

若是仅看分数Widget,咱们须要在一段时间内更改Widget的位置和不透明度值。

new Positioned(
        child: new Opacity(opacity: 1.0, 
          child: new Container(
            ...
          )),
        bottom: 100.0
    );
复制代码

假设咱们但愿分数widget须要150毫秒才能从底部显示出来。在如下时间轴上考虑一下:

这是一个简单的2D图形。 position将随着时间而改变。 请注意,对角线是直线。若是你喜欢,它也能够是曲线。

你可使position随时间缓慢增长,而后变得愈来愈快。或者,你也可让它以超高速进入,而后在最后放慢速度。

下面是咱们介绍的第一个组件:Animation Controller

scoreInAnimationController = new AnimationController(duration: new Duration(milliseconds: 150), vsync: this);
复制代码

在这里,咱们为动画建立了一个简单的控制器(Controller)。咱们已经指定但愿动画运行150ms。可是,vsync是什么东西?

移动设备每隔几毫秒刷新一次屏幕。这就是咱们将一组图像视为连续流或电影的方式。

屏幕刷新的速率因设备而异。 假设移动设备每秒刷新屏幕60次(每秒60帧)。 那就是每16.67毫秒以后,咱们就会向大脑提供新的图像。 有时,图像就会错位(在屏幕刷新时发出不一样的图像),而且看到屏幕撕裂。 VSync就是解决这个问题的。

咱们给控制器设置一个监听器,而后开始动画:

scoreInAnimationController.addListener(() {
      print(scoreInAnimationController.value);
    });
scoreInAnimationController.forward(from: 0.0);
/* OUTPUT I/flutter ( 1913): 0.0 I/flutter ( 1913): 0.0 I/flutter ( 1913): 0.22297333333333333 I/flutter ( 1913): 0.3344533333333333 I/flutter ( 1913): 0.4459333333333334 I/flutter ( 1913): 0.5574133333333334 I/flutter ( 1913): 0.6688933333333335 I/flutter ( 1913): 0.7803666666666668 I/flutter ( 1913): 0.8918466666666668 I/flutter ( 1913): 1.0 */
复制代码

控制器在150ms内生成了0.01.0的数字。请注意,生成的值几乎是线性的。 0.20.30.4…咱们如何改变这种行为?这将在第二部分完成:曲线动画

曲线动画
bounceInAnimation = new CurvedAnimation(parent: scoreInAnimationController, curve: Curves.bounceIn);
    bounceInAnimation.addListener(() {
      print(bounceInAnimation.value);
    });

/*OUTPUT I/flutter ( 5221): 0.0 I/flutter ( 5221): 0.0 I/flutter ( 5221): 0.24945376519722218 I/flutter ( 5221): 0.16975716286388898 I/flutter ( 5221): 0.17177866222222238 I/flutter ( 5221): 0.6359024059750003 I/flutter ( 5221): 0.9119433941222221 I/flutter ( 5221): 1.0 */
复制代码

经过将parent属性设置为咱们的控制器,并提供动画遵循曲线,就能够建立一个CurvedAnimation,Flutter曲线文档页面上提供了多种曲线供咱们选择:api.flutter.dev/flutter/ani…

控制器在150ms的时间内为曲线动画Widget提供从0.01.0的值。曲线动画Widget根据咱们设置的曲线对这些值进行插值。

尽管咱们获得了0.01.0之间的一系列值,可是咱们但愿显示分数的Widget显示的值为0-100,咱们能够简单地乘以100来获得结果,或者咱们可使用第三个组件:Tween类。

tweenAnimation = new Tween(begin: 0.0, end: 100.0).animate(scoreInAnimationController);
    tweenAnimation.addListener(() {
      print(tweenAnimation.value);
    });

/* Output I/flutter ( 2639): 0.0 I/flutter ( 2639): 0.0 I/flutter ( 2639): 33.452000000000005 I/flutter ( 2639): 44.602000000000004 I/flutter ( 2639): 55.75133333333334 I/flutter ( 2639): 66.90133333333334 I/flutter ( 2639): 78.05133333333333 I/flutter ( 2639): 89.20066666666668 I/flutter ( 2639): 100.0 */
复制代码

Tween类生成beginend之间的值,前面咱们已经使用过线性的scoreInAnimationController,相反,咱们可使用反弹曲线来得到不一样的值。Tween的优势远不止这些,你还能够补间其余东西,好比你能够补间color(颜色)offset(偏移量)position(位置)、和其余Widget属性,从而进一步扩展了基础补间类。

Score Widget 位置动画

至此,咱们已经掌握了足够的知识,如今可使咱们的得分Widget在按下按钮时从底部弹出,而在离开时隐藏。

initState() {
    super.initState();
    scoreInAnimationController = new AnimationController(duration: new Duration(milliseconds: 150), vsync: this);
    scoreInAnimationController.addListener((){
      setState(() {}); // Calls render function
    });
  }

void onTapDown(TapDownDetails tap) {
    scoreInAnimationController.forward(from: 0.0);
    ...    
}
Widget getScoreButton() {
    var scorePosition = scoreInAnimationController.value * 100;
    var scoreOpacity = scoreInAnimationController.value;
    return new Positioned(
        child: new Opacity(opacity: scoreOpacity, 
                           child: new Container(...)
                          ),
        bottom: scorePosition
    );
  }
复制代码

如上图所示,点击按钮,Score Widget 从底部弹出了,可是这儿还有一个小问题:当屡次点击按钮的时候,score widget 一次又一次的弹出,这是因为上述代码中的一个小错误。每次点击按钮时,咱们都告诉控制器从0开始,即forward(from: 0.0)

score widget 退出动画

如今,咱们为score Widget 添加退出动画,首先,咱们添加一个枚举来更轻松地管理score Widget的状态。

enum ScoreWidgetStatus {
  HIDDEN,
  BECOMING_VISIBLE,
  BECOMING_INVISIBLE
}
复制代码

而后,建立一个退出动画的控制器,动画控制器将使score widget的位置从100非线性变化到150。咱们还为动画添加了状态监听器。动画结束后,咱们将得分组件的状态设置为隐藏。

scoreOutAnimationController = new AnimationController(vsync: this, duration: duration);
    scoreOutPositionAnimation = new Tween(begin: 100.0, end: 150.0).animate(
      new CurvedAnimation(parent: scoreOutAnimationController, curve: Curves.easeOut)
    );
    scoreOutPositionAnimation.addListener((){
      setState(() {});
    });
    scoreOutAnimationController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _scoreWidgetStatus = ScoreWidgetStatus.HIDDEN;
      }
    });
复制代码

当用户手指离开组件的时候,咱们将相应地设置状态,并启动300毫秒的计时器。 300毫秒后,咱们将为得分组件添加位置和不透明度动画。

void onTapUp(TapUpDetails tap) {
    // User removed his finger from button.
    scoreOutETA = new Timer(duration, () {
      scoreOutAnimationController.forward(from: 0.0);
      _scoreWidgetStatus = ScoreWidgetStatus.BECOMING_INVISIBLE;
    });
    holdTimer.cancel();
  }
复制代码

咱们还修改了onTapDown事件以处理某些边角状况。

void onTapDown(TapDownDetails tap) {
    // User pressed the button. This can be a tap or a hold.
    if (scoreOutETA != null) scoreOutETA.cancel(); // We do not want the score to vanish!
    if (_scoreWidgetStatus == ScoreWidgetStatus.HIDDEN) {
      scoreInAnimationController.forward(from: 0.0);
      _scoreWidgetStatus = ScoreWidgetStatus.BECOMING_VISIBLE;
    }
    increment(null); // Take care of tap
    holdTimer = new Timer.periodic(duration, increment); // Takes care of hold
  }
复制代码

最后,咱们须要选择用于score widget的位置和不透明度的控制器值。一个简单的开关就完成了。

Widget getScoreButton() {
    var scorePosition = 0.0;
    var scoreOpacity = 0.0;
    switch(_scoreWidgetStatus) {
      case ScoreWidgetStatus.HIDDEN:
        break;
      case ScoreWidgetStatus.BECOMING_VISIBLE :
        scorePosition = scoreInAnimationController.value * 100;
        scoreOpacity = scoreInAnimationController.value;
        break;
      case ScoreWidgetStatus.BECOMING_INVISIBLE:
        scorePosition = scoreOutPositionAnimation.value;
        scoreOpacity = 1.0 - scoreOutAnimationController.value;
    }
  return ...
}
复制代码

score widget的运行效果很棒,先弹出而后逐渐消失。

Score Widget 尺寸动画

到这一步,咱们几乎知道如何在分数增长时也改变大小。让咱们快速添加大小动画,而后继续搞火花闪烁效果

我已经更新了ScoreWidgetStatus枚举来保留一个额外的VISIBLE值。如今,咱们为size属性添加一个新的控制器。

scoreSizeAnimationController = new AnimationController(vsync: this, duration: new Duration(milliseconds: 150));
    scoreSizeAnimationController.addStatusListener((status) {
      if(status == AnimationStatus.completed) {
        scoreSizeAnimationController.reverse();
      }
    });
    scoreSizeAnimationController.addListener((){
      setState(() {});
    });
复制代码

控制器在150ms的时间内生成从01的值,完成以后((status == AnimationStatus.completed),又会生成从10的值。这会产生很好的增加和收缩效果。

void increment(Timer t) {
    scoreSizeAnimationController.forward(from: 0.0);
    setState(() {
      _counter++;
    });
复制代码

咱们须要注意处理枚举的visible属性状况。为此,咱们须要在 T​​ouch down事件中添加一些基本条件。

void onTapDown(TapDownDetails tap) {
    // User pressed the button. This can be a tap or a hold.
    if (scoreOutETA != null) {
      scoreOutETA.cancel(); // We do not want the score to vanish!
    }
    if(_scoreWidgetStatus == ScoreWidgetStatus.BECOMING_INVISIBLE) {
      // We tapped down while the widget was flying up. Need to cancel that animation.
      scoreOutAnimationController.stop(canceled: true);
      _scoreWidgetStatus = ScoreWidgetStatus.VISIBLE;
    }
    else if (_scoreWidgetStatus == ScoreWidgetStatus.HIDDEN ) {
        _scoreWidgetStatus = ScoreWidgetStatus.BECOMING_VISIBLE;
        scoreInAnimationController.forward(from: 0.0);
    }
    increment(null); // Take care of tap
    holdTimer = new Timer.periodic(duration, increment); // Takes care of hold
  }
复制代码

最后,咱们使用Widget中控制器的值

extraSize = scoreSizeAnimationController.value * 10;
...
height: 50.0 + extraSize,
width: 50.0  + extraSize,
...
复制代码

完整的代码,能够在github(gist.github.com/Kartik1607/… 中找到。咱们同时使用大小和位置动画。大小动画须要一些调整,咱们最后会介绍。

最后,火花闪烁动画

在进行火花闪烁动画以前,咱们须要对尺寸动画进行一些调整。目前,该按钮已增加太多。解决方法很简单,咱们将额外的乘数从10更改成一个较小的数字。

如今来看看火花闪烁动画,咱们能够看到到火花其实就是位置在变化的5张图片

我在MS Paint中制做了一个三角形和一个圆形的图片,并将其保存为flutter资源。而后,咱们就能够将该图片用做Image asset

在实现动画以前,让咱们考虑一下定位以及须要完成的一些任务:

  • 一、咱们须要定位5个图片,每张图片以不一样的角度造成一个完整的圆。

  • 二、咱们须要根据角度旋转图片

  • 三、随着时间增长圆的半径

  • 四、须要根据角度和半径找到坐标。

简单的三角函数给了咱们根据角度的正弦和余弦来得到xy坐标的公式。

var sparklesWidget =
        new Positioned(child: new Transform.rotate(
            angle: currentAngle - pi/2,
            child: new Opacity(opacity: sparklesOpacity,
                child : new Image.asset("images/sparkles.png", width: 14.0, height: 14.0, ))
          ),
          left:(sparkleRadius*cos(currentAngle)) + 20,
          top: (sparkleRadius* sin(currentAngle)) + 20 ,
      );
复制代码

如今,咱们须要建立5widget。每一个widget具备不一样的角度。一个简单的for循环就ok了。

for(int i = 0;i < 5; ++i) {
      var currentAngle = (firstAngle + ((2*pi)/5)*(i));
      var sparklesWidget = ...
      stackChildren.add(sparklesWidget);
    }
复制代码

2 * pi(360度)分红5个部分,并相应地建立一个widget。而后,咱们将widget添加到stackChildren数组中。

好了,到这一步,大多数的准备工做都作完了,咱们只须要设置sparkleRadius的动画并生成一个新的firstAngle便可。

sparklesAnimationController = new AnimationController(vsync: this, duration: duration);
    sparklesAnimation = new CurvedAnimation(parent: sparklesAnimationController, curve: Curves.easeIn);
    sparklesAnimation.addListener((){
      setState(() { });
    });

 void increment(Timer t) {
    sparklesAnimationController.forward(from: 0.0);
     ...
    setState(() {
    ...
      _sparklesAngle = random.nextDouble() * (2*pi);
    });
     
Widget getScoreButton() {
    ...
    var firstAngle = _sparklesAngle;
    var sparkleRadius = (sparklesAnimationController.value * 50) ;
    var sparklesOpacity = (1 - sparklesAnimation.value);
    ...
}     
复制代码

这就是咱们对flutter中的基本动画介绍。我将继续探索flutter,学习建立高级UI。

完整代码访问:github.com/Kartik1607/…

相关文章
相关标签/搜索