作者:付十一
連結:https://juejin.cn/post/6992169168938205191
前段時間自如團隊發了自如客APP裸眼3D效果的文章讓人眼前一亮,還沒看過的,可以看看之前發的文章:Android自如客APP裸眼3D效果的實現
之後 Nayuta 大佬使用 Flutter 也實現了該功能,那 Jetpack compose 版本怎麼能落下。
前人栽樹後人乘涼,首先在這裡感謝 自如大前端團隊 和 Nayuta ,下文所用的素材也有一部分來自 Nayuta 的項目。
從自如團隊所提供的思路來看,裸眼3D效果是將整個圖片結構分為3層:上層、中層、以及底層。在手機左右上下旋轉時,上層和底層的圖片呈相反的方向進行移動,中層則不動,在視覺上給人一種3D的效果。
至於使用 Jetpack Compose 來實現,主要想法如下:
使用 Compose 的 Canvas 對三層圖片進行繪製,且使用 translate 對上層和底層圖片進行平移;註冊手機陀螺儀傳感器的監聽,拿到手機旋轉時,xyz 軸的旋轉角度;根據旋轉角度計算圖片平移的距離,期間做好最大平移距離的控制;得到平移距離後,將距離設置給標記了 mutableStateOf 的平移距離變量,使得UI刷新,呈平移效果。實現根據上面的思路,我們首先使用 compose 繪製出靜態的三張圖片,compose 繪製圖片的方式有多種,Image、Canvas 等,因為考慮到後面圖片需要進行移動,這裡就選用 Canvas 進行繪製。
val imageBack = ImageBitmap.imageResource(id = R.drawable.back)
val imageMid = ImageBitmap.imageResource(id = R.drawable.mid)
val imageFore = ImageBitmap.imageResource(id = R.drawable.fore)
Canvas(
modifier = Modifier
.fillMaxSize()) {
//底層
drawImage(imageBack)
//中層
drawImage(imageMid)
//s
drawImage(imageFore)
}生成靜態的效果圖如下:
靜態圖片加載是件簡單的事情,那如何讓圖片動起來?
Compose 的 Canvas 中有一個 translate 方法,作平移效果用,也就是分別在 x 和 y 坐標中通過給定的像素增量對坐標空間進行平移。參數傳入 x 軸上平移的距離以及 y 軸上平移的距離。
這裡分別定義為 xDistance , yDistance 。因為只有上層和底層的圖片會進行移動,所以在 Canvas 中,對上層和底層圖片的繪製加上 translate。如下:
translate(-xDistance, -yDistance) { drawImage(imageBack) }
drawImage(imageMid)
translate(xDistance, yDistance) { drawImage(imageFore) }傳入 xDistance,yDistance 參數值,這裡需要注意的是,上層與底層圖片為互為相反移動,所以對上層圖片傳入的是 xDistance 的相反值。到這裡,圖片就會根據 xDistance 以及 yDistance 的距離進行平移。
那 xDistance 和 yDistance 的值該如何動態改變呢?
Compose 其實提供了一個狀態 mutableStateOf,
標記了 mutableStateOf 的 data 後,該 data 就表明是有狀態的,如果後續狀態發生了改變,那麼所有引用這個狀態的控制項都會重新繪製。也就是說,將 xDistance 和 yDistance 設置成該狀態,因為 Canvas 引用了 xDistance 值,所有當 xDistance 值發生改變時,圖片也就會重新繪製,也就是做平移的效果。
var xDistance by remember { mutableStateOf(0f) }
var yDistance by remember { mutableStateOf(0f) }xDistance 和 yDistance 已經動態標記。下面就需要依據手機陀螺儀移動,來動態設置 xDistance 和 yDistance 的值。在開始說傳感器之前,這裡還存在一個問題,當圖片進行平移上下或者作用平移時,會存在左右或者上下兩側屏幕露出的情況,這個時候就需要將圖片做放大處理,
給圖片設置邊界,讓圖片在最大平移距離中移動,防止圖片平移露出屏幕背景,將 Canvas 設置為原來的 1.3 倍。
Canvas(
modifier = Modifier
.fillMaxSize()
.scale(1.3f)) {}最後的效果也就是如下所示:
手機陀螺儀傳感器通過手機的旋轉,圖片進行移動的操作歸功於傳感器,
如圖所示,傳感器坐標系一共分為 x, y, z 三軸,當手機左右翻轉時,則是圍繞 Y 軸運動,當手機上下翻轉時,則是圍繞 x 軸運動,當手機平放在桌面,左右畫圓時,則是圍繞 z 軸運動。
當手機旋轉時,傳感器則會通知我們三個方向的移動角速度,也就根據這移動角度來確定圖片的平移距離。
首先我們先看看傳感器該如何監聽?Android 其實已經為我們封裝好了 API,SensorManager,直接按照說明創建就好。
val context = LocalContext.current
val sensorManager: SensorManager? = getSystemService(context, SensorManager::class.java)
val sensor = sensorManager?.getDefaultSensor(Sensor.TYPE_GYROSCOPE)通過 getSystemService 獲取到 SensorManager 後,設置 sensor 的種類為 TYPE_GYROSCOPE,也就是陀螺儀傳感器。並且監聽 xyz 三個方向旋轉角速度。
sensorManager?.registerListener(object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent?) {
//Y軸角速度
speedY = event?.values?.get(1)!!
//X軸角速度
speedX = event?.values?.get(0)!!
//Z軸角速度
speedZ = event?.values?.get(2)!!
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
}
}通過 SensorEventListener 監聽到手機三個方向的角速度,因為陀螺儀讀出的是角速度,大家都知道,角速度乘以時間,就是轉過的角度,直接計算旋轉的角度值。
// 將手機在各個軸上的旋轉角度相加
angularX += (event.values[0] * dT).toLong()
angularY += (event.values[1] * dT).toLong()
angularZ += (event.values[2] * dT).toLong()
//設置x軸y軸最大邊界值,
if (angularY > mMaxAnular) {
angularY = mMaxAnular.toFloat()
} else if (angularY < -mMaxAnular) {
angularY = -mMaxAnular.toFloat()
}
if (angularX > mMaxAnular) {
angularX = mMaxAnular.toFloat()
} else if (angularX < -mMaxAnular) {
angularX = -mMaxAnular.toFloat()
}角度計算完成後,因為圖片移動是需要移動距離的,那接下來就需要知道圖片的平移距離。其實在上面就提出為圖片設置了最大平移邊界,這裡也設置了最大旋轉角度,那麼就可以依據角度比例來到推出平移距離。
依據公式 旋轉角度/最大角度 = 平移距離/最大平移距離 反推出 平移距離= 旋轉角度/最大角度*最大平移距離
val xRadio: Float = (angularY / mMaxAnular).toFloat()
val yRadio: Float = (angularX / mMaxAnular).toFloat()
xDistance = xRadio * maxOffset
yDistance = yRadio * maxOffset圖片距離計算完成,基本上隨手機移動,圖片會呈平移效果,但是發現還有一個問題,onSensorChanged 的回調刷新很快,當圍繞 Y 軸左右運動時,圖片也會上下平移,這就導致圖片會不規則跳動,繞 Y 軸左右運動其實只需要左右平移即可,同樣的,圍繞 x 軸運動,圖片只需要上下移動即可。這裡針對 x,y 軸運動,設置了旋轉條件控制。
x = Math.abs(event.values[0])
y = Math.abs(event.values[1])
z = Math.abs(event.values[2])
if (x > y + z) {
xDistance = 0f
yDistance = yRadio * maxOffset
} else if (y > x + z) {
xDistance = xRadio * maxOffset
yDistance = 0f}好了,功能完成,我們來看看最後的效果:
最後市面上的App的設計基本上是千篇一律,一個有意思的 idea 總是會讓人多看一眼,再次感謝自如團隊提供了這個創意。對了,今天在螞蟻森林收能量時,發現樹木也有點此效果的味道,你不妨去瞅一眼。