欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android 自定义滑动控件

程序员文章站 2022-03-15 21:21:28
简介...

简介

通过自定义滑动控件进一步熟悉触摸事件的分发,以及常见的滑动的实现,目的的达到能够完成简单的自定义滑动控件,以及能够读懂第三方开源的自定义滑动组件。通过工程实现一步一步的实现滑动组件,工程源码 https://github.com/CodeKitBox/Control.git

滑动

在触摸事件分发的过程中,UI控件通过响应 MotionEvent来接收到触摸事件。在不考虑多指触摸的前提下,触摸事件的典型事件有 MotionEvent.ACTION_DOWNMotionEvent.ACTION_MOVE,MotionEvent.ACTION_UP,MotionEvent.ACTION_CANCEL

正常情况下,手指触摸屏幕会发生一些列的事件,事件为 MotionEvent.ACTION_DOWN->N* MotionEvent.ACTION_MOVE ->MotionEvent.ACTION_UP。当N为0 ,或者 MotionEvent.ACTION_MOVE的情况下滑动的距离小于滑动的阈值,事件为点击事件。在N 大于0,且MotionEvent.ACTION_MOVE的情况下滑动的距离大于滑动的阈值,事件为滑动事件。

自定义控件实现滑动

class MyScrollView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    LinearLayout(context, attrs, defStyleAttr){
        /**
        LinearLayout 布局对于超过容器尺寸的,不调用子控件的布局接口,因此子类重写,调用所有子空间的布局接口
        **/
        override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int){
            ...
        }
        /**
        分发触摸事件
        1. 当函数 onInterceptTouchEvent 返回值为true的时候,由控件本身的 onTouchEvent 来处理触摸事件,其返回值影响了dispatchTouchEvent的返回值。
        2. 当函数 onInterceptTouchEvent 返回值为false的时候,先分发给子控件的 dispatchTouchEvent来处理事件,如果子控件的dispatchTouchEvent返回值为false, 然后再由控件本身的 onTouchEvent 来处理触摸事件,其返回值影响了dispatchTouchEvent的返回值。
        @return true: 控件消费触摸事件 false: 控件不消费触摸事件
        **/
        override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        	val superConsume = super.dispatchTouchEvent(ev)
            // 控件需要处理触摸事件,滑动事件由自身来处理,点击事件由子类来实现,因此直接返回true
        	return true
    	}
        /**
        1. 当返回值为 true 是,由控件本身的 onTouch 函数来处理事件,否则通过分发流程来处理。
        2. 是否返回 true 的规则是滑动距离是否满足大于阈值。
        **/
        override fun onInterceptTouchEvent(ev: MotionEvent): Boolean{
            ...
        }
        /**
        1. 在事件类型为  MotionEvent.ACTION_MOVE 需要进行滑动处理。
        2. 存在由系统分发到控件本身的onTouchEvent函数,此时 onInterceptTouchEvent 返回值为 false,因此
        onInterceptTouchEvent 函数中的一些前置操作也要在 onTouchEvent中执行。
        **/
        override fun onTouchEvent(event: MotionEvent){
           val vtev = MotionEvent.obtain(event)
        when(vtev.actionMasked){
            MotionEvent.ACTION_DOWN->{}
            MotionEvent.ACTION_MOVE->{
                val dy = differY(event.y.toInt())
                // 通过系统分发到这里,判断是否可以滑动
                if(abs(dy) > touchSlop && !mIsBeingDragged){
                    mIsBeingDragged = true
                    parent?.requestDisallowInterceptTouchEvent(true)
                }
                if (mIsBeingDragged){
                    // 调用View的接口判断滑动
                    // 参数  deltaX ,deltaY 指的是滑动的偏移量
                    // 参数 scrollX scrollY 指的是已经滑动的距离
                    // 参数 scrollRangeX scrollRangeY 指的是滑动的范围
                    // 参见 maxOverScrollX  maxOverScrollY 指的是越界滑动的距离
                    //  isTouchEvent 系统中此参数没有使用
                    // 返回值 true 标识达到了最大越界,在惯性滑动中使用
                    // 调用onOverScrolled 实现真正的滑动
                    //println("dy = $dy ;scrollY = $scrollY ")
                    overScrollBy(0,dy,0,scrollY,0,scrollRange,0,0,true)
                    // 记录触摸点坐标
                    saveLocation(event.x.toInt(),event.y.toInt())
                }
            }
            MotionEvent.ACTION_UP,MotionEvent.ACTION_CANCEL->{}
        }
        
    }
     override fun onOverScrolled(scrollX: Int, scrollY: Int, clampedX: Boolean, clampedY: Boolean) {
        // 在ScrollView 中对  clampedX == true clampedY == true 进行了处理
        // 滑动到指定坐标
        super.scrollTo(scrollX, scrollY)

    }

通过以上自定义的滑动控件可以实现简单的滑动组件。滑动组件的本质时通过 View#scrollBy,View#scrollTo实现控件内容的滑动。与其他的类无关。

惯性滑动

在生活中,我们开车行驶一段事件,刹车停止,一般都有一个制动距离,将这一场景模拟到滑动过程中,因此手指离开屏幕也存在一个惯性滑动。模拟这一惯性滑动可以收到更好的用户体验,因此我们使用系统的滑动控件的时候,手指离开屏幕也控件也会继续滑动,就是这个原因。

class MyScrollFlingView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    LinearLayout(context, attrs, defStyleAttr){
        // 普通的滑动不需要处理这两个类
    //<editor-fold desc="惯性或者平滑滑动相关属性">
    // 滑动辅助类,在惯性滑动,平滑滑动时使用
    private val mScroller = Scroller(context)
    // 速度模拟相关类
    private var mVelocityTracker: VelocityTracker?= null
    //</editor-fold>
        // 惯性滑动是在 MotionEvent.ACTION_UP 中事件处理
        override fun onTouchEvent(event: MotionEvent): Boolean{
            // 初始化速度控制器
            initVelocityTrackerIfNotExists()
            when(vtev.actionMasked){
                //...
                MotionEvent.ACTION_UP,MotionEvent.ACTION_CANCEL->{
                // 开始惯性滑动
                if(mIsBeingDragged){
                    mVelocityTracker?.let {
                        it.computeCurrentVelocity(1000)
                        // 判断是否支持惯性滑动,参考系统源码
                        println("惯性滑动 ${it.xVelocity};${it.xVelocity}; minFling =$minFling")
                        // 惯性滑动的速度大于系统最小的惯性滑动速度,执行惯性滑动
                        if(abs(it.yVelocity) > minFling){
                            val velocityY = -(it.yVelocity.toInt())
                            // 判断是否可以滑动
                            val canFling = (scrollY > 0 || velocityY > 0) &&
                                    (scrollY < getScrollRange() || velocityY < 0)
                            println("canFling == $canFling")
                            /**
                             * 参数 startX, startY 起始的滑动距离
                             * 参数 velocityX velocityY 滑动的速度
                             * 参数  minX minY  最小的滑动距离
                             * 参数  maxX maxY 最大的滑动就离
                             */
                            if(canFling){
                                mScroller.fling(0,scrollY,
                                        0,velocityY, 0,0, minFling, getScrollRange())
                                // 通知界面刷新 在onDraw 的时候调用 computeScroll 方法
                                invalidate()
                            }
                        }
                    }
                }
                // 设置为非拖动状态
                mIsBeingDragged = false
                // 记录触摸点坐标
                saveLocation(event.x.toInt(),event.y.toInt())
                recycleVelocityTracker()
            }
            }
            
        }
        override fun computeScroll() {}
    }

惯性滑动的原理是:当手机离开屏幕产生了 MotionEvent.ACTION_UP,按照以下流程实现惯性滑动:

  1. 通过之前滑动的速度,计算出惯性滑动的速度
  2. 根据惯性滑动的速度,是否大于系统惯性滑动的阈值决定是否需要执行惯性滑动。
  3. 执行惯性滑动,首先通过 Scroller#fling 接口,记录惯性滑动需要的参数,调用 invalidate 通知界面开始重绘。
  4. 在界面开始重绘 onDraw 中调用 computeScrollcomputeScroll函数中进行View的滑动距离,同时再次通知View进行重绘。

平滑

对控件进行滑动的方式有

  1. 通过手指触摸屏幕来实现,以上的源码已实现该流程
  2. 通过代码来实现,常见的操作有对 RecyclerView滑动到顶部,滑动到底部,滑动到指定的item项,如果使用 scrollTo接口,屏幕会立即滑动到指定位置,会出现闪一下的情况,因此需要通过代码实现在一定的事件内滑动到指定位置,因此出现了平滑的概念。

实现平滑的代码

class MyScrollSmoothView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    LinearLayout(context, attrs, defStyleAttr){
            fun smoothScrollBy(dx: Int, dy: Int) {
        var dy = dy
        if (childCount == 0) {
            // Nothing to do.
            return
        }
        val duration: Long = AnimationUtils.currentAnimationTimeMillis() - mLastScroll
        if (duration >ANIMATED_SCROLL_GAP) {
            val maxY = getScrollRange()
            val scrollY: Int = scrollY
            dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY
            mScroller.startScroll(scrollY, scrollY, 0, dy,ANIMATED_SCROLL_GAP)
            postInvalidateOnAnimation()
        } else {
            if (!mScroller.isFinished) {
                mScroller.abortAnimation()
            }
            scrollBy(dx, dy)
        }
        mLastScroll = AnimationUtils.currentAnimationTimeMillis()
    }
    }

实现平滑的原理

  1. 首先通过 Scroller#startScroll 接口,记录惯性滑动需要的参数,调用 invalidate 通知界面开始重绘
  2. 在界面开始重绘 onDraw 中调用 computeScrollcomputeScroll函数中进行View的滑动距离,同时再次通知View进行重绘

总结

通过以上的源码实现了一个自定义的滑动控件,熟悉了事件的分发流程。

本文地址:https://blog.csdn.net/cxmfzu/article/details/114207345