Android 仿自如APP裸眼3D效果

作者: 写BUG的张永 | 来源:发表于2021-08-27 09:18 被阅读0次

    最近听同事说自如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<FrameLayout>(R.id.frame_layout).translationX =
                                pageWidth * position
                        }
                        factor = mMinAlpha + (1 - mMinAlpha) * (1 + position)
                    } else {
                        //出现的页面
                        view.translationX = pageWidth.toFloat() //直接设置出现的页面到底
                        (view as FrameLayout).run {
                            view.findViewById<FrameLayout>(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布局示例

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
    
            <com.example.zyuidemo.widget.ZiRuLayout
                android:id="@+id/zr_bac"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    
                <ImageView
                    android:id="@+id/sdv_single"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:scaleType="fitXY"
                    android:src="@drawable/img_bac_1" />
    
            </com.example.zyuidemo.widget.ZiRuLayout>
    
            <FrameLayout
                android:id="@+id/frame_layout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">
    
                <ImageView
                    android:id="@+id/iv_ad_text"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:scaleType="fitXY"
                    android:src="@drawable/img_ad_text_1" />
    
                <com.example.zyuidemo.widget.ZiRuLayout
                    android:id="@+id/zr_text"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent">
    
                    <ImageView
                        android:id="@+id/iv_ad_icon"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:scaleType="fitXY"
                        android:src="@drawable/img_ad_bird_1" />
    
                </com.example.zyuidemo.widget.ZiRuLayout>
    
    
            </FrameLayout>
    
        </FrameLayout>
    
    </layout>
    

    相关文章

      网友评论

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

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