Flutter - 创建拍手动画

在本文中,我们将从头开始探索Flutter动画。我们将通过在Flutter中创建拍手动画的模型来学习关于动画的一些核心概念。

正如标题所说,这篇文章将更多地关注动画而不是关于Flutter的基础知识。


入门

我们将从创建新的flutter项目开始。只需创建一个新的颤振项目,我们就会受到这个代码的欢迎。

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为我们提供了一些带有入门代码。它已经在管理点击次数状态,并为我们创建了一个浮动操作按钮。

我们目前拥有的按钮

以下是我们想要实现的最终产品。

我们将创建的动画。作者:Thuy Gia Nguyen

在添加动画之前,让我们快速浏览并修复一些简单的问题。

  1. 更改按钮图标和背景。
  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(),
            ],
          )
      ),
    );
  }
}

看看最终产品,我们需要添加3件事。

  1. 更改小部件的大小。
  2. 按下按钮时显示分数小部件,并在释放时隐藏它。
  3. 添加那些微小的洒落小部件并为它们制作动画。

让我们一个接一个地慢慢增加学习曲线。首先,我们需要了解一些关于动画的基本知识。


了解Flutter中基本动画的组件

动画只不过是一些随时间变化的值。例如,当我们点击按钮时,我们想要从底部上升得分小部件的动画,当我们离开按钮时,它应该上升更多然后隐藏。

如果您只查看得分小部件,我们需要在一段时间内更改小部件的位置和不透明度

new Positioned(
        child: new Opacity(opacity: 1.0, 
          child: new Container(
            ...
          )),
        bottom: 100.0
    );

得分小工具

假设我们想要得分小部件需要150毫秒从底部显示自己。在下面的时间表上考虑这一点

这是一个简单的2D图。这个职位会随着时间而变化。

请注意,对角线是直的。如果你愿意,这甚至可以是弯曲的。

你可以随着时间的推移缓慢增加位置,然后越来越快。或者你可以让它以超高的速度进入,然后在最后减速。

这是我们介绍第一个组件的地方:动画控制器。

scoreInAnimationController = new AnimationController(duration: new Duration(milliseconds: 150), vsync: this);

动画控制器构造

在这里,我们为动画创建了一个简单的控制器。我们已经指定要运行动画持续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
*/

控制器在150毫秒内生成0.0到1.0的数字。请注意,生成的值几乎是线性的。0.2,0.3,0.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
*/

我们通过将父级设置为控制器并提供我们想要遵循的曲线来创建“弯曲”动画。我们可以在颤振曲线文档页面中使用一系列曲线选择控制器在150毫秒的时间内为弯曲的动画小部件提供从0.0到1.0的值。弯曲的动画小部件根据我们设置的曲线插入这些值。

不过,我们的价值从0.0到1.0。但我们希望得分小部件的值从0.0到100.0。我们可以简单地乘以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类生成的值开始结束。我们使用了早期的scoreInAnimationController,它使用了一条线性曲线。相反,我们可以使用我们的反弹曲线来获得不同的价值。Tween的优点并不止于此。你也可以补充其他东西。您可以使用进一步扩展基础补间类的类直接补间颜色,偏移,位置和其他窗口小部件属性。


分数小部件位置动画

在这一点上,我们有足够的知识,当我们按下按钮时,我们的分数小部件从底部弹出,当我们点击时隐藏。

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

动画的当前状态

分数小部件会弹出。但仍有一个问题。

当我们多次点击按钮时,分数小部件会一次又一次地弹出。这是因为上面的代码中有一个小错误。我们告诉控制器每次按下按钮时从0开始转发。

现在,让我们为得分小部件添加out动画。

首先,我们添加一个枚举来更轻松地管理得分小部件的状态。

enum ScoreWidgetStatus {
  HIDDEN,
  BECOMING_VISIBLE,
  BECOMING_INVISIBLE
}

然后,我们创建一个out动画控制器。动画控制器将非线性地将小部件的位置从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();
  }

我们还修改了点击事件以处理一些角落情况。

 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
  }

最后,我们需要选择我们需要使用哪个控制器的值来获得得分小部件的位置和不透明度。一个简单的开关完成这项工作。

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 ...
}
14.gif

得分小部件效果很好。它弹出然后逐渐淡出。


分数小工具大小动画

在这一点上,我们几乎已经知道如何在分数增加时改变大小。让我们快速添加大小动画,然后我们进入微小的火花

我更新了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(() {});
    });

控制器在150 ms的时间内从0到1生成值,一旦完成,我们就会生成从1到0的值。这会产生很好的增长和收缩效果。

我们还更新了增量函数,以便在数字递增时启动动画。

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

我们需要处理处理枚举的可见属性的案例。为此,我们需要在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
  }

最后,我们在小部件中使用控制器中的值。

extraSize = scoreSizeAnimationController.value * 10;
...
height: 50.0 + extraSize,
width: 50.0  + extraSize,
...

完整的代码,可以在这个github gist找到我们同时拥有大小和位置动画。大小动画需要稍微调整一下,我们最后会对此进行调整。

大小和位置动画一起工作。


闪耀动画

在做火花动画之前,我们需要对大小动画进行一些调整。按钮现在增长太多了。修复很简单。我们将extrasize乘数从10更改为更低的数字。

现在来到闪光动画,我们可以观察到闪光只是5个图像,其位置正在发生变化。

我在MS Paint中制作了一个三角形和一个圆圈的图像,然后将其保存为Flutter资源。然后我们可以将该图像用作图像资源。

在动画之前,让我们想一下我们需要完成的定位和一些任务。

  1. 我们需要定位5个图像,每个图像以不同的角度形成一个完整的圆圈。
  2. 我们需要根据角度旋转图像。
  3. 我们需要随着时间增加圆的半径。
  4. 我们需要根据角度和半径找到坐标。

简单的三角函数为我们提供了基于角度的sin和余弦得到x和y坐标的公式。

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

现在,我们需要创建其中的5个小部件。每个小部件都有不同的角度。一个简单的for循环就可以了。

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

我们简单地将2 * pi,(360度)分成5个部分并相应地创建一个小部件。然后,我们将小部件添加到一个数组,该数组将作为堆栈的子级。

现在,在这一点上,大部分工作已经完成。我们只需要为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的基本动画的介绍。我会继续探索更多的东西,并学习创建高级用户界面。

你可以在我的混帐回购协议得到完整的代码在这里

转:https://proandroiddev.com/flutter-animation-creating-mediums-clap-animation-in-flutter-3168f047421e

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

推荐阅读更多精彩内容