如何利用 Flutter 實現炫酷的 3D 卡片和帥氣的 360° 展示效果

語言: CN / TW / HK

theme: smartblue

我正在參加「創意開發 投稿大賽」詳情請看:掘金創意開發大賽來了!

本篇將帶你在 Flutter 上快速實現兩個炫酷的動畫特效,希望最後的效果可以驚豔到你。

這次靈感的來源於更新 MIUI 13 時剛好看到的卡片效果,其中除了卡片會跟隨手勢出現傾斜之外,內容裡的部分文字和綠色圖示也有類似懸浮的視差效果,恰逢此時靈機一動,我們也來用 Flutter 快速實現炫酷的 3D 視差卡片,最後再拓展實現一個支援帥氣的 360° 展示的卡片效果

❤️ 本文正在參加徵文投稿活動,還請看官們走過路過來個點贊一鍵三連,感激不盡~

既然需要卡片跟隨手勢產生不規則形變,我們第一個想到的肯定是矩陣變換,在 Flutter 裡我們可以使用 Matrix4 配合 Transform 來實現矩陣變換效果。

開始之前,首先我們建立用 Transform 巢狀一個 GestureDetector ,並繪製出一個 300x400 的圓角卡片,用於後續進行矩陣變換處理。

js Transform( transform: Matrix4.identity(), child: GestureDetector( child: Container( width: 300, height: 400, padding: EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.blueGrey, borderRadius: BorderRadius.circular(20), ), ), ), );

接著,如下程式碼所示,因為我們需要卡片跟隨手勢進行矩陣變換,所以我們可以直接在 GestureDetectoronPanUpdate 裡獲取到手勢資訊,例如 localPosition 位置資訊,然後把對應的 dxdy賦值到 Matrix4rotateXrotateY 上實現旋轉。

js child: Transform( transform: Matrix4.identity() ..rotateX(touchY) ..rotateY(touchX), alignment: FractionalOffset.center, child: GestureDetector( onPanUpdate: (details) { setState(() { touchX = details.localPosition.dx; touchY = details.localPosition.dy; }); }, child: Container(

這裡有個需要注意的是:上面程式碼裡 rotateX 使用的是 touchY ,而 rotateY 使用的是 touchX ,為什麼要這樣做呢?

⚠️舉個例子,當我們手指左右移動時,是希望卡片可以圍繞 Y 軸進行旋轉,所以我們會把 touchX 傳遞給了 rotateY ,同樣 touchY 傳遞給 rotateX 也是一個道理。

但是當我們實際執行上述程式碼之後,如下圖所示,可以看到基本上我們只是稍微移動手指,卡片就會陷入瘋狂旋轉的情況,並且實際的旋轉速度會比 GIF 裡快很多。

問題的原因其實是因為 rotateXrotateY 需要的是一個 angle 引數,假設這裡對 rotateXrotateY 設定 pi / 4 ,就可以看到卡片在 X 軸和 Y 軸上都產生了 45 度的旋轉效果。

js Transform( transform: Matrix4.identity() ..rotateX(pi / 4) ..rotateY(pi / 4), alignment: FractionalOffset.center,

所以如果直接使用手勢的 localPosition 作用於 Matrix4 肯定是不行的,我們首先需要對手勢資料進行一個取樣,因為程式碼裡我們設定了 FractionalOffset.center ,所以我們可以用卡片的中心點來計算手指位置,再進行壓縮處理

如下程式碼所示,我們通過以卡片中心點為原點進行計算,其中 / 2 就是得到卡片的中心點,/ 100 是對資料進行壓縮取樣,但是為什麼 touchXtouchY 的計算方式是相反的呢

js touchX = (cardWidth / 2 - details.localPosition.dx) / 100; touchY = (details.localPosition.dy - cardHeight / 2 ) / 100;

如下圖所示,因為在設定 rotateXrotateY 時,賦予 > 0 的資料時卡片就會以圖片中的方向進行旋轉,由於我們是需要手指往哪邊滑動,卡片就往哪邊傾斜,所以:

  • 當我們往左水平滑動時,需要卡片往左邊傾斜,也就是圖中繞 Y 軸轉動的 >0 的方向,並且越靠近左邊需要正向的 Angle 數值越大,由於此時 localPosition.dx 是越往左越小,所以需要利用 CardWidth / 2 - details.localPosition.dx 進行計算,得到越往左有越大的正向 Angle 數值
  • 同理,當我們往下滑動時,需要卡片往下邊傾斜,也就是圖中繞 X 軸轉動的 >0 的方向,並且越靠近下邊需要正向 Angle 數值越大,由於此時 localPosition.dy 越往下越大,所以使用 details.localPosition.dy - cardHeight / 2 去計算得到正確資料

| | | | ----------------------------------------------------------- | ------------------------------------------ |

如果覺得太抽象,可以結合上邊右側的動圖,和大家買股票一樣,圖中顯示紅色時是正數,顯示綠色時是負數,可以看到:

  • 手指往左移動時,第一行 TouchX 是紅色正數,被設定給 rotateY , 然後卡片繞 Y 軸正方向旋轉
  • 手指往下移動時,第二行 TouchY 是紅色正數,被設定給 rotateX , 然後卡片繞 X 軸正方向旋轉

到這裡我們就初步實現了卡片跟隨手機旋轉的效果,但是這時候的立體旋轉效果看起來其實“很彆扭”,總感覺差了點什麼,其實這是因為卡片在旋轉時沒有產生視覺上的深度感知

所以我們可以通過矩陣的透視變換調整視覺效果,而為了在 Z 方向實現深度感知,我們需要在矩陣中配置 .setEntry(3, 2, 0.001) ,這裡的 3 表示第 3 列,2 表示第 2 行,因為是從 0 開始排列,所以也就是圖片中 Z 的位置。

其實 .setEntry(3, 2, 0.001) 就是調整 Z 軸的視角,而在 Z 上的 0.001 就是需要的透視效果測量值,類似於相機上的對焦點進行放大和縮小的作用,這個數字越大就會讓交點處看起來好像離你視覺更近,所以最終程式碼如下

js Transform( transform: Matrix4.identity() ..setEntry(3, 2, 0.001) ..rotateX(touchY) ..rotateY(touchX), alignment: FractionalOffset.center,

執行之後,可以看到在增加了 Z 角度的視角調整之後,這時候看起來的立體效果就好了很多,並且也有了類似 3D 空間的感覺。

接著我們在卡片上放上一個新增一個 13Text 文字,執行之後可以看到此時文字是跟隨卡片發生變化,而接下來我們需要做的,就是通過另外一個 Transform 來讓 Text 文字和卡片之間產生視差,從而出現懸浮的效果

| | | | ----------------------------------------------------------- | ---------------------------------------- |

所以接下來需要給文字內容設定一個 translateMatrix4 ,讓它向著傾斜角度的相反方向移動,然後對前面的 touchXtouchY 進行放大,然後再通過 - 10 操作來產生一個位差。

js Transform( transform: Matrix4.identity() ..translate(touchX * 100 - 10, touchY * 100 - 10, 0.0),

-10 這個是我隨意寫的,你也可以根據自己的需求調節。

例如,這時候當卡片往左傾斜時,文字就會向右移動,從而產生視覺差的效果,得到類似懸浮的感覺。

| | | | ----------------------------------------------------------- | ----------------------------------------- |

完成這一步之後,接下來可以我們對文字內容進行一下美化處理,例如增加漸變顏色,新增陰影,更換字型,目的是讓字型看起來更加具備立體的效果,這裡使用的 shader ,也可以讓文字在移動過程中出現不同角度的漸變效果

| | | | ----------------------------------------------------------- | ----------------------------------------- |

最後,我們還需要對卡片旋轉進行一個範圍約束,這裡主要是通過卡片大小比例:

  • onPanUpdate 時對 touchXtouchY 進行範圍約束,從而約束的卡片的傾斜角度
  • 增加了 startTransform 標誌位,用於在 onTapUp 或者 onPanEnd 之後,恢復卡片回到預設狀態的作用。

```js Transform( transform: Matrix4.identity() ..setEntry(3, 2, 0.001) ..rotateX(startTransform ? touchY : 0.0) ..rotateY(startTransform ? touchX : 0.0), alignment: FractionalOffset.center, child: GestureDetector( onTapUp: () => setState(() { startTransform = false; }), onPanCancel: () => setState(() => startTransform = false), onPanEnd: () => setState(() { startTransform = false; }), onPanUpdate: (details) { setState(() => startTransform = true); ///y軸限制範圍 if (details.localPosition.dx < cardWidth * 0.55 && details.localPosition.dx > cardWidth * 0.3) { touchX = (cardWidth / 2 - details.localPosition.dx) / 100; }

  ///x軸限制範圍
  if (details.localPosition.dy > cardHeight * 0.4 &&
      details.localPosition.dy < cardHeight * 0.6) {
    touchY = (details.localPosition.dy - cardHeight / 2) / 100;
  }
},
child:

```

到這裡,我們只需要在全域性再進行一些美化處理,執行之後就會如下圖所示,再配合陰影和漸變效果,整體的視覺立體感會更強烈,此時我們基本就實現了一開始想要的功能,

完整程式碼可見: card_perspective_demo_page.dart

Web 體驗地址,PC 端記得開 Chrome 手機模式: 3D 視差卡片

那有人可能就想問了: 學會了這個我們還可以實現什麼

舉個例子,比如我們可以實現一個 “偽3D” 的 360° 卡片效果,利用堆疊實現立體的電子銀行卡效果。

依舊是前面的手勢旋轉邏輯,只是這裡我們可以把具有前後畫面的銀行卡圖片,通過 IndexedStack 巢狀起來,巢狀之後主要是根據旋轉角度來調整 IndexedStack 裡需要展示的圖片,然後利用透視旋轉來實現類似 3D 物體的 360° 旋轉展示

這裡的關鍵是通過手勢旋轉角度,判斷當前需要展示 IndexedStack 裡的哪個卡片,因為 Flutter 使用的 Skia 是 2D 渲染引擎,如果沒有這部分邏輯,你就只會看到單張圖片畫面的旋轉效果。

js if (touchX.abs() % (pi * 3 / 2) >= pi / 2 || touchY.abs() % (pi * 3 / 2) >= pi / 2) { showIndex = 0; } else { showIndex = 1; }

執行效果如下圖所示,可以看到在視差和圖片切換的作用下,我們用很低的成本在 Flutter 上實現了 “偽3D” 的卡片的 360° 展示,類似的實現其實還可以用於一些商品展示或者頁面切換的場景,本質上就是利用視差的效果,在 2D 螢幕上模擬現實中的畫面效果,從而達到類似 3D 的視覺作用

| | | | ---------------------------------------------------- | ----------------------------------------- |

最後我們只需要用 Text 在卡片上新增“模擬”凹凸的文字,就實現了我們現實中類似銀行卡的卡面效果

完整程式碼可見: card_3d_demo_page.dart

Web 體驗地址,PC 端記得開 chrome 手機模式: 360° 視覺化 3D 電子銀行卡

好了,本篇動畫特效就到此為止,如果你有什麼想法,歡迎留言評論,感謝大家耐心看完,也還請看官們走過路過的來個點贊一鍵三連,感激不盡