Android 仿自如APP裸眼3D效果

最近听同事说自如banner的裸眼3D效果很有创意,下载APP体验了一番觉得效果确实非常不错,所以立马就仿了一下。代码已上传至github仓库中,AndroidUiDemo

地址:https://github.com/SHPDZY/AndroidUiDemo

下面是demo和自如app的效果对比。

忽略搜索栏哈哈,我是直接截屏自如主页用ps扣来的图。

1628563109014.gif

1628563131510.gif

自定义ZiRuLayout

通过自定义view实现自如banner的裸眼3D效果,需要效果的view外层套上ZiRuLayout即可,demo中主要做测试,具体实现需要根据业务定制。本次是直接在自定义view中注册传感器,如需更好的体验的话,建议自定义个SersenManager类来注册监听,数据通过livedata对外暴露。下面来实现裸眼3D效果的具体代码。

注册传感器监听

代码中分别使用了陀螺仪和加速度传感器来实现裸眼3D效果的效果。主要是通过传感器相应的值,计算view的移动位置,并通过Scroller进行滑动

private fun initView(context: Context) {
    mScroller = Scroller(context)
    //SensorManager实例
    mSensorManager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
    mSensorGyroscope = mSensorManager?.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
    mSensorAccelerometer = mSensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
    //设置传感器监听,灵敏度设置为game就足够
    if (useGyroscope) {
        mSensorManager?.registerListener(this, mSensorGyroscope, SENSOR_DELAY_GAME)
    } else {
        mSensorManager?.registerListener(this, mSensorAccelerometer, SENSOR_DELAY_GAME)
    }
    if (context is FragmentActivity) {
        addZiRuLifecycleObserver(context)
    }
}

重写监听的onSensorChanged方法

数据计算和view的滑动都在此方法中执行

    override fun onSensorChanged(sensorEvent: SensorEvent?) {
        when (sensorEvent?.sensor?.type) {
            Sensor.TYPE_GYROSCOPE -> {
                if (timestamp != 0f) {
                    val dT = (sensorEvent.timestamp - timestamp) * NS2S
                    angle[0] += sensorEvent.values[0] * dT
                    angle[1] += sensorEvent.values[1] * dT
                    val angleY = Math.toDegrees(angle[0].toDouble()).toFloat()
                    val angleX = Math.toDegrees(angle[1].toDouble()).toFloat()
                    if (totalY == 0f) {
                        totalY = angleY; return
                    }
                    if (totalX == 0f) {
                        totalX = angleX; return
                    }
                    var scrollX = 0f
                    var scrollY = 0f
                    val dx = totalX - angleX
                    val dy = totalY - angleY
                    if (abs(dx) >= 0.1) scrollX = handleX(dx) * mDirection * 1.5f
                    if (abs(dy) >= 0.1) scrollY = handleY(dy) * mDirection * 1f
                    if (scrollX != 0f) totalX = angleX
                    if (scrollY != 0f) totalY = angleY
                    if (scrollX != 0f || scrollY != 0f)
                        smoothScrollBy(scrollX.toInt(), scrollY.toInt())
                }
                timestamp = sensorEvent.timestamp.toFloat()
            }
            Sensor.TYPE_ACCELEROMETER -> {
                val angleY = sensorEvent.values[1]
                val angleX = sensorEvent.values[0]
                if (totalY == 0f) {
                    totalY = angleY; return
                }
                if (totalX == 0f) {
                    totalX = angleX; return
                }
                var scrollX = 0f
                var scrollY = 0f
                val dx: Float = totalX - angleX
                val dy: Float = totalY - angleY
                if (abs(dx) > 0.2 && abs(dx) < 2) scrollX = handleX(dx) * mDirection * 5
                if (abs(dy) > 0.2 && abs(dy) < 2) scrollY = handleY(dy) * mDirection * 2f
                if (scrollX != 0f) totalX = angleX
                if (scrollY != 0f) totalY = angleY
                if (scrollX != 0f || scrollY != 0f)
                    smoothScrollBy(-scrollX.toInt(), -scrollY.toInt())
            }
        }
    }

传感器发生变化后通过Scroller滑动view

    fun smoothScrollTo(fx: Int, fy: Int) {
        val dx = fx - mScroller.finalX
        val dy = fy - mScroller.finalY
        smoothScrollBy(dx, dy)
    }

    fun smoothScrollBy(dx: Int, dy: Int) {
        // 参数一:startX 参数二:startY为开始滚动的位置,dx,dy为滚动的偏移量
        mScroller.startScroll(mScroller.finalX, mScroller.finalY, dx, dy, 200)
        invalidate()
    }

    override fun computeScroll() {
        // 判断滚动是否完成 true就是未完成
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.currX, mScroller.currY)
            postInvalidate()
        }
        super.computeScroll()
    }

生命周期感知

通过addZiRuLifecycleObserver()方法设置activity生命周期观察者,使view拥有生命周期感知的能力,自主注册和反注册。

    fun addZiRuLifecycleObserver(owner: LifecycleOwner?) {
        owner?.lifecycle?.addObserver(ZiRuLifecycleObserverAdapter(owner, this))
    }

    override fun onResume(owner: LifecycleOwner?) {
        if (useGyroscope) {
            mSensorManager?.registerListener(this, mSensorGyroscope, SENSOR_DELAY_GAME)
        } else {
            mSensorManager?.registerListener(this, mSensorAccelerometer, SENSOR_DELAY_GAME)
        }
    }

    override fun onPause(owner: LifecycleOwner?) {
        mSensorManager?.unregisterListener(this)
    }

    override fun onDestroy(owner: LifecycleOwner?) {
    }
    ...

interface ZiRuLifecycleObserver : LifecycleObserver {
    fun onResume(owner: LifecycleOwner?)
    fun onPause(owner: LifecycleOwner?)
    fun onDestroy(owner: LifecycleOwner?)
}

class ZiRuLifecycleObserverAdapter(
    private val mLifecycleOwner: LifecycleOwner,
    private val mObserver: ZiRuLifecycleObserver
) :
    LifecycleObserver {
    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun onResume() {
        LogUtils.d("ZiRuLifecycleObserverAdapter onResume")
        mObserver.onResume(mLifecycleOwner)
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun onPause() {
        LogUtils.d("ZiRuLifecycleObserverAdapter onPause")
        mObserver.onPause(mLifecycleOwner)
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy() {
        LogUtils.d("ZiRuLifecycleObserverAdapter onDestroy")
        mObserver.onDestroy(mLifecycleOwner)
    }
}

Banner翻页效果

自定义viewpager的PageTransformer,重写transformPage()方法实现banner背景淡入淡出,上层浮动的图案正常左右滑动。

class ZiRuBannerTransformer : BasePageTransformer() {
    private var mMinAlpha: Float = DEFAULT_MIN_ALPHA

    override fun transformPage(view: View, position: Float) {
        val pageWidth = view.width //得到view宽
        when {
            position < -1 -> { // [-Infinity,-1)
                // This page is way off-screen to the left. 出了左边屏幕
                view.alpha = mMinAlpha
            }
            position <= 1 -> { // [-1,1]
                var factor = 0f
                if (position < 0) {
                    //消失的页面
                    view.translationX = -pageWidth * position //阻止消失页面的滑动
                    (view as FrameLayout).run {
                        view.findViewById(R.id.frame_layout).translationX =
                            pageWidth * position
                    }
                    factor = mMinAlpha + (1 - mMinAlpha) * (1 + position)
                } else {
                    //出现的页面
                    view.translationX = pageWidth.toFloat() //直接设置出现的页面到底
                    (view as FrameLayout).run {
                        view.findViewById(R.id.frame_layout).translationX =
                            pageWidth * position
                    }
                    view.translationX = -pageWidth * position //阻止出现页面的滑动
                    factor = mMinAlpha + (1 - mMinAlpha) * (1 - position)
                }
                //透明度改变Log
                view.alpha = factor
            }
            else -> { // (1,+Infinity]
                // This page is way off-screen to the right.    出了右边屏幕
                view.alpha = mMinAlpha
            }
        }
    }

    companion object {
        private const val DEFAULT_MIN_ALPHA = 0.0f
    }
}

Banner的Item布局示例




    

        

            

        

        

            

            

                

            


        

    


你可能感兴趣的:(Android 仿自如APP裸眼3D效果)