Android 自定义滑动控件

简介

通过自定义滑动控件进一步熟悉触摸事件的分发,以及常见的滑动的实现,目的的达到能够完成简单的自定义滑动控件,以及能够读懂第三方开源的自定义滑动组件。通过工程实现一步一步的实现滑动组件,工程源码 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进行重绘

总结

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值