<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                在上一篇文章中,我帶你一起學習了 Flutter 中實現頁面路由的兩種方式:基本路由與命名路由,即手動創建頁面進行切換,和通過前置路由注冊后提供標識符進行跳轉。除此之外,Flutter 還在這兩種路由方式的基礎上,支持頁面打開和頁面關閉傳遞參數,可以更精確地控制路由切換。 通過前面第[12](https://time.geekbang.org/column/article/110292)、[13](https://time.geekbang.org/column/article/110859)、[14](https://time.geekbang.org/column/article/110848)和[15](https://time.geekbang.org/column/article/111673)篇文章的學習,我們已經掌握了開發一款樣式精美的小型 App 的基本技能。但當下,用戶對于終端頁面的要求已經不再滿足于只能實現產品功能,除了樣式美觀之外,還希望交互良好、有趣、自然。 動畫就是提升用戶體驗的一個重要方式,一個恰當的組件動畫或者頁面切換動畫,不僅能夠緩解用戶因為等待而帶來的情緒問題,還會增加好感。Flutter 既然完全接管了渲染層,除了靜態的頁面布局之外,對組件動畫的支持自然也不在話下。 因此在今天的這篇文章中,我會向你介紹 Flutter 中動畫的實現方法,看看如何讓我們的頁面動起來。 ## Animation、AnimationController 與 Listener 動畫就是動起來的畫面,是靜態的畫面根據事先定義好的規律,在一定時間內不斷微調,產生變化效果。而動畫實現由靜止到動態,主要是靠人眼的視覺殘留效應。所以,對動畫系統而言,為了實現動畫,它需要做三件事兒: 1. 確定畫面變化的規律; 2. 根據這個規律,設定動畫周期,啟動動畫; 3. 定期獲取當前動畫的值,不斷地微調、重繪畫面。 這三件事情對應到 Flutter 中,就是 Animation、AnimationController 與 Listener: 1. Animation 是 Flutter 動畫庫中的核心類,會根據預定規則,在單位時間內持續輸出動畫的當前狀態。Animation 知道當前動畫的狀態(比如,動畫是否開始、停止、前進或者后退,以及動畫的當前值),但卻不知道這些狀態究竟應用在哪個組件對象上。換句話說,Animation 僅僅是用來提供動畫數據,而不負責動畫的渲染。 2. AnimationController 用于管理 Animation,可以用來設置動畫的時長、啟動動畫、暫停動畫、反轉動畫等。 3. Listener 是 Animation 的回調函數,用來監聽動畫的進度變化,我們需要在這個回調函數中,根據動畫的當前值重新渲染組件,實現動畫的渲染。 接下來,我們看一個具體的案例:讓大屏幕中間的 Flutter Logo 由小變大。 首先,我們初始化了一個動畫周期為 1 秒的、用于管理動畫的 AnimationController 對象,并用線性變化的 Tween 創建了一個變化范圍從 50 到 200 的 Animaiton 對象。 然后,我們給這個 Animaiton 對象設置了一個進度監聽器,并在進度監聽器中強制界面重繪,刷新動畫狀態。 接下來,我們調用 AnimationController 對象的 forward 方法,啟動動畫: ~~~ class _AnimateAppState extends State<AnimateApp> with SingleTickerProviderStateMixin { AnimationController controller; Animation<double> animation; @override void initState() { super.initState(); // 創建動畫周期為 1 秒的 AnimationController 對象 controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 1000)); // 創建從 50 到 200 線性變化的 Animation 對象 animation = Tween(begin: 50.0, end: 200.0).animate(controller) ..addListener(() { setState(() {}); // 刷新界面 }); controller.forward(); // 啟動動畫 } ... } ~~~ 需要注意的是,我們在創建 AnimationController 的時候,設置了一個 vsync 屬性。這個屬性是用來防止出現不可見動畫的。vsync 對象會把動畫綁定到一個 Widget,當 Widget 不顯示時,動畫將會暫停,當 Widget 再次顯示時,動畫會重新恢復執行,這樣就可以避免動畫的組件不在當前屏幕時白白消耗資源。 我們在一開始提到,Animation 只是用于提供動畫數據,并不負責動畫渲染,所以我們還需要在 Widget 的 build 方法中,把當前動畫狀態的值讀出來,用于設置 Flutter Logo 容器的寬和高,才能最終實現動畫效果: ~~~ @override @override Widget build(BuildContext context) { return MaterialApp( home: Center( child: Container( width: animation.value, // 將動畫的值賦給 widget 的寬高 height: animation.value, child: FlutterLogo() ))); } ~~~ 最后,別忘了在頁面銷毀時,要釋放動畫資源: ~~~ @override void dispose() { controller.dispose(); // 釋放資源 super.dispose(); } ~~~ 我們試著運行一下,可以看到,Flutter Logo 動起來了: :-: ![](https://img.kancloud.cn/c7/3f/c73f5a245ecea87be428a83634ec12db_636x1132.gif) 圖 1 動畫示例 我們在上面用到的 Tween 默認是線性變化的,但可以創建 CurvedAnimation 來實現非線性曲線動畫。CurvedAnimation 提供了很多常用的曲線,比如震蕩曲線 elasticOut: ~~~ // 創建動畫周期為 1 秒的 AnimationController 對象 controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 1000)); // 創建一條震蕩曲線 final CurvedAnimation curve = CurvedAnimation( parent: controller, curve: Curves.elasticOut); // 創建從 50 到 200 跟隨振蕩曲線變化的 Animation 對象 animation = Tween(begin: 50.0, end: 200.0).animate(curve) ~~~ 運行一下,可以看到 Flutter Logo 有了一個彈性動畫: :-: ![](https://img.kancloud.cn/ce/0f/ce0f1ce6380329e3d9194518e2be2d05_640x1132.gif) 圖 2 CurvedAnimation 示例 現在的問題是,這些動畫只能執行一次。如果想讓它像心跳一樣執行,有兩個辦法: 1. 在啟動動畫時,使用 repeat(reverse: true),讓動畫來回重復執行。 2. 監聽動畫狀態。在動畫結束時,反向執行;在動畫反向執行完畢時,重新啟動執行。 具體的實現代碼,如下所示: ~~~ // 以下兩段語句等價 // 第一段 controller.repeat(reverse: true);// 讓動畫重復執行 // 第二段 animation.addStatusListener((status) { if (status == AnimationStatus.completed) { controller.reverse();// 動畫結束時反向執行 } else if (status == AnimationStatus.dismissed) { controller.forward();// 動畫反向執行完畢時,重新執行 } }); controller.forward();// 啟動動畫 ~~~ 運行一下,可以看到,我們實現了 Flutter Logo 的心跳效果。 :-: ![](https://img.kancloud.cn/a7/e5/a7e5b1fd635a557cb4289273bd299e48_636x1132.gif) 圖 3 Flutter Logo 心跳 ## AnimatedWidget 與 AnimatedBuilder 在為 Widget 添加動畫效果的過程中我們不難發現,Animation 僅提供動畫的數據,因此我們還需要監聽動畫執行進度,并在回調中使用 setState 強制刷新界面才能看到動畫效果。考慮到這些步驟都是固定的,Flutter 提供了兩個類來幫我們簡化這一步驟,即 AnimatedWidget 與 AnimatedBuilder。 接下來,我們分別看看這兩個類如何使用。 在構建 Widget 時,AnimatedWidget 會將 Animation 的狀態與其子 Widget 的視覺樣式綁定。要使用 AnimatedWidget,我們需要一個繼承自它的新類,并接收 Animation 對象作為其初始化參數。然后,在 build 方法中,讀取出 Animation 對象的當前值,用作初始化 Widget 的樣式。 下面的案例演示了 Flutter Logo 的 AnimatedWidget 版本:用 AnimatedLogo 繼承了 AnimatedWidget,并在 build 方法中,把動畫的值與容器的寬高做了綁定: ~~~ class AnimatedLogo extends AnimatedWidget { //AnimatedWidget 需要在初始化時傳入 animation 對象 AnimatedLogo({Key key, Animation<double> animation}) : super(key: key, listenable: animation); Widget build(BuildContext context) { // 取出動畫對象 final Animation<double> animation = listenable; return Center( child: Container( height: animation.value,// 根據動畫對象的當前狀態更新寬高 width: animation.value, child: FlutterLogo(), )); } } ~~~ 在使用時,我們只需把 Animation 對象傳入 AnimatedLogo 即可,再也不用監聽動畫的執行進度刷新 UI 了:\ ~~~ MaterialApp( home: Scaffold( body: AnimatedLogo(animation: animation)// 初始化 AnimatedWidget 時傳入 animation 對象 )); ~~~ 在上面的例子中,在 AnimatedLogo 的 build 方法中,我們使用 Animation 的 value 作為 logo 的寬和高。這樣做對于簡單組件的動畫沒有任何問題,但如果動畫的組件比較復雜,一個更好的解決方案是,**將動畫和渲染職責分離**:logo 作為外部參數傳入,只做顯示;而尺寸的變化動畫則由另一個類去管理。 這個分離工作,我們可以借助 AnimatedBuilder 來完成。 與 AnimatedWidget 類似,AnimatedBuilder 也會自動監聽 Animation 對象的變化,并根據需要將該控件樹標記為 dirty 以自動刷新 UI。事實上,如果你翻看[源碼](https://github.com/flutter/flutter/blob/ca5411e3aa99d571ddd80b75b814718c4a94c839/packages/flutter/lib/src/widgets/transitions.dart#L920),就會發現 AnimatedBuilder 其實也是繼承自 AnimatedWidget。 我們以一個例子來演示如何使用 AnimatedBuilder。在這個例子中,AnimatedBuilder 的尺寸變化動畫由 builder 函數管理,渲染則由外部傳入 child 參數負責: ~~~ MaterialApp( home: Scaffold( body: Center( child: AnimatedBuilder( animation: animation,// 傳入動畫對象 child:FlutterLogo(), // 動畫構建回調 builder: (context, child) => Container( width: animation.value,// 使用動畫的當前狀態更新 UI height: animation.value, child: child, //child 參數即 FlutterLogo() ) ) ) )); ~~~ 可以看到,通過使用 AnimatedWidget 和 AnimatedBuilder,動畫的生成和最終的渲染被分離開了,構建動畫的工作也被大大簡化了。 ## hero 動畫 現在我們已經知道了如何在一個頁面上實現動畫效果,那么如何實現在兩個頁面之間切換的過渡動畫呢?比如在社交類 App,在 Feed 流中點擊小圖進入查看大圖頁面的場景中,我們希望能夠實現小圖到大圖頁面逐步放大的動畫切換效果,而當用戶關閉大圖時,也實現原路返回的動畫。 這樣的跨頁面共享的控件動畫效果有一個專門的名詞,即“共享元素變換”(Shared Element Transition)。 對于 Android 開發者來說,這個概念并不陌生。Android 原生提供了對這種動畫效果的支持,通過幾行代碼,就可以實現在兩個 Activity 共享的組件之間做出流暢的轉場動畫。 又比如,Keynote 提供了的“神奇移動”(Magic Move)功能,可以實現兩個 Keynote 頁面之間的流暢過渡。 Flutter 也有類似的概念,即 Hero 控件。**通過 Hero,我們可以在兩個頁面的共享元素之間,做出流暢的頁面切換效果。** 接下來,我們通過一個案例來看看 Hero 組件具體如何使用。 在下面的例子中,我定義了兩個頁面,其中 page1 有一個位于底部的小 Flutter Logo,page2 有一個位于中部的大 Flutter Logo。在點擊了 page1 的小 logo 后,會使用 hero 效果過渡到 page2。 為了實現共享元素變換,我們需要將這兩個組件分別用 Hero 包裹,并同時為它們設置相同的 tag “hero”。然后,為 page1 添加點擊手勢響應,在用戶點擊 logo 時,跳轉到 page2: ~~~ class Page1 extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( body: GestureDetector(// 手勢監聽點擊 child: Hero( tag: 'hero',// 設置共享 tag child: Container( width: 100, height: 100, child: FlutterLogo())), onTap: () { Navigator.of(context).push(MaterialPageRoute(builder: (_)=>Page2()));// 點擊后打開第二個頁面 }, ) ); } } class Page2 extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Hero( tag: 'hero',// 設置共享 tag child: Container( width: 300, height: 300, child: FlutterLogo() )) ); } } ~~~ 運行一下,可以看到,我們通過簡單的兩步,就可以實現元素跨頁面飛行的復雜動畫效果了! :-: ![](https://img.kancloud.cn/c5/fe/c5fe68b6e627d8285ed6aadf932abcd6_636x1132.gif) 圖 4 Hero 動畫 ## 總結 好了,今天的分享就到這里。我們簡單回顧一下今天的主要內容吧。 在 Flutter 中,動畫的狀態與渲染是分離的。我們通過 Animation 生成動畫曲線,使用 AnimationController 控制動畫時間、啟動動畫。而動畫的渲染,則需要設置監聽器獲取動畫進度后,重新觸發組件用新的動畫狀態刷新后才能實現動畫的更新。 為了簡化這一步驟,Flutter 提供了 AnimatedWidget 和 AnimatedBuilder 這兩個組件,省去了狀態監聽和 UI 刷新的工作。而對于跨頁面動畫,Flutter 提供了 Hero 組件,只要兩個相同(相似)的組件有同樣的 tag,就能實現元素跨頁面過渡的轉場效果。 可以看到,Flutter 對于動畫的分層設計還是非常簡單清晰的,但造成的副作用就是使用起來稍微麻煩一些。對于實際應用而言,由于動畫過程涉及到頁面的頻繁刷新,因此我強烈建議你盡量使用 AnimatedWidget 或 AnimatedBuilder 來縮小受動畫影響的組件范圍,只重繪需要做動畫的組件即可,要避免使用進度監聽器直接刷新整個頁面,讓不需要做動畫的組件也跟著一起銷毀重建。 我把今天分享中所涉及的針對控件的普通動畫,AnimatedBuilder 和 AnimatedWidget,以及針對頁面的過渡動畫 Hero 打包到了[GitHub](https://github.com/cyndibaby905/22_app_animation)上,你可以把工程下載下來,多運行幾次,體會這幾種動畫的具體使用方法。 ## 思考題 最后,我給你留下兩個小作業吧。 ~~~ AnimatedBuilder( animation: animation, child:FlutterLogo(), builder: (context, child) => Container( width: animation.value, height: animation.value, child: child ) ) ~~~ 1. 在 AnimatedBuilder 的例子中,child 似乎被指定了兩遍(第 3 行的 child 與第 7 行的 child),你可以解釋下這么做的原因嗎? 2. 如果我把第 3 行的 child 刪掉,把 Flutter Logo 放到第 7 行,動畫是否能正常執行?這會有什么問題嗎?
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看