如何利用 Flutter 實現炫酷的 3D 卡片和帥氣的 360° 展示效果
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),
),
),
),
);
接著,如下程式碼所示,因為我們需要卡片跟隨手勢進行矩陣變換,所以我們可以直接在 GestureDetector
的 onPanUpdate
裡獲取到手勢資訊,例如 localPosition
位置資訊,然後把對應的 dx
和 dy
賦值到 Matrix4
的 rotateX
和 rotateY
上實現旋轉。
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 裡快很多。
問題的原因其實是因為 rotateX
和 rotateY
需要的是一個 angle
引數,假設這裡對 rotateX
和 rotateY
設定 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
是對資料進行壓縮取樣,但是為什麼 touchX
和 touchY
的計算方式是相反的呢?
js
touchX = (cardWidth / 2 - details.localPosition.dx) / 100;
touchY = (details.localPosition.dy - cardHeight / 2 ) / 100;
如下圖所示,因為在設定 rotateX
和 rotateY
時,賦予 > 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 空間的感覺。
接著我們在卡片上放上一個新增一個 13
的 Text
文字,執行之後可以看到此時文字是跟隨卡片發生變化,而接下來我們需要做的,就是通過另外一個 Transform
來讓 Text
文字和卡片之間產生視差,從而出現懸浮的效果。
| |
|
| ----------------------------------------------------------- | ---------------------------------------- |
所以接下來需要給文字內容設定一個 translate
的 Matrix4
,讓它向著傾斜角度的相反方向移動,然後對前面的 touchX
和 touchY
進行放大,然後再通過 - 10
操作來產生一個位差。
js
Transform(
transform: Matrix4.identity()
..translate(touchX * 100 - 10,
touchY * 100 - 10, 0.0),
-10
這個是我隨意寫的,你也可以根據自己的需求調節。
例如,這時候當卡片往左傾斜時,文字就會向右移動,從而產生視覺差的效果,得到類似懸浮的感覺。
| |
|
| ----------------------------------------------------------- | ----------------------------------------- |
完成這一步之後,接下來可以我們對文字內容進行一下美化處理,例如增加漸變顏色,新增陰影,更換字型,目的是讓字型看起來更加具備立體的效果,這裡使用的 shader
,也可以讓文字在移動過程中出現不同角度的漸變效果。
| |
|
| ----------------------------------------------------------- | ----------------------------------------- |
最後,我們還需要對卡片旋轉進行一個範圍約束,這裡主要是通過卡片大小比例:
- 在
onPanUpdate
時對touchX
和touchY
進行範圍約束,從而約束的卡片的傾斜角度 - 增加了
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 電子銀行卡
好了,本篇動畫特效就到此為止,如果你有什麼想法,歡迎留言評論,感謝大家耐心看完,也還請看官們走過路過的來個點贊一鍵三連,感激不盡~
- 面向 ChatGPT 開發 ,我是如何被 AI 從 “逼瘋” 到 “覺悟” ,未來又如何落地
- 維護高 Star Github 專案,會遇到什麼有趣的問題 2023 版
- Flutter - Dart 3α 新特性 Record 和 Patterns 的提前預覽講解
- 2023 年第一彈, Flutter 3.7 釋出啦,快來看看有什麼新特性
- 2023 Flutter Forward 大會回顧,快來看看 Flutter 的未來會有什麼
- Flutter 的下一步, Dart 3 重大變更即將在 2023 到來
- Flutter 小技巧之快速理解手勢邏輯
- 一文快速帶你瞭解 KMM 、 Compose 和 Flutter 的現狀
- Flutter 工程化框架選擇 — 混合開發的摸爬滾打
- 如何利用 Flutter 實現炫酷的 3D 卡片和帥氣的 360° 展示效果
- Flutter 工程化框架選擇——搞定 Flutter 動畫
- Flutter 實現 “真” 3D 動畫效果,用純程式碼實現立體 Dash 和 3D 掘金 Logo
- Android Studio Dolphin | 2021.3.1 釋出,快來看看有什麼更新吧~
- 掘金 XDC 2022 - 普通技術人的彎道超車指南
- Flutter 工程化框架選擇 — 搞定 UI 生產力
- Dart 2.18 釋出,Objective-C 和 Swift interop
- Flutter 工程化框架選擇 — 搞定資料儲存選型
- 2022 年 App 上架稽核問題集錦,全面踩坑上線不迷路
- React Native 0.70 版本釋出,Hermes 終於成為預設 Engine
- Flutter 3.3 之 SelectionArea 好不好用?用 “Bug” 帶你全面瞭解它