使用Flutter仿寫TikTok的手勢交互

本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨(dú)家發(fā)布

寫在前面

Flutter 是 Google推出并開源的移動應(yīng)用開發(fā)框架,主打跨平臺、高保真、高性能。開發(fā)者可以通過 Dart語言開發(fā) App,一套代碼同時運(yùn)行在 iOS 和 Android平臺。

Flutter官網(wǎng):https://flutter-io.cn

抖音,英文名TikTok,一款火遍全球的短視頻App。在玩抖音的日子里,最令我感到舒服的就是抖音的手勢交互,加上近期都在進(jìn)行Flutter方面的學(xué)習(xí),因此就產(chǎn)生了使用Flutter來仿寫TikTok手勢交互的想法。

來看看實(shí)現(xiàn)的效果

Gif:https://giphy.com/gifs/Y0nMQwaOg14vWmwQDz

Github地址:https://github.com/ditclear/tiktok_gestures

demo下載:

GestureDetector以及Transform

既然是手勢交互,那么就必然要檢測手勢,在Flutter中提供了GestureDetector來幫助開發(fā)者,并提供了多個回

調(diào)來處理手勢。

Property/Callback Description
onTapDown 用戶每次和屏幕交互時都會被調(diào)用
onTapUp 用戶停止觸摸屏幕時觸發(fā)
onTap 短暫觸摸屏幕時觸發(fā)
onTapCancel 用戶觸摸了屏幕,但是沒有完成Tap的動作時觸發(fā)
onDoubleTap 用戶在短時間內(nèi)觸摸了屏幕兩次
onLongPress 用戶觸摸屏幕時間超過500ms時觸發(fā)
onVerticalDragDown 當(dāng)一個觸摸點(diǎn)開始跟屏幕交互,同時在垂直方向上移動時觸發(fā)
onVerticalDragStart 當(dāng)觸摸點(diǎn)開始在垂直方向上移動時觸發(fā)
onVerticalDragUpdate 屏幕上的觸摸點(diǎn)位置每次改變時,都會觸發(fā)這個回調(diào)
onVerticalDragEnd 當(dāng)用戶停止移動,這個拖拽操作就被認(rèn)為是完成了,就會觸發(fā)這個回調(diào)
onVerticalDragCancel 用戶突然停止拖拽時觸發(fā)
onHorizontalDragDown 當(dāng)一個觸摸點(diǎn)開始跟屏幕交互,同時在水平方向上移動時觸發(fā)
onHorizontalDragStart 當(dāng)觸摸點(diǎn)開始在水平方向上移動時觸發(fā)
onHorizontalDragUpdate 屏幕上的觸摸點(diǎn)位置每次改變時,都會觸發(fā)這個回調(diào)
onHorizontalDragEnd 水平拖拽結(jié)束時觸發(fā)
onHorizontalDragCancel onHorizontalDragDown沒有成功完成時觸發(fā)
onPanDown 當(dāng)觸摸點(diǎn)開始跟屏幕交互時觸發(fā)
onPanStart 當(dāng)觸摸點(diǎn)開始移動時觸發(fā)
onPanUpdate 屏幕上的觸摸點(diǎn)位置每次改變時,都會觸發(fā)這個回調(diào)
onPanEnd pan操作完成時觸發(fā)
onScaleStart 觸摸點(diǎn)開始跟屏幕交互時觸發(fā),同時會建立一個焦點(diǎn)為1.0
onScaleUpdate 跟屏幕交互時觸發(fā),同時會標(biāo)示一個新的焦點(diǎn)
onScaleEnd 觸摸點(diǎn)不再跟屏幕有任何交互,同時也表示這個scale手勢完成

GestureDetector并不會監(jiān)聽上面所有的手勢,只有傳入的callbacks非空時,才會監(jiān)聽。所以,如果你想要禁用某個手勢時,可以給對應(yīng)的callback傳null。

本文主要關(guān)注的的是拖動相關(guān)的,比如onPanXX、onHorizontalDragXX、onVerticalDragXX等等回調(diào)事件。

Transform可以在其子Widget繪制時對其應(yīng)用一個矩陣變換(transformation),Matrix4是一個4D矩陣,通過它我們可以實(shí)現(xiàn)各種矩陣操作。

Container(
  color: Colors.black,
  child: new Transform(
    alignment: Alignment.topRight, //相對于坐標(biāo)系原點(diǎn)的對齊方式
    transform: new Matrix4.skewY(0.3), //沿Y軸傾斜0.3弧度
    child: new Container(
      padding: const EdgeInsets.all(8.0),
      color: Colors.deepOrange,
      child: const Text('Apartment for rent!'),
    ),
  ),
);

效果如下:

在Flutter中提供了一些封裝好的transform效果供開發(fā)者選擇,比如:平移(translate)、旋轉(zhuǎn)(rotate)、縮放(scale)。

在了解了這兩點(diǎn)之后,我們來逐步分解前文的效果。

交互分解

首先,需要明確的是這些交互效果其實(shí)都是通過檢測手指的滑動,得到一個x坐標(biāo)或者y坐標(biāo)的偏移量,然后配合Transform進(jìn)行各種不同的變換,明白了這一點(diǎn),想做到這樣的效果并不難。

  • 首頁的交互
image

Gif :https://media.giphy.com/media/iFgOIQ7abZx0i98MCA/giphy.gif

這里的交互都是橫向的滑動,因此這里主要處理onHorizontalDragXX相關(guān)的事件。

然后來看看首頁的布局:

Left:拍攝頁 Middle:主頁 Right:用戶頁

外層是一個GestureDetector用于處理整個頁面的手勢,里面用的是一個Stack,類似于Android中的FrameLayout,它包含3個Transform的子Widget。

這里選取拍攝頁(left)來具體談?wù)?

通過觀察可以發(fā)現(xiàn),隨著偏移量的改變,這里其實(shí)包含兩個變化:1.縮放 2. 前景色透明度。

縮放可以直接采用前文提到的Transform.scale,前景色可以用foregroundDecoration通過改變Color的透明度來達(dá)到效果,看看實(shí)現(xiàn):

/// 左側(cè)Widget
///
/// 通過 [Transform.scale] 進(jìn)行根據(jù) [offsetX] 縮放
/// 最小 0.88 最大為 1
Transform buildLeftPage(double screenWidth) {
  return Transform.scale(
    scale: 0.88 + 0.12 * offsetX / screenWidth < 0.88 ? 0.88 : 0.88 + 0.12 * offsetX / screenWidth,
    child: Container(
      child: Image.asset(
        "assets/left.png",
        fit: BoxFit.fill,
      ),
      foregroundDecoration: BoxDecoration(
          color: Color.fromRGBO(0, 0, 0, 1 - (offsetX / screenWidth)),
         ),
    ),
  );
}

當(dāng)我們的手指在橫向移動的時候,記錄下偏移總量offsetX,然后通過setState進(jìn)行更新。

onHorizontalDragUpdate: (details) {
  // 控制 offsetX 的值在 -screenWidth 到 screenWidth 之間
  if (offsetX + details.delta.dx >= screenWidth) {
    setState(() {
      offsetX = screenWidth;
    });
  } else if (offsetX + details.delta.dx <= -screenWidth) {
    setState(() {
      offsetX = -screenWidth;
    });
  } else {
    setState(() {
      offsetX += details.delta.dx;
    });
  }
}

通過setState更新偏移量offsetX之后,F(xiàn)lutter便會重新渲染視圖,從而達(dá)到上圖的效果。

  • Hero動畫

Gif:https://media.giphy.com/media/Q7LdOFFBM4HB8xGSpg/giphy.gif

Flutter提供了Hero動畫來實(shí)現(xiàn)這樣的過渡效果。Hero指的是可以在路由(頁面)之間“飛行”的widget,簡單來說Hero動畫就是在路由切換時,有一個共享的Widget可以在新舊路由間切換,由于共享的Widget在新舊路由頁面上的位置、外觀可能有所差異,所以在路由切換時會逐漸過渡,這樣就會產(chǎn)生一個Hero動畫。

/// tiktok_page.dart
Widget build(BuildContext context) {
        return  Hero(
              tag: "detail",
              //child
            )
 )               
 
 /// detail_page.dart
 Widget build(BuildContext context) {
    return Hero(
      tag: "detail",
      // child
        )
  }

保證tag一致就可以了。

  • 詳情頁的交互

Gif :https://media.giphy.com/media/h4HMGtcLLcQQmqTZTI/giphy.gif

跟首頁一樣的思路,只是這里的手勢是垂直方向。

布局同樣是GestureDetector加上Stack再配合Transform.translate

Hero(
      tag: "detail",
      child: GestureDetector(
        onVerticalDragUpdate: (details){
          // dy 不超過 -screenHeight * 0.6
          dy += details.delta.dy;
          if ((dy < 0 && dy.abs() > screenHeight * 0.6)) {
            dy = -screenHeight * 0.6;
          } else {
            setState(() {});
          }
        },
        child: Stack(
          children: <Widget>[
            Image.asset(
              "assets/detail.png",
              fit: BoxFit.fitWidth,
              width: screenWidth,
              height: screenHeight,
            ),
            Transform.translate(
              offset: Offset(0, dy + screenHeight),
              child: Container(
                  height: screenHeight * 0.6,
                  child: GestureDetector(
                    onTap: () {},
                    child: Image.asset(
                        "assets/comment.png",),
                  )
              ),
            ),
          ],
        ),
      ),
    );

在手指離開屏幕時,根據(jù)偏移利用動畫進(jìn)行調(diào)整。

onVerticalDragEnd: (_){
          // 滑動截止時,根據(jù) dy 判斷是展開還是回縮
          if (dy < 0) {
            if (!isCommentShow && dy.abs() > screenHeight * 0.2) {
              if (dy.abs() > screenHeight * 0.2) {
                animateToTop(screenHeight);
              } else {
                animateToBottom(screenHeight);
              }
            } else {
              if (dy.abs() > screenHeight * 0.4) {
                animateToTop(screenHeight);
              } else {
                animateToBottom(screenHeight);
              }
            }
          }
        },

寫在最后

總的來說,這些交互都是依靠著對手勢的檢測做到的,相比于Android,F(xiàn)lutter有著一切都是Widget的概念,

GestureDetector以及Hero都是Widget而且提供了很多回調(diào)函數(shù),再配合數(shù)據(jù)驅(qū)動UI和Flutter優(yōu)秀的渲染機(jī)制,減輕了開發(fā)者進(jìn)行手勢交互的難度。

Github地址:https://github.com/ditclear/tiktok_gestures

如果本文對你有幫助,請點(diǎn)贊支持。

參考資料:

==================== 分割線 ======================

如果你想了解更多關(guān)于MVVM、Flutter、響應(yīng)式編程方面的知識,歡迎關(guān)注我。

你可以在以下地方找到我:

簡書:http://www.lxweimin.com/u/117f1cf0c556

掘金:https://juejin.im/user/582d601d2e958a0069bbe687

Github: https://github.com/ditclear

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。