最近听同事说自如banner的裸眼3D效果很有创意,下载APP体验了一番觉得效果确实非常不错,所以立马就仿了一下。代码已上传至github仓库中,AndroidUiDemo
地址:https://github.com/SHPDZY/AndroidUiDemo
下面是demo和自如app的效果对比。
忽略搜索栏哈哈,我是直接截屏自如主页用ps扣来的图。
1628563109014.gif1628563131510.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>
网友评论