Flutter 手勢處理 & Hero 動畫

Flutter筆記發表於2019-05-26

App Store可以說是蘋果業內設計的標杆了。

我們就來簡單的實現一下 App Store的首頁裡其中的一個小功能。

先看圖:

Flutter 手勢處理 & Hero 動畫

可以看到,這裡有兩點需要關注一下:

  1. 在點選這個卡片的時候會縮放,鬆開或者滑動的時候會回彈回去。
  2. 跳新頁面的時候有元素共享。

實現結果:

Flutter 手勢處理 & Hero 動畫

手勢處理

在Flutter中的手勢事件分為兩層。

第一層有原始指標事件,它描述了螢幕上指標(例如,觸控,滑鼠和觸控筆)的位置和移動。

第二層有手勢,描述由一個或多個指標移動組成的語義動作。

簡單的手勢處理,我們使用 Flutter 封裝好的GestureDetector來處理就完全夠用。

我們這裡的圖片縮放效果就用GestureDetector來處理。

先來看一下GestureDetector 給我們提供了什麼樣的方法:

  • onTapDown:按下
  • onTap:點選動作
  • onTapUp:抬起
  • onTapCancel:觸發了 onTapDown,但並沒有完成一個 onTap 動作
  • onDoubleTap:雙擊
  • onLongPress:長按
  • onScaleStart, onScaleUpdate, onScaleEnd:縮放
  • onVerticalDragDown, onVerticalDragStart, onVerticalDragUpdate, onVerticalDragEnd, onVerticalDragCancel, onVerticalDragUpdate:在豎直方向上移動
  • onHorizontalDragDown, onHorizontalDragStart, onHorizontalDragUpdate, onHorizontalDragEnd, onHorizontalDragCancel, onHorizontalDragUpdate:在水平方向上移動
  • onPanDown, onPanStart, onPanUpdate, onPanEnd, onPanCancel:拖曳(觸碰到螢幕、在螢幕上移動)

那我們知道了這些方法,我們就可以來分析一下,哪些適合我們做這個效果:

我們可以看到,當我們的手指觸碰到卡片的時候就開始縮放,當開始移動或者抬起的時候回彈。

那我們根據上面 GestureDetector 的方法,可以看到 onPanDown、onPanCancel 似乎非常適合我們的需求。

那我們就可以來試一下:

Flutter 手勢處理 & Hero 動畫

監聽手勢的方法有了,那我們下面就來寫動畫。

如何讓Card 進行縮放呢,Flutter 有一個 Widget,ScaleTransition

照例點開原始碼看註釋:

/// Animates the scale of a transformed widget.
複製程式碼

對scale進行動畫縮放的元件。

那這就結了,直接在 onPanDown、onPanCancel 方法中寫上動畫就完了:

Widget createItemView(int index) {
  var game = _games[index]; // 獲取資料
  // 定義動畫控制器
  var _animationController = AnimationController(
    vsync: this,
    duration: Duration(milliseconds: 200),
  );  
  // 定義動畫
  var _animation =
    Tween<double>(begin: 1, end: 0.98).animate(_animationController);
  return GestureDetector(
    onPanDown: (details) {
      print('onPanDown');
      _animationController.forward(); // 點選的時候播放動畫
    },
    onPanCancel: () {
      print('onPanCancel');
      _animationController.reverse(); // cancel的時候回彈動畫
    },

    child: Container(
      height: 450,
      margin: EdgeInsets.symmetric(horizontal: 30, vertical: 10),
      child: ScaleTransition(
        scale: _animation, // 定義動畫
        child: Stack( // 圓角圖片為背景,上面為text
          children: <Widget>[
            Positioned.fill(
              child: ClipRRect(
                borderRadius: BorderRadius.all(Radius.circular(15)),
                child: Image.asset(
                  game.imageUrl,
                  fit: BoxFit.cover,
                ),
              ),
            ),
            
            Padding(
              padding: const EdgeInsets.all(18.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text(
                    game.headText,
                    style: TextStyle(
                      fontSize: 16,
                      color: Colors.grey,
                    ),
                  ),
                  
                  Expanded(
                    child: Text(
                      game.title,
                      style: TextStyle(
                        fontSize: 30,
                        color: Colors.white,
                      ),
                    ),
                  ),
                  
                  Text(
                    game.footerText,
                    style: TextStyle(
                      fontSize: 16,
                      color: Colors.grey,
                    ),
                  ),
                ],
              ),
            )
          ],
        ),
      )),
  );
}
複製程式碼

這樣就可以完成我們剛上圖的動畫效果了。

這裡有一個需要注意的地方是:

ListView 中必須每一個 item 有一個 動畫。

不然所有的item公用一個動畫的話,點選其中一個,所有的item 都會執行動畫效果。

Hero動畫

點選縮放效果我們處理完了,下面就應該來跳轉了。

在Android中,5.0以後版本就有了元素共享,可以實現這種效果。

在Flutter當中我們可以使用 Hero 來實現這個效果。

開啟官網看介紹:

A widget that marks its child as being a candidate for hero animations.

When a PageRoute is pushed or popped with the Navigator, the entire screen's content is replaced. An old route disappears and a new route appears. If there's a common visual feature on both routes then it can be helpful for orienting the user for the feature to physically move from one page to the other during the routes' transition. Such an animation is called a hero animation. The hero widgets "fly" in the Navigator's overlay during the transition and while they're in-flight they're, by default, not shown in their original locations in the old and new routes.

To label a widget as such a feature, wrap it in a Hero widget. When navigation happens, the Hero widgets on each route are identified by the HeroController. For each pair of Hero widgets that have the same tag, a hero animation is triggered.

If a Hero is already in flight when navigation occurs, its flight animation will be redirected to its new destination. The widget shown in-flight during the transition is, by default, the destination route's Hero's child.

For a Hero animation to trigger, the Hero has to exist on the very first frame of the new page's animation.

Routes must not contain more than one Hero for each tag.
複製程式碼

簡單來說:

Hero動畫就是在路由切換時,有一個共享的Widget可以在新舊路由間切換,由於共享的Widget在新舊路由頁面上的位置、外觀可能有所差異,所以在路由切換時會逐漸過渡,這樣就會產生一個Hero動畫。

要觸發Hero動畫,Hero必須存在於新頁面動畫的第一幀。

並且一個路由裡只能有一個Hero 的 tag。
複製程式碼

說了這麼多,怎麼用?

// Page 1
Hero(
  tag: "avatar", //唯一標記,前後兩個路由頁Hero的tag必須相同
  child: ClipOval(
    child: Image.asset("images/avatar.png",
                       width: 50.0,),
  ),
),

// Page 2
Center(
  child: Hero(
    tag: "avatar", //唯一標記,前後兩個路由頁Hero的tag必須相同
    child: Image.asset("images/avatar.png"),
  ),
)
複製程式碼

可以看到只需要在你想要共享的widget 前加上 Hero,寫上 tag即可。

趕緊試一下:

child: Container(
  height: 450,
  margin: EdgeInsets.symmetric(horizontal: 30, vertical: 10),
  child: ScaleTransition(
    scale: _animation,
    child: Hero(
      tag: 'hero',
      child: Stack(
        children: <Widget>[
          Positioned.fill(
            child: ClipRRect(
              borderRadius: BorderRadius.all(Radius.circular(15)),
              child: Image.asset(
                game.imageUrl,
                fit: BoxFit.cover,
              ),
            ),
          ),
   
複製程式碼

執行看下:

Flutter 手勢處理 & Hero 動畫

直接黑屏了是什麼鬼?

看到了報錯資訊:

There are multiple heroes that share the same tag within a subtree.

多個hero widget 使用了同一個標籤
複製程式碼

那我們改造一下:

child: Container(
  height: 450,
  margin: EdgeInsets.symmetric(horizontal: 30, vertical: 10),
  child: ScaleTransition(
    scale: _animation,
    child: Hero(
      tag: 'hero${game.title}',
      child: Stack(
        children: <Widget>[
          Positioned.fill(
            child: ClipRRect(
              borderRadius: BorderRadius.all(Radius.circular(15)),
              child: Image.asset(
                game.imageUrl,
                fit: BoxFit.cover,
              ),
            ),
          ),
 // ........程式碼省略
複製程式碼

我們使用 ListView 裡的資料來填充tag,這樣就不會重複了,執行一下:

Flutter 手勢處理 & Hero 動畫

這跳轉的時候文字下面有兩個下劃線是什麼鬼?

查了一下,是因為跳轉的時候,Flutter 把源 Hero 放在了疊加層,而疊加層裡是沒有 Theme的。

簡單理解就是疊加層裡沒有Scaffold,所以就會出現下劃線。

解決辦法如下:

在textStyle中加入 decoration: TextDecoration.none,

現在就完全沒有問題了:

Flutter 手勢處理 & Hero 動畫

總結

在初學Flutter 時,我們確實會出現這樣那樣的問題。

不要心煩,點開原始碼,或者去 Flutter 官網找到該類,看一下注釋和demo,問題分分鐘解決。

程式碼已經傳至GitHub:github.com/wanglu1209/…

Flutter 手勢處理 & Hero 動畫

相關文章