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閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容