拿去吧你!Flutter 仿自如 App 裸眼 3D 效果| 8月更文挑戰

Nayuta發表於2021-08-01

引言

最近看到 自如團隊 釋出的 自如客APP裸眼3D效果的實現,這個佈局確實做得很有趣,越玩越上癮,感謝自如團隊的分享。隨即按照自己的思路用 Flutter 實現一遍,來看看最終效果。

banner 樣式全屏樣式
IMG_0020.gifIMG_0021.gif

本文會著重介紹我在實現過程中的思路和設計,所以無論你是前端 /iOS/Android/Flutter 都可以參考同樣的路子去實現。如果有任何問題,也歡迎探討。


一、整體構思

從效果上可以看出,隨著我們裝置的旋轉,有的部分順著傾斜方向滑動,有的朝著相反方向,而有的則不動。所以圖片上的元素肯定分為不同的圖層,旋轉裝置讓圖層發生移動即可達到效果。

將圖片分為了前、中、後三層,隨著手機角度的旋轉,中層保持不動,上層順著旋轉方向移動,下層與上層相逆。

(圖片來自自如分享)

所以在圖片分層之後,這個效果就變成了兩步:

1、獲取手機的旋轉資訊

2、根據旋轉資訊移動不同的圖層


二、獲取手機的旋轉資訊

Flutter 中有這樣一個外掛 sensors_plus ,使用它可以幫助我們獲取兩個感測器的資訊:Accelerometer(加速度感測器)、Gyroscope(陀螺儀)。

感測器.gif

每個感測器提供了一個 Stream ,其傳送的事件包含 X、Y、Z 表示手機不同方向的變化的速度。通過對 Stream 的監聽,我們便可實時獲取相關感測器資料。

這個倉庫中也附帶了一個體感貪吃蛇的 demo,傾斜裝置,小蛇便朝著傾斜方向前進。

貪吃蛇.gif

外掛的更多介紹可以檢視視訊: Flutter Widgets 介紹合集 —— 103. Sensors_plus

我們實現的效果需要根據手機旋轉移動圖層,自然使用陀螺儀感測器即可:

 gyroscopeEvents.listen(
       (GyroscopeEvent event) {
       // event.x  event.y  event.z
     ································
   },
 ),
複製程式碼

回撥的 GyroscopeEvent 包含三個屬性,x、y、z,分別對應下圖三個方向所檢測到的旋轉速度(單位:弧度/秒)

xyz.png

結合需求來看,我們只需使用 Y 軸(對應影像在水平方向的移動)和 X 軸(對應影像在豎直方向的移動)的資料即可。


三、根據旋轉資訊移動圖層

在網上找了一個 psd 檔案,匯出圖片之後整體長這樣:

封面.png

我在 psd 檔案中匯出 3 個圖層,需要注意圖片格式要為 .png,這樣上一個圖層的透明區域不會被填充為白色而遮擋住下一個圖層,之後直接使用 Image widget 展示圖片即可:

前景中景(白色的文字,所以看不見)背景
fore.pngmid.pngback.png

1、讓圖層動起來

圖片分為三層,我們自然想到使用 Stack 作為容器,依次放入三個圖層(Widget)

 // 背景圖層
 Widget? backgroundWidget;
 // 中景圖層
 Widget? middleWidget;
 // 前景圖層
 Widget? foregroundWidget;
複製程式碼

圖層移動其實很簡單,就是去修改每一個圖層的偏移量。再觀察這個實現效果,會發現隨著我們的旋轉,圖層中的內容好像 出來一樣。

所以我們一開始進入時,看到的肯定只是圖片的部分割槽域。我的想法是給每一個圖層設定 scale,將圖片進行放大。顯示視窗是固定的,那麼一開始只能看到圖片的正中位置。(中層可以不用,因為中層本身是不移動的,所以也不必放大)

image.png

旋轉手機修改偏移量,為前景和背景層設定相反的偏移量,便可達到兩個圖層反向運動的效果。

在計算偏移量的時候還需要考慮兩個因素:

1、圖層的最大偏移量

圖層經過了一定比例的放大,所以存在一個最大的偏移範圍,偏移量不能超過這個範圍。

image.png

不難看出水平方向上最大偏移計算方法為:(縮放比例-1) * 寬 / 2, 豎直方向同理。

2、前景與背景圖層的相對偏移速度

因為前景和背景的縮放比例可能不同,如果兩者以 1:1 的相對偏移,可能會出現以下情況。

image-20210729005136818.png 假如 前景縮放是 1.4,背景為 1.8,當顯示區域向左移動 2 畫素的時候。這時背景層所顯示的區域同樣向左移動 2 個畫素,前景層相反。但這時前景已經達最大的偏移量,不能再繼續移動。而背景其實還有區域未能顯示,所以可以通過兩者的縮放比計算對應的偏移比,保證兩個圖片都能完整的展示出來。

 // 通過背景偏移計算前景偏移
 Offset getForegroundOffset(Offset backgroundOffset) {
   // 假如前景縮放比是 1.4 背景是 1.8 控制元件寬度為 10
   // 那麼前景最大移動 4 畫素,背景最大 8 畫素
   double offsetRate = ((widget.foregroundScale ?? 1) - 1) /
       ((widget.backgroundScale ?? 1) - 1);
   // 前景取反
   return -Offset(
       backgroundOffset.dx * offsetRate, backgroundOffset.dy * offsetRate);
 }
複製程式碼

這裡我通過背景偏移為標準,計算前景偏移,並且在計算背景偏移的之前先考慮了最大偏移範圍,這樣保證前景和背景都不會發生越界行為。先通過拖拽改變偏移量呼叫 setState 更新介面,看看圖層部分實現的效果:

1627550866693665.gif

背景隨著手指滑動而位移,同時前景朝相反的方向移動,當滑動到圖層邊界時無法繼續,整個過程中層保持不動。

2、感測器控制偏移

圖層位移實現之後,我們只需要將上面由手指滑動觸發的偏移改變為由感測器觸發即可。

這裡我們來想一個問題,我們裝置處於水平狀態時,顯示區域居中,而當裝置傾斜的時候,顯示區域移動。

image.png

那麼該旋轉多少角度達到最大偏移量呢?

所以這裡我定義了兩個變數:

  double maxAngleX;
  double maxAngleY;
複製程式碼

分別表示水平和垂直方向的最大旋轉角度。假設 maxAngleX 為 10,表示當你在水平方向旋轉裝置 10° 度的時候,影像顯示到某一個方向的邊緣。

有了這個定義我們便可反推出背景層 旋轉 1° 的偏移量為:

1/maxAngleX * maxBackgroundOffset.dx,垂直方向同理。

思路就是這樣,不過我在實現的時候還遇到了一個棘手的問題:

由於 sensors_plus 外掛中提供的是各方向的旋轉速度(rad/s),我們改如何計算實際的旋轉角度?

其實並不難:旋轉弧度 = (旋轉速度(rad/s) * 時間),那麼這裡時間是多少?

看 sensors_plus 外掛的安卓端實現,這個外掛通過 SensorManager 註冊陀螺儀感測器的回撥,通過 chanel 將採集到的資料直接傳遞到 Flutter 側。

sensorManager.registerListener(sensorEventListener, sensor, SensorManager.SENSOR_DELAY_NORMAL);
複製程式碼

在安卓端 SensorManager 的採集靈敏度分幾種

  • SensorManager.SENSOR_DELAY_FASTEST(0微秒):最快。最低延遲,一般不是特別敏感的處理不推薦使用,該模式可能在成手機電力大量消耗,由於傳遞的為原始資料,演算法不處理好會影響遊戲邏輯和UI的效能
  • SensorManager.SENSOR_DELAY_GAME(20000微秒):遊戲。遊戲延遲,一般絕大多數的實時性較高的遊戲都是用該級別
  • SensorManager.SENSOR_DELAY_NORMAL(200000微秒):普通。標準延時,對於一般的益智類或EASY級別的遊戲可以使用,但過低的取樣率可能對一些賽車類遊戲有跳幀現象
  • SensorManager.SENSOR_DELAY_UI(60000微秒):使用者介面。一般對於螢幕方向自動旋轉使用,相對節省電能和邏輯處理,一般遊戲開發中不使用

不同靈敏度的採集時間不同,sensors_plus 預設是 SENSOR_DELAY_NORMAL 即 0.2S ,實際使用感應延遲非常高,不太適合這種需要及時響應的場景。所以我直接 fork 專案下來,將 SENSOR_DELAY_NORMAL 改為了 SENSOR_DELAY_GAME ,即每次採集時間為 20000微秒(0.02秒)。(如果你有類似需求可以通過 nayuta_sensors: 1.0.0 使用)

換算成角度就是:x * 0.02 * 180 / π,再用角度換算背景偏移量,背景偏移量考慮最大偏移範圍之後,計算前景,呼叫 setState 更新介面即可。關鍵步驟如下:

gyroscopeEvents.listen((event) {
  setState(() {
    // 通過採集的旋轉速度計算出背景 delta 偏移
    Offset deltaOffset = gyroscopeToOffset(-event.y, -event.x);
    // 初始偏移量 + delta 偏移 之後考慮越界
    backgroundOffset = considerBoundary(deltaOffset + backgroundOffset);
    // 背景偏移根據縮放比例獲取前景偏移
    foregroundOffset = getForegroundOffset(backgroundOffset);
  });
});
複製程式碼

四、使用說明

專案依賴

倉庫已上傳至 pub 通過依賴:

dependencies:
  flutter:
    flutter_interactional_widget: 1.0.0
複製程式碼

github 現已加入 ? 全家桶:github.com/fluttercand…

建構函式

InteractionalWidget

屬性說明是否必選
double width視窗寬度
double height視窗高度
double maxAngleX水平方向最大的旋轉角度
double maxAngleY豎直方向最大的旋轉角度
double? backgroundScale背景層縮放比
double? middleScale中景層縮放比
double? foregroundScale前景層的縮放比
Widget? backgroundWidget背景層 widget
Widget? middleWidget中景層 widget
Widget? foregroundWidget前景層 widget

三個圖層均非必傳,所以你也可以只指定 前景/背景 單一圖層的位移。你也可以參考 github 中的案例用法:

Widget banner() {
    return InteractionalWidget(
      width: MediaQuery.of(context).size.width,
      height: height,
      maxAngleY: 30,
      maxAngleX: 40,
      middleScale: 1,
      foregroundScale: 1.1,
      backgroundScale: 1.3,
      backgroundWidget: backgroundWidget(),
      middleWidget: middleWidget(),
      foregroundWidget: foregroundWidget(),
    );
  }
複製程式碼

github 中的 演示程式 (訪問連結下載)可以直接執行,包含一個 banner 樣式和一個全屏樣式的案例。後面這個倉庫還會更新一些互動式的小元件,這麼良心的博主給個點贊、關注、 star 不過分吧~


五、最後

本來是打算接著寫網路程式設計,中途看到 自如客APP裸眼3D效果的實現 於是趁著週末趕緊實現了一下,再次感謝 自如團隊 提供這麼妙的創意。下一期,還是按照之前的計劃,通過 廣播/組播的方式實現一個基礎的區域網多端群聊服務。

如果你有任何疑問可以通過公眾號與聯絡我,如果文章對你有所啟發,希望能得到你的點贊、關注和收藏,這是我持續寫作的最大動力。Thanks~

公眾號:進擊的Flutter或者 runflutter 裡面整理收集了最詳細的Flutter進階與優化指南,歡迎關注。

往期精彩內容:

Flutter 進階優化

Flutter核心渲染機制

Flutter路由設計與原始碼解析

相關文章