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<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>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,273评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,349评论 3 398
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,709评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,520评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,515评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,158评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,755评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,660评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,203评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,287评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,427评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,122评论 5 349
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,801评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,272评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,393评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,808评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,440评论 2 359

推荐阅读更多精彩内容