自定义控件翻页效果

786 阅读5分钟

1.需求

看小说时候感觉页面翻转动画挺有意思,现象如下:

00283243-8216-4839-bb0a-338b5d7ab1f4.gif

2.分析现象

从图中可以看到。手指在上面View1左滑,上面View1随手左移,下面View2内容逐渐展示出来,这时至少存在两个页面展示。从现象来看,在上面View1滑动之前,下面View2数据就已经加载完成。手指在屏幕上滑动,最上层View1随手指滑动方向移动。抬手以后上面页面View会划出屏幕,让View2显示。

注意: View1代表上面滑View,View2代表下面被覆盖View。另外,须知整个过程只有View1滑动。

3.代码

3.1 创建布局文件

上面提到,这个界面需要至少两个View交互,才能完成上,下覆盖出现的效果。 首先需要考虑下最外层的GroupView,对于常用布局,FrameLayoutRelativeLayout可以做到view1整个盖在view2上面效果,而LinearLayout只会让View并排显示,其他可以实现覆盖效果的布局也可以,需要进行调研使用。多方面考虑相较于RelativeLayoutFrameLayout更友好,此处便使用这个父布局。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="@color/cardview_light_background">

<TextView
    android:id="@+id/tv1"
    android:layout_margin="50dp"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:background="#333333"/>

<TextView
    android:id="@+id/tv2"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:layout_margin="50dp"
    android:background="#333333"/>
</FrameLayout>

3.2 滑动处理

3.2.1 随手移动

移动过程中,上层View需要根据手势判断向左还是向右移动,当前event.x大于上次保存startX值,表示需要将上面View1向左移动,此处使用view的translationX进行移动。相反当前event.x小于上次保存startX值则向右移动。

MotionEvent.ACTION_MOVE -> {
        //上一页
        if (event.x > startX) {
            Log.d("TAG-sun", "onTouchEvent: 上一页")
            movePage(1)
            binding?.tv2?.translationX = event.x - startX
        }
        //下一页
        if (event.x < startX) {
            movePage(-1)
            binding?.tv2?.translationX = event.x - startX
        }
}

3.2.2 手指离开屏幕

手指离开屏幕后,会触发ACTION_UP,此时上层View1需要移出窗口,这里使用属性动画进行移动。screenWidth代表屏幕宽度。statu为1时表示上一个页面,statu为-1时表示下一个页面。从下面代码,ACTION_UP事件触发后可以根据statu值进行左右滑动区分。

MotionEvent.ACTION_UP -> {

        transitionX?.setFloatValues(
            binding?.tv2!!.translationX,
            screenWidth!!.toFloat() * status
        )
        transitionX?.start()
}

3.2.3 View1位置恢复

由于在布局过程中View1始终是盖在View2上面的,为了体现出划入和划出的效果。在整个过程中,其实只有最上层的View1进行了滑动,View2进行数据的展示。那么对于 3.2.2 手指离开屏幕中我们将上层View1移出了窗口,此时我们需要将View1再次回到桌面,这个时候回来的过程我们需要悄悄进行,使用户看不出区别。可以这样做

addListener(object : Animator.AnimatorListener {
            override fun onAnimationStart(animation: Animator) {

            }

            override fun onAnimationEnd(animation: Animator) {
                updateText(text, binding?.tv2)
                binding?.tv2?.alpha = 0f
                binding?.tv2?.translationX = 0f
                binding?.tv2?.alpha = 1f
            }

            override fun onAnimationCancel(animation: Animator) {

            }

            override fun onAnimationRepeat(animation: Animator) {

            }

        }

我们可以在动画开始和结束过程中做一些操作。上面代码中监听了MotionEvent.ACTION_UP时属性动画,当动画结束时做如下操作: 第一步:binding?.tv2?.alpha = 0f将View1隐藏 第二步:updateText(text, binding?.tv2)更新View1数据与View2数据同步 第三步:binding?.tv2?.translationX = 0f将View1移回到窗口 第四步:binding?.tv2?.alpha = 1f将View1显示出来,由于此时View1和View2数据完全相同,就能彻底覆盖,看不出有切换的效果。

3.2.4 完整代码

3.2.4.1 View代码

class PageMoveView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0)
    : FrameLayout(context,attrs,defStyleAttr) {

    val TAG = "PageMoveView"

    var current = 0

    var startX = 0f

    var startY = 0f

    var transitionX: ValueAnimator? = null

    //1代表下一页 -1代表上一页
    var status: Int = 1

    var datas: MutableList<String> = mutableListOf<String>()

    private var screenWidth: Int? = null

    private var text: String? = null

    private var interceptorUpdate = true

    private var interceptorMove = false


    private var binding: MovePageViewBinding? = null

    init {
        screenWidth = getScreenWidth(context)
        binding = MovePageViewBinding.inflate(LayoutInflater.from(context))
        val viewGroup = ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT
        )
        addView(binding!!.root, viewGroup)
    }


    fun setData(datas: MutableList<String>) {
        this.datas = datas
        initView()
    }

    private fun initView() {
        transitionX = getAnimatorValue("transitionX")?.apply {
            addUpdateListener {
                binding?.tv2?.translationX = it.animatedValue as Float
            }
            addListener(object : Animator.AnimatorListener {
                override fun onAnimationStart(animation: Animator) {

                }

                override fun onAnimationEnd(animation: Animator) {
                    updateText(text, binding?.tv2)
                    interceptorUpdate = true
                    binding?.tv2?.alpha = 0f
                    binding?.tv2?.translationX = 0f
                    binding?.tv2?.alpha = 1f
                }

                override fun onAnimationCancel(animation: Animator) {

                }

                override fun onAnimationRepeat(animation: Animator) {

                }

            })
        }
        if (datas.size <= 0) {
            Log.d(TAG, "initView: ")
        } else {
            //第一页和第二页加载相同页面
            updateText(datas[current], binding?.tv1)
            updateText(datas[current], binding?.tv2)
        }
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                startX = event.x
                startY = event.y
            }

            MotionEvent.ACTION_MOVE -> {
                    //上一页
                    if (event.x > startX) {
                        Log.d("TAG-sun", "onTouchEvent: 上一页")
                        movePage(1)
                        binding?.tv2?.translationX = event.x - startX
                    }
                    //下一页
                    if (event.x < startX) {
                        movePage(-1)
                        binding?.tv2?.translationX = event.x - startX
                    }
            }

            MotionEvent.ACTION_UP -> {
                    transitionX?.setFloatValues(
                        binding?.tv2!!.translationX,
                        screenWidth!!.toFloat() * status
                    )
                    transitionX?.start()
            }

            MotionEvent.ACTION_CANCEL -> {
                transitionX?.setFloatValues(
                    binding?.tv2!!.translationX,
                    screenWidth!!.toFloat() * status
                )
                transitionX?.start()
            }
        }
        return super.onTouchEvent(event)
    }


    private fun movePage(status: Int) {
        this.status = status
        if (current >= 0 && datas.size > 0) {
            //每次近来北京还一次锁死
            if (interceptorUpdate) {
                if (status == 1) {
                    //下一页
                    current = (current + 1) % datas.size
                } else {
                    if (current == 0) {
                        current = datas.size - 1
                    } else {
                        current -= 1
                    }
                }
                text = datas[current]
                updateText(text, binding!!.tv1)
                interceptorUpdate = false
            }
        }
    }

    private fun updateText(text: String?, textView: TextView?) {
        text.let {
            textView?.text = text
        }
    }


    fun getScreenWidth(context: Context): Int {
        val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        val display = windowManager.defaultDisplay
        val size = Point()
        display.getSize(size)
        return size.x
    }

    var animaValue = HashMap<String, ValueAnimator>()

    fun getAnimatorValue(key: String): ValueAnimator? {
        if (animaValue[key] == null) {
            animaValue[key] = ValueAnimator.ofFloat().apply {
                duration = 100
            }
        }
        return animaValue[key]
    }

}

3.2.4.2 View中加载的布局文件

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="@color/cardview_light_background">

<TextView
    android:id="@+id/tv1"
    android:layout_margin="50dp"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:background="#333333"/>

<TextView
    android:id="@+id/tv2"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:layout_margin="50dp"
    android:background="#333333"/>
</FrameLayout>

3.2.4.3 Activity布局文件

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="@color/cardview_light_background">
<com.kt.view.move.PageMoveView
    android:id="@+id/page"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:focusable="true"
    android:clickable="true"/>
</FrameLayout>

3.2.4.4 Activity文件

class PageMoveActivity() : BaseActivity<MovePageActivityBinding>() {

    var textList = mutableListOf<String>().apply {
        add("第一页")
        add("第二页")
        add("第三页")
        add("第四页")
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding.page.setData(textList)
    }
}

3.3 效果展示

5e235cd4-c62e-45dd-8e44-13c73b54f571.gif

边界不太明显,加上阴影效果就可以了。