[Android]Jetpack Compose 实现仿裸眼3D
前言
最近在学传感器的时候,看到了有大佬使用 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的功能。
结果展示
这里直接展示

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