引言
最近看到 自如團隊 釋出的 自如客APP裸眼3D效果的實現,這個佈局確實做得很有趣,越玩越上癮,感謝自如團隊的分享。隨即按照自己的思路用 Flutter 實現一遍,來看看最終效果。
banner 樣式 | 全屏樣式 |
---|---|
本文會著重介紹我在實現過程中的思路和設計,所以無論你是前端 /iOS/Android/Flutter 都可以參考同樣的路子去實現。如果有任何問題,也歡迎探討。
一、整體構思
從效果上可以看出,隨著我們裝置的旋轉,有的部分順著傾斜方向滑動,有的朝著相反方向,而有的則不動。所以圖片上的元素肯定分為不同的圖層,旋轉裝置讓圖層發生移動即可達到效果。
將圖片分為了前、中、後三層,隨著手機角度的旋轉,中層保持不動,上層順著旋轉方向移動,下層與上層相逆。
(圖片來自自如分享)
所以在圖片分層之後,這個效果就變成了兩步:
1、獲取手機的旋轉資訊
2、根據旋轉資訊移動不同的圖層
二、獲取手機的旋轉資訊
Flutter 中有這樣一個外掛 sensors_plus ,使用它可以幫助我們獲取兩個感測器的資訊:Accelerometer(加速度感測器)、Gyroscope(陀螺儀)。
每個感測器提供了一個 Stream ,其傳送的事件包含 X、Y、Z 表示手機不同方向的變化的速度。通過對 Stream 的監聽,我們便可實時獲取相關感測器資料。
這個倉庫中也附帶了一個體感貪吃蛇的 demo,傾斜裝置,小蛇便朝著傾斜方向前進。
外掛的更多介紹可以檢視視訊: Flutter Widgets 介紹合集 —— 103. Sensors_plus
我們實現的效果需要根據手機旋轉移動圖層,自然使用陀螺儀感測器即可:
gyroscopeEvents.listen(
(GyroscopeEvent event) {
// event.x event.y event.z
································
},
),
複製程式碼
回撥的 GyroscopeEvent 包含三個屬性,x、y、z,分別對應下圖三個方向所檢測到的旋轉速度(單位:弧度/秒)
結合需求來看,我們只需使用 Y 軸(對應影像在水平方向的移動)和 X 軸(對應影像在豎直方向的移動)的資料即可。
三、根據旋轉資訊移動圖層
在網上找了一個 psd 檔案,匯出圖片之後整體長這樣:
我在 psd 檔案中匯出 3 個圖層,需要注意圖片格式要為 .png,這樣上一個圖層的透明區域不會被填充為白色而遮擋住下一個圖層,之後直接使用 Image widget 展示圖片即可:
前景 | 中景(白色的文字,所以看不見) | 背景 |
---|---|---|
1、讓圖層動起來
圖片分為三層,我們自然想到使用 Stack
作為容器,依次放入三個圖層(Widget)
// 背景圖層
Widget? backgroundWidget;
// 中景圖層
Widget? middleWidget;
// 前景圖層
Widget? foregroundWidget;
複製程式碼
圖層移動其實很簡單,就是去修改每一個圖層的偏移量。再觀察這個實現效果,會發現隨著我們的旋轉,圖層中的內容好像 滑
出來一樣。
所以我們一開始進入時,看到的肯定只是圖片的部分割槽域。我的想法是給每一個圖層設定 scale
,將圖片進行放大。顯示視窗是固定的,那麼一開始只能看到圖片的正中位置。(中層可以不用,因為中層本身是不移動的,所以也不必放大)
旋轉手機修改偏移量,為前景和背景層設定相反的偏移量,便可達到兩個圖層反向運動的效果。
在計算偏移量的時候還需要考慮兩個因素:
1、圖層的最大偏移量
圖層經過了一定比例的放大,所以存在一個最大的偏移範圍,偏移量不能超過這個範圍。
不難看出水平方向上最大偏移計算方法為:(縮放比例-1) * 寬 / 2,
豎直方向同理。
2、前景與背景圖層的相對偏移速度
因為前景和背景的縮放比例可能不同,如果兩者以 1:1 的相對偏移,可能會出現以下情況。
假如 前景縮放是 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 更新介面,看看圖層部分實現的效果:
背景隨著手指滑動而位移,同時前景朝相反的方向移動,當滑動到圖層邊界時無法繼續,整個過程中層保持不動。
2、感測器控制偏移
圖層位移實現之後,我們只需要將上面由手指滑動觸發的偏移改變為由感測器觸發即可。
這裡我們來想一個問題,我們裝置處於水平狀態時,顯示區域居中,而當裝置傾斜的時候,顯示區域移動。
那麼該旋轉多少角度達到最大偏移量呢?
所以這裡我定義了兩個變數:
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進階與優化指南,歡迎關注。
往期精彩內容: