Compose Edition is here! Imitation of the naked eye 3D effect

This is the second day of my participation in the August Update Challenge. For details of the event, please check: August Update Challenge

Some time ago, the Ziroom  team realized the naked-eye 3D effect of the App , which is really eye-catching. Yesterday, Nayuta used Flutter to achieve this function, so how can the Jetpack compose version fall?

The predecessors planted the trees and the others enjoyed the shade. First of all, I would like to thank the front-end team  of Ziru and Nayuta  . Some of the materials used below are also from Nayuta  , thank you again.

ideas

From the ideas provided by the Ziroom team, the naked eye 3D effect is to divide the entire picture structure into three layers: the upper layer, the middle layer, and the bottom layer. When the mobile phone is rotated left and right up and down, the upper and lower layers of the picture move in opposite directions, while the middle layer does not move, giving a 3D effect visually.

As for using Jetpack Compose, the main idea is as follows:

  1. Use Compose's Canvas to draw three-layer images, and use translate to translate the upper and lower images;
  2. Register the monitoring of the gyroscope sensor of the mobile phone, and get the rotation angle of the xyz axis when the mobile phone is rotated;
  3. Calculate the translation distance of the picture according to the rotation angle, and control the maximum translation distance during the period;
  4. After getting the translation distance, set the distance to the translation distance variable marked with mutableStateOf, so that the UI is refreshed with a translation effect.

accomplish

According to the above ideas, we first use compose to draw three static pictures. There are many ways to draw pictures with compose, such as Image, Canvas, etc. Because considering that the following pictures need to be moved, Canvas is used here for drawing.


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)
    
}
复制代码

The resulting static rendering is as follows:

image.png

Static image loading is a simple matter, so how to make the image move?

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)) {}
复制代码

最后的效果也就是如下所示: image.png

手机陀螺仪传感器

通过手机的旋转,图片进行移动的操作归功于传感器,

3c265f69fa9fe35400d8a2f4d2bf325.jpg

如图所示,传感器坐标系一共分为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}
复制代码

好了,功能完成,我们来看看最后的效果:

d32e6727beecdf9c00cee0902f6798b1 (1).gif

最后

市面上的App的设计基本上是千篇一律,一个有意思的idea总是会让人多看一眼,再次感谢自如团队提供了这个创意。对了,今天在蚂蚁森林收能量时,发现树木也有点此效果的味道,你不妨去瞅一眼。

参考资料:

自如客APP裸眼3D效果的实现

Take it to you! Flutter imitates Ziru App with naked-eye 3D effect|August update challenge

Recommended reading:

Play around with Compose, Genshin Impact themed list

Guess you like

Origin juejin.im/post/6992169168938205191