美文网首页
Andoid 仿自如裸眼 3D 效果

Andoid 仿自如裸眼 3D 效果

作者: ziwenl | 来源:发表于2021-09-30 11:28 被阅读0次

    前言

      前段时间自如技术团队发布了一篇名为《自如客APP裸眼3D效果的实现》的技术分享文章,简述了通过将图层分为前中后景,监听手机倾斜角度,再根据倾斜角度反向移动前后景,实现类似裸眼 3D 的效果。 该文章中已将思路与原理讲述清楚,抱着好奇心尝试仿现了一下。

    1.自如的思路分析探究

    1.1 自如 APP 上的裸眼 3D 效果
       UI 层面上:将普通的 2D 图像切割出 后景中景前景 三个部分 1.2 普通 2D 图像
    1.3 切割出来的 后景、 中景 及 前景
       技术层面上:通过 Android 中的 磁场传感器加速度传感器 监听设备的倾斜角度,保持 中景 不动,根据倾斜角度反向移动 背景前景 ,将 2D 图像转化为景深效果,呈现出类似裸眼 3D 的视觉效果。
    [图片上传失败...(image-b54255-1632972462366)]

    思路上就是这么清晰和简单,现需求如下:
      根据设备倾斜角度 平稳移动 前后景,实现裸眼 3D 效果
      其中前后景在 Y 轴上的移动范围和速度均比 X 轴小和慢

    2.具体实现

    2.1 实现效果

    2.1.1 仿现效果

    2.2 具体实现

    2.2.1 自定义 GravityRotationImageView :

       1.继承于 ImageView ,内部实现 Scroller
       2.提供自定义属性 isBack 区分该 View 用作前景还是后景,前后景移动方向不同,且后景 ImageView 的填充应存在一定的放大倍数

        /**
         * 设置当前 view 为前景或后景
         * @param isBack true 后景 ; false 前景
         */
        fun isBack(isBack: Boolean) {
            /**
             * 判断该 view 用作前景还是后景
             * 后景则需调整放大倍数使内容滚动时不会出现白边
             * 并根据前后景记录对应的滚动方向
             */
            if (isBack) {
                mDirection = DIRECTION_BACK
                scaleType = ScaleType.CENTER_CROP
                scaleX = 1.1f
                scaleY = 1.2f
            } else {
                mDirection = DIRECTION_FRONT
            }
        }
    

       3.提供 handleSensorChangedValues 方法,该方法中根据得到的传感器数据计算倾斜角度,过滤抖动(角度变化过小/过大),并得到需要移动的距离,最后通过 Scroller 辅助移动

        /**
         * 处理传感器得到的数据,过滤后再根据倾斜角度移动当前 view
         * 旋转移动过程中,前景后景随旋转角度偏移
         */
        internal fun handleSensorChangedValues(
            gravity: FloatArray,
            geomagnetic: FloatArray,
            maxMovingRange: Float = MOVING_RANGE_DEFAULT
        ) {
            if (maxMovingRange != MOVING_RANGE_DEFAULT) {
                mMaxMovingRange = dip2px(this.context, maxMovingRange)
            }
            //旋转角度值集
            val orientationValues = FloatArray(3)
            //旋转矩阵
            val rotationMatrix = FloatArray(9)
            SensorManager.getRotationMatrix(
                rotationMatrix,
                null,
                gravity,
                geomagnetic
            )
            SensorManager.getOrientation(rotationMatrix, orientationValues)
            // z 轴的偏转角度
            orientationValues[0] = Math.toDegrees(orientationValues[0].toDouble()).toFloat()
            // x 轴的偏转角度
            orientationValues[1] = Math.toDegrees(orientationValues[1].toDouble()).toFloat()
            // y 轴的偏转角度
            orientationValues[2] = Math.toDegrees(orientationValues[2].toDouble()).toFloat()
            val newAngleX = orientationValues[1].toInt()
            val newAngleY = orientationValues[2].toInt()
            // x 、 y 轴角度变化值
            val rotationAngleXChangeValue = abs(newAngleX - rotationAngleX)
            val rotationAngleYChangeValue = abs(newAngleY - rotationAngleY)
            var targetX = mScroller.finalX
            var targetY = mScroller.finalY
            if (rotationAngleYChangeValue in (RESPONSE_ANGLE_CHANGE_MIN + 1) until RESPONSE_ANGLE_CHANGE_MAX
                || rotationAngleXChangeValue in (RESPONSE_ANGLE_CHANGE_MIN + 1) until RESPONSE_ANGLE_CHANGE_MAX
            ) {
                if (newAngleX <= 0 && newAngleX > -MAX_ROTATION_ANGLE || newAngleX in 1 until MAX_ROTATION_ANGLE) {
                    targetY = mMaxMovingRange * -mDirection * newAngleX / MAX_ROTATION_ANGLE_Y
                }
                if (newAngleY <= 0 && newAngleY > -MAX_ROTATION_ANGLE || newAngleY in 1 until MAX_ROTATION_ANGLE) {
                    targetX = mMaxMovingRange * mDirection * newAngleY / MAX_ROTATION_ANGLE
                }
                val dx = targetX - scrollX
                val dy = targetY - scrollY
                smoothScroll(dx, dy)
                //更新角度
                rotationAngleX = newAngleX
                rotationAngleY = newAngleY
            }
        }
    

    2.2.2 自定义帮助类 GravityRotationHelper:

       1.构造方法中得到已实现 LifecycleOwner 的 context 对象,通过 Lifecycle 特性在 context 对象的相应生命周期中进行 加速度传感器磁场传感器 的注册与反注册

        init {
            if (context is LifecycleOwner) {
                //获取传感器管理类实例
                mSensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
                //加速度传感器实例
                val accelerationSensor = mSensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
                //磁场传感器
                val magneticSensor = mSensorManager?.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
                context.lifecycle.addObserver(object : LifecycleObserver {
                    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
                    fun onResume(@NotNull owner: LifecycleOwner) {
                        //注册监听
                        mSensorManager?.registerListener(
                            mSensorEventListener,
                            accelerationSensor,
                            SensorManager.SENSOR_DELAY_GAME
                        )
                        mSensorManager?.registerListener(
                            mSensorEventListener,
                            magneticSensor,
                            SensorManager.SENSOR_DELAY_GAME
                        )
                    }
     
                    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
                    fun onPause(@NotNull owner: LifecycleOwner) {
                        mSensorManager?.unregisterListener(mSensorEventListener)
                    }
                })
            } else {
                Log.e(
                    "GravityRotationHelper",
                    "GravityRotationHelper init error : context is LifecycleOwner = false "
                )
            }
        }
    

       2.提供 attachViews 方法,得到外部需要实现裸眼 3D 效果的前景与后景 View ,旧持有前景后景 View 不为空时,记录并重置对应 scroll 值

        /**
         * 添加需要实现裸眼 3D 效果的视图组
         * 旋转移动过程中,前景后景随旋转角度偏移
         * @param frontView 前景
         * @param backView 后景
         * @param maxMovingRange 最大可移动范围 dp
         */
        fun attachViews(
            frontView: GravityRotationImageView,
            backView: GravityRotationImageView,
            maxMovingRange: Float = MOVING_RANGE_DEFAULT
        ) {
            //旧持有前景后景 View 不为空时,记录并重置对应 scroll 值
            val oldFrontViewScrollX = mFrontView?.scrollX ?: 0
            val oldFrontViewScrollY = mFrontView?.scrollY ?: 0
            val oldBackViewScrollX = mBackView?.scrollX ?: 0
            val oldBackViewScrollY = mBackView?.scrollY ?: 0
            val oldRotationAngleX = mFrontView?.rotationAngleX ?: 0
            val oldRotationAngleY = mFrontView?.rotationAngleY ?: 0
            mFrontView = frontView
            mBackView = backView
            mFrontView?.rotationAngleX = oldRotationAngleX
            mFrontView?.rotationAngleY = oldRotationAngleY
            mBackView?.rotationAngleX = oldRotationAngleX
            mBackView?.rotationAngleY = oldRotationAngleY
            //继承上一组前景后景 View 的 scroll 值
            mFrontView?.scrollTo(oldFrontViewScrollX, oldFrontViewScrollY)
            mBackView?.scrollTo(oldBackViewScrollX, oldBackViewScrollY)
            mMaxMovingRange = maxMovingRange
        }
    

       3.传感器数值变化时调用前后景 View 的 handleSensorChangedValues 方法进行移动

        private var mSensorEventListener = object : SensorEventListener {
            override fun onSensorChanged(event: SensorEvent) {
                when (event.sensor.type) {
                    Sensor.TYPE_ACCELEROMETER -> {
                        //加速度
                        mAccelerationValues = event.values
                        handleAccelerometerAndMagneticData()
                    }
                    Sensor.TYPE_MAGNETIC_FIELD -> {
                        //磁场
                        mMagneticValues = event.values
                        handleAccelerometerAndMagneticData()
                    }
                }
     
            }
     
            override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
     
            }
        }
     
        private fun handleAccelerometerAndMagneticData() {
            if (mAccelerationValues != null && mMagneticValues != null) {
                if (mFrontView != null && mBackView !== null) {
                    mFrontView?.handleSensorChangedValues(
                        mAccelerationValues!!,
                        mMagneticValues!!,
                        mMaxMovingRange
                    )
                    mBackView?.handleSensorChangedValues(
                        mAccelerationValues!!,
                        mMagneticValues!!,
                        mMaxMovingRange
                    )
                }
            }
        }
    

    2.3 使用步骤

       1.复制 Demo 中的 GravityRotationHelperGravityRotationImageView 以及自定义属性 attrs 到项目中
       2.布局中使用 GravityRotationImageView 作为需要实现 3D 效果的前景与后景 View

    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipChildren="false">
     
        <com.ziwenl.library.GravityRotationImageView
            android:id="@+id/iv_back"
            android:layout_width="match_parent"
            android:layout_height="250dp"
            android:paddingBottom="40dp"
            android:src="@mipmap/banner_a_back"
            app:isBack="true"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
        
        <FrameLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="20dp"
            android:clipChildren="false"
            app:layout_constraintBottom_toBottomOf="@+id/iv_back"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent">
     
            <ImageView
                android:id="@+id/iv_middle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@mipmap/banner_a_middle" />
     
     
            <com.ziwenl.library.GravityRotationImageView
                android:id="@+id/iv_front"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@mipmap/banner_a_front" />
        </FrameLayout>
     
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    ( ps : 可按需给父 View 设置 android:clipChildren="false" 属性,控制前景移动到边界时是否裁剪 )
       3.使用帮助类 GravityRotationHelper 绑定前景和后景 View 实现目标效果

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val viewBinding = ActivitySinglepageBinding.inflate(layoutInflater)
            setContentView(viewBinding.root)
     
            GravityRotationHelper(this).attachViews(viewBinding.ivFront, viewBinding.ivBack)
        }
    

    ( ps:关于在 banner 中实现该效果,可参考 demo 中的 BannerActivity 类 )

    3.补充说明

    • 提取成帮助类而不是在自定义 View 中进行传感器的创建与注册监听,主要是为了减少耦合及资源开销
    • 自定义 ImageView 是为了使用 Scroller 来进行辅助滚动,如果只是在 View 外部通过监听设备倾斜角再通过 View 的 scroll 方法进行移动,会出现抖动及跳动问题
    • 除了使用 磁场传感器加速度传感器 来感知设备倾斜角度变化,还能使用 陀螺仪传感器 来感知设备的倾斜角度变化,同样能实现目标效果
        private val NS2S = 1.0f / 1000000000.0f
        private var timestamp = 0f
     
        private fun init(context: Context){
            val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
            val gyroscopeSensor = sensorManager?.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
            sensorManager?.registerListener(object : SensorEventListener {
                override fun onSensorChanged(event: SensorEvent) {
                    if (event.sensor.type == Sensor.TYPE_GYROSCOPE) {
                        if (timestamp != 0f) {
                            val dT = (event.timestamp - timestamp) * NS2S
                            angle[0] += event.values[0] * dT
                            angle[1] += event.values[1] * dT
                            val angleY = Math.toDegrees(angle[0].toDouble()).toFloat()
                            val angleX = Math.toDegrees(angle[1].toDouble()).toFloat()
                            //TODO
     
                        }
                        timestamp = event.timestamp.toFloat()
                    }
                }
     
                override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
     
                }
            }, gyroscopeSensor, SENSOR_DELAY_GAME)
        }
    

    4.最后

      关于该伪裸眼 3D 效果,自自如团队发布技术文章之后,网上也有一系列 Demo 及技术文章,本人在实现过程中遇到了抖动和跳动问题(主要由于传感器数值变化过于敏感及频繁导致),曾去下载一些 Demo 进行参考,发现同样是存在该问题。其中有篇文章是通过 陀螺仪传感器 来实现该效果的,也做了抖动过滤,但在小米 6 上运行时发现会出现卡顿效果,所以最后还是自己调整优化避免了该现象的出现。
      最后感谢 自如大前端团队 的实现方案分享,通过新颖取巧的方式,加强了用户的 UI 体验。而自如的技术文章更着重于分享思路,所以在此基础上进行实现与优化,也是一种不可多得的乐趣。

    相关文章

      网友评论

          本文标题:Andoid 仿自如裸眼 3D 效果

          本文链接:https://www.haomeiwen.com/subject/vihenltx.html