前言

  最近在学传感器的时候,看到了有大佬使用 Jetpack Compose 实现了一个仿裸眼3D的效果,十分的牛皮,看得我是心血来潮、热血澎湃,这里放上推荐链接:Compose版来啦!仿自如裸眼3D效果 - 掘金 (juejin.cn)。于是就也想拥有,所以按照大佬的思路,使用Android 传感器中的陀螺仪来进行一个实现。同时也可以去了解一下 Android 中传感器(Sensor)的使用,这里送上官方传感器文档:传感器 | Android 开发者 | Android Developers (google.cn)

实现原理

  根据上方大佬的思路,我们可以很好地理解的这个仿裸眼3D的原理,简单来说,就是将一张图进行抠图,将其分为前、中、后三个图层(也可以是自己找的三个元素),然后使用 Canvas 里面的 drawImage 函数将三张图画出(使用这个方法加载图片方便进行位移)。

  接着对前、后图层使用 translate 函数,通过设置位移量来实现图层的移动,所以只需要通过改变位移量就可以对图片进行位移。这里要注意,要实现仿裸眼3D,就需要在前层图片往一个方向移动时,后层图片向另一个方向移动,中层图片则不动,所以对于前层和后层的 translate 函数的传值要对应取反。

  于是现在就需要实现在翻转手机时,为 translate 函数提供相应的位移量。就使用到了陀螺仪传感器,在转动手机时,传感器会传回 x、y、z 三个方向(判断方向如下图所示)的角速度,往正方向翻转会传回一个正的角速度,往负方向翻转会传回一个负的角速度。

  得到了这个方向上的角速度,就可以由一个基本的物理公式 $\omega = \frac{{\Delta }\Theta }{{\Delta}t} $ 求出所转过的角度(rad)是多少(若不熟悉弧度制,可使用 Math 库中的 Degress 函数将弧度制转化为角度制),然后和大佬思路一致,设置最大角度和最大平移距离(可以根据自己的),通过

求出该旋转角度下图片的平移距离。

  为了获取角度值,就需要得到时间,不用担心,在官方文档介绍陀螺仪的部分中,其示例中就有对时间的积累,即获取时间。如下

val NS2S = 1.0f / 1000000000.0f
var timestamp: Float = 0f
sensorManager.registerListener(object : SensorEventListener{
        override fun onSensorChanged(event: SensorEvent?) {
            // This timestep's delta rotation to be multiplied by the current rotation
            // after computing it from the gyro sample data.
            if (timestamp != 0f && event != null) {
                val dT = (event.timestamp - timestamp) * NS2S
                /*
                *中间省略大部分
                */
            }
            timestamp = event?.timestamp?.toFloat() ?: 0f

这样就可以开始实操了。

代码实现

三层图片绘制

  按照上面的思路,首先需要有三层的图片,然后把他们绘制在一起,然后对前层与后层的图片加上 translate 函数用于平移。

private const val NS2S = 1.0f / 1000000000.0f
private var timestamp : Float = 0f
private const val maxAngle : Float = 60f          //设置最大角度,我使用的是角度制
private const val maxOffset : Float = 100f        //设置最大平移距离  

@Composable
fun ClassFree3D(
    sensorManager: SensorManager
){
    val imageBack = ImageBitmap.imageResource(id = R.drawable.imgback)
    val imageMid = ImageBitmap.imageResource(id = R.drawable.imgmid)
    val imageFore = ImageBitmap.imageResource(id = R.drawable.imgfore)
    
    var xDistance by remember { mutableFloatStateOf(0f) }
    var yDistance by remember { mutableFloatStateOf(0f) }

    Box(modifier = Modifier){
        Canvas(
            modifier = Modifier
                .fillMaxSize()
            //通过设置scale设置图片边界,防止在平移过程中图片平移过大导致露出屏幕背景
                .scale(1.3f)
        ){
            translate(-yDistance,-xDistance) {     //对于前层取反,实现反向移动
                drawImage(imageBack)
            }
            drawImage(
                image = imageMid,
                //这里通过 drawImage 同时对图片进行一个定位,使三张图片组合得好看
                dstOffset = IntOffset(x = 150,y = 350),
                dstSize = IntSize(width = imageMid.width - 1000, height = imageMid.height - 300)
            )
            translate(yDistance,xDistance) {
                drawImage(
                    image = imageFore,
                    //与上面同理
                    dstOffset = IntOffset(x = 0,y = 480),
                    dstSize = IntSize(width = imageFore.width - 200, height = imageFore.height)
                )
            }
        }

    }
}

平面绘制出来的图片如下所示(本人直男审美,可根据个人喜爱自由发挥。)

陀螺仪获取角速度

  Android为我们提供了一个 SensorManager 类来管理所有的传感器。故可以在主活动中获取一个 sensorManager 将其传给我们的这个组件,再在这个组件中获取陀螺仪传感器,并对其进行注册监听事件,获取翻转时的角速度和时间。

  在主活动中获取 sensorManager:

val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager

  将其传入组件:

ClassFree3D(sensorManager = sensorManager)

  然后再组件中获取陀螺仪传感器并注册监听器,获取角速度的累积,乘以时间,获取x、y、z三个方向的角度。

val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
var angularX by remember { mutableFloatStateOf(0f) }
var angularY by remember { mutableFloatStateOf(0f) }
var angularZ by remember { mutableFloatStateOf(0f) }
sensorManager.registerListener(object : SensorEventListener{
        override fun onSensorChanged(event: SensorEvent?) {

            if (timestamp != 0f && event != null){
                val dT = (event.timestamp - timestamp) * NS2S

                angularX += event.values[0] * dT
                angularY += event.values[1] * dT
                angularZ += event.values[2] * dT

				//这里将弧度制转化为角度制
                var angleX : Float = Math.toDegrees(angularX.toDouble()).toFloat()
                var angleY : Float = Math.toDegrees(angularY.toDouble()).toFloat()
                var angleZ : Float = Math.toDegrees(angularZ.toDouble()).toFloat()

            }
            timestamp = event?.timestamp?.toFloat() ?: 0f
        }

        override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {

        }

    },sensor,SensorManager.SENSOR_STATUS_ACCURACY_LOW)

进行运算

  然后由上面的公式进行运算,通过角度获得平移距离,同时也设置了最大的角度,这样就有最大的平移距离,这样可以防止平移距离过大而导致屏幕背景暴露。

if (angleY > maxAngle) {
        angleY = maxAngle
    } else if (angleY < -maxAngle) {
        angleY = -maxAngle
    }

if (angleX > maxAngle) {
        angleX = maxAngle
    } else if (angleX < -maxAngle) {
        angleX = -maxAngle
    }

    val xRadio : Float = (angleX / maxAngle)
    val yRadio : Float = (angleY / maxAngle)

    xDistance = xRadio * maxOffset
    yDistance = yRadio * maxOffset

  这样就在手机每次翻转的时候都会获取新的角度,从而改变平移距离,这样图片就可以随时发生平移,实现了仿裸眼3D的功能。

结果展示

  这里直接展示

总结

  说实话,实现之后自己都觉得很好看,同时也知道了如何去操作陀螺仪传感器,这也可以去实现许多交互上的创意。