事件分发机制与NestedScrolling机制

事件分发机制与NestedScrolling机制

一、事件分发机制

1.理论分析

事件分发涉及的是ViewViewGroup,相关事件:dispatchTouchEventonInterceptTouchEventOnTouchEvent,其中onInterceptTouchEvent只有ViewGroup才有这个方法。

当一个Touch事件到来时,它会从Activity向下依次分发,分发的过程是通过调用ViewGroup或View的dispatchTouchEvent方法实现的。详细的说就是根ViewGroup遍历包含的子view,如果子view处于事件触发范围内,调用每个子View的dispatchTouchEvent,当这个View为ViewGroup的时候,又会进行遍历其内部子view继续调用子view的dispatchTouchEvent。该方法有boolean类型的返回值,当返回true时,事件分发会中断,交给返回true的view的onTouchEvent处理事件,返回false才会执行下一个view的dispatchTouchEvent,如果子view的dispatchTouchEvent都返回false(其实就是view内部的onTouchEvent返回false),那么就会执行到父view的onTouchEvent。

下面是该过程分析的伪代码

public boolean dispatchTouchEvent(MotionEvent ev) {
	 if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

//如果是DOWN 则会遍历子view, 找出消耗事件的子view
//mFirstTouchTarget指向找到的子view, 并且子view处理事件

if(mFirstTouchTarget == null){
	onTouchEvent() //执行自身OnTouchEvent
}else{
	if(intercepted){
		//给子view分发cancel事件,并把mFirstTouchTarget置为null
		//下次move事件才会真正给自己处理
	} else{
		mFirstTouchTarget.child.dispatchTouchEvent() //子view处理事件
	}
}     

下面是简单的事件分发流程图:(针对down事件分析)

事件分发流程图

总结:其实就是dipatchTouchEvent层层向下分发,分发的过程中涉及onInterceptTouchEvent和OnTouchEvent的调用。对于ViewGroup,当其下面的所有子view都不处理事件的时候,才会调用到自己的onTouchEvent,对于View,dispatchTouchEvent里面就直接调用了onTouchEvent,然后根据返回值决定是否处理该事件。

如果某个父view的onInterceptTouchEvent返回false,之后每次事件都会询问是否拦截 如果onInterceptTouchEvent返回true,那么后续的move up事件将不再询问是否拦截,直接交给自己onTouchEvent处理

如果某个父view的onInterceptTouchEvent返回true,事件不会向下继续分发,会回调自己的onTouchEvent 如果自己的onTouchEvent返回false,则回调给父ViewGroup的onTouchEvent,如果自己的onTouchEvent返回true,那么后续的move up事件都给他

如果ACTION_DOWN遍历了所有view都没有找到处理事件的view,那么MOVE,UP事件将不会分发,即事件存在没有被消费的情况

2.事件冲突解决方法
1.外部拦截法

即父view根据需要对事件进行拦截,重写onInterceptTouchEvent

伪代码为:

	 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercept = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if(满足父容器拦截要求){
                    intercept = true;
                }else{
                    intercept = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercept = false;
                break;
        }
        return intercept;
    }

有几点需要注意:

1.ACTION_DOWN一定要返回false,否则如果返回true,那么后续的move up事件就会都交给父view处理,事件没有机会到达子view

2.ACTION_MOVE中在父view的需要的时候返回true,然后父view进行事件处理

3.原则上ACTION_UP也需要返回false,因为如果在move中没达到父view的拦截条件,up的时候返回true,那子view就收不到up事件,onClick等事件就无法触发。如果在move中达到了父view的拦截条件,那么up返回什么都无所谓了,因为都会直接交给父view处理。

2.内部拦截法

即子view根据实际情况允许或不允许父view拦截。

伪代码为:

//父view的实现
//父view除了ACTION_DOWN,其他都默认拦截  ACTION_DOWN不拦截主要是为了让子view能收到事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            return false;
        } else {
            return true;
        }
}
//子child
@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                 //不允许父view进行拦截
            		getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
            		//在不需要自己处理的时候,允许父容器进行拦截
                if(不需要自己处理){
                 getParent().requestDisallowInterceptTouchEvent(false);
                }else{
                		//在这里做自己的事情
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        return true;
    }

二、NestedScrolling机制

NestedScrolling机制是为了解决现在事件分发同一个事件中只能有一个view响应事件的问题,在这个机制中,父view和子view可以配合滚动,子view滚动之前会先询问父view是否要进行嵌套滚动,并且如果嵌套滚动的话,消耗多少,剩余的交给子view继续滚动。

现在android中很多组件已经实现了嵌套滚动的机制,比如RecyclerView和NestedScrollView,CoordinatorLayout等,当然我们也可以自定义,如果自定义的话我们首先需要了解下面的几个类:

  • NestedScrollingChild 实现嵌套滚动的子view需要实现的接口
  • NestedScrollingChildHelper 对实现嵌套滚动的子view提供的嵌套滚动工具类
  • NestedScrollingParent 实现嵌套滚动的父view需要实现的接口
  • NestedScrollingParentHelper 对实现嵌套滚动的父view提供的嵌套滚动工具类

在这里插入图片描述

  1. 在 ACTION_DOWN 或者ACTION_MOVE 刚开始的时候,Scrolling child 会调用 startNestedScroll 方法
    通过 ChildHelper 回调 Scrolling Parent 的 onStartNestedScroll 方法, 如果onStartNestedScroll返回true,则还会调用 parent的 onNestedScrollAccepted

  2. 在 ACTION_MOVE 的时候,Scrolling Child 每 MOVE一段距离,可以调用 dispatchNestedPreScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否要先于 Child 进行 滑动,
    会调用 Parent 的 onNestedPreScroll 方法,然后父容器先根据自身情况滑动一段距离,剩余距离子view继续滑动

  3. 当 ScrollingChild 滑动完成的时候,可以调用 dispatchNestedScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否需要进行滑动
    会调用 Parent 的 onNestedScroll 方法,父view再根据自身情况处理是否要继续滑动未消费的距离

  4. 如果需要处理 Fling 动作,我们可以通过 VelocityTracker 获得相应的速度,并在 ACTION_UP 的时候,调用 dispatchNestedPreFling 方法,
    通过 ChildHelper 询问 Parent 是否需要先于 child 进行 Fling 动作, 会调用Parent 的 onNestedPreFling 方法, 剩余距离交给子view fling

  5. 在 Child 处理完 Fling 动作时候,如果 Scrolling Parent 还需要处理 Fling 动作,
    我们可以调用 dispatchNestedFling 方法,通过 ChildHelper ,调用 Parent 的 onNestedFling 方法, 父view根据自身情况处理是否继续fling未消费的距离

  6. 在 ACTION_UP或者ACTION_CANCEL 的时候,会调用 Scrolling Child 的stopNestedScroll ,
    通过 ChildHelper 询问 Scrolling parent 的 onStopNestedScroll 方法

class OuterNestedScrollViewGroup @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyle: Int = 0
) : ConstraintLayout(context, attributeSet, defStyle), NestedScrollingParent3 {
    private var mLastConsumedY = 0
    private var mNestedScrolling = false
    private var mListener: SlideListener? = null
    private val mScroller: Scroller = Scroller(context)
    private var mCurrentStage = -1
    private var mTargetStage = -1
    private lateinit var mHeightRatios: FloatArray

    fun setPanelSlideListener(listener: SlideListener?) {
        mListener = listener
    }

    fun setMultiHeightRatioFromBottomToTop(args: FloatArray) {
        mHeightRatios = args
    }

    fun go2StageBySmooth(targetStage: Int) {
        if (targetStage >= mHeightRatios.size) {
            throw RuntimeException("stage must not beyond the max stage")
        }
        this.scrollY = -getRealHeight()
        log("getRealHeight = ${getRealHeight()}")
        smoothScrollToStage(targetStage, DEFAULT_SCROLL_DURATION)
        this.visibility = View.VISIBLE
    }

    private fun getRealHeight(): Int {
        var totalHeight = 0
        for (index in 0 until childCount) {
            totalHeight += getChildAt(index).height
        }
        if (totalHeight > measuredHeight) {
            totalHeight = measuredHeight
        }
        return totalHeight
    }

    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        if (event.action == MotionEvent.ACTION_DOWN) {
            if (!mScroller.isFinished) {
                log("abortAnimation")
                mScroller.abortAnimation()
            }
        }
        return super.onInterceptTouchEvent(event)
    }

    override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
        log("onStartNestedScroll target = $target")
        return type == ViewCompat.TYPE_TOUCH
    }

    override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
        log("onNestedScrollAccepted")
    }

    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
        //上滑dy > 0 下滑dy < 0
        //滑出上屏幕scrollY > 0   在屏幕下面scrollY < 0
        log("onNestedPreScroll dy = $dy scrollY = $scrollY target = $target")
        val scrollToTop = dy > 0 && scrollY < getMaxScrollY() //上滑
        val scrollToBottom = //下滑
            dy < 0 && scrollY > getStageScrollY(0) && !target.canScrollVertically(-1)
        if (scrollToTop || scrollToBottom) {
            mNestedScrolling = true
            scrollBy(0, dy)
            consumed[1] = dy
            mLastConsumedY = dy
            log("consumed[1] = $dy")
        } else {
            log("not consume")
        }
    }

    private fun getMaxScrollY(): Int {
        return getStageScrollY(mHeightRatios.size - 1)
    }

    override fun onNestedScroll(
        target: View,
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        type: Int,
        consumed: IntArray
    ) {

    }

    override fun onNestedScroll(
        target: View,
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        type: Int
    ) {
    }

    override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
        log("onNestedPreFling velocityY = $velocityY")
        return onFingerReleased(velocityY)
    }

    private fun onFingerReleased(velocityY: Float): Boolean {
        if (!mNestedScrolling || scrollY >= getMaxScrollY()) {
            return false
        }
        log("onFingerReleased velocityY = $velocityY")
        mNestedScrolling = false
        var targetStage = mCurrentStage
        when {
            (velocityY > 0) -> { //上滑
                mHeightRatios.forEachIndexed { stage, _ ->
                    if (scrollY > getStageScrollY(stage)) {
                        targetStage = if (stage < mHeightRatios.size - 1) {
                            stage + 1
                        } else {
                            stage
                        }
                    }
                }
                log("onFingerReleased 上滑 targetStage = $targetStage")
            }
            (velocityY < 0) -> { //下滑
                mHeightRatios.forEachIndexed { stage, _ ->
                    if (scrollY > getStageScrollY(stage)) {
                        targetStage = stage
                    }
                }
                log("onFingerReleased 下滑 targetStage = $targetStage")
            }
        }
        mListener?.onSlideReleased(0f, velocityY, targetStage)
        smoothScrollToStage(targetStage, DEFAULT_SCROLL_DURATION)
        mLastConsumedY = 0
        return true
    }

    private fun smoothScrollToStage(targetStage: Int, duration: Int) {
        if (getRealHeight() <= 0) {
            return
        }
        if (targetStage == mCurrentStage) {
            return
        }
        mTargetStage = targetStage
        val y = getStageScrollY(targetStage)
        mScroller.startScroll(0, scrollY, 0, y - scrollY, duration)
        invalidate()
    }

    private fun getStageScrollY(stage: Int): Int {
        return -(mHeightRatios[stage] * getRealHeight()).toInt()
    }

    override fun onStopNestedScroll(target: View, type: Int) {
        log("onStopNestedScroll target = $target")
        onFingerReleased(mLastConsumedY.toFloat())
    }

    override fun computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.currX, mScroller.currY)
            invalidate()
        } else {
            //startScroll 最后一次会返回false, 这里要回调状态
            if (mNestedScrolling) {
                return
            }
            setPanelStage(mTargetStage)
        }
    }

    private fun setPanelStage(stage: Int) {
        if (mCurrentStage != stage) {
            mListener?.onStateChanged(mCurrentStage, stage)
            mCurrentStage = stage
        }
    }

    override fun scrollTo(@Px x: Int, @Px y: Int) {
        // 控制滚动的边界值:
        var yy = y
        if (y > getMaxScrollY()) {
            yy = getMaxScrollY()
        }
        if (y < getStageScrollY(0)) {
            yy = getStageScrollY(0)
        }
        val realHeight = getRealHeight()
        if (realHeight != 0) {
            val offset = -yy / realHeight.toFloat()
            log("scrollOffset = $offset")
            mListener?.onScrollOffset(offset)
        }
        super.scrollTo(x, yy)
    }


    companion object {
        const val DEFAULT_SCROLL_DURATION = 600
        val mTag = OuterNestedScrollViewGroup::class.simpleName
    }

    interface SlideListener {
        fun onStateChanged(oldStage: Int, newStage: Int)
        fun onScrollOffset(offset: Float)
        fun onSlideReleased(dx: Float, dy: Float, stage: Int)
    }

    private fun log(logText: String) {
        Log.d(mTag, logText)
    }
}
class InnerNestedScrollViewGroup @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyle: Int = 0
) : ConstraintLayout(context, attributeSet, defStyle), NestedScrollingChild3 {

    private var mTouchSlop: Int = 0
    private var mMinimumFlingVelocity: Int = 0
    private var mMaximumFlingVelocity: Int = 0
    private var mVelocityTracker: VelocityTracker = VelocityTracker.obtain()
    private var mNestedScrollingChildHelper: NestedScrollingChildHelper =
        NestedScrollingChildHelper(this)
    private var mCurrentY = 0
    private var mCurrentX = 0
    private var mStartedNestedScroll = false

    init {
        val viewConfiguration = ViewConfiguration.get(context)
        mTouchSlop = viewConfiguration.scaledTouchSlop
        log("mTouchSlop = $mTouchSlop")
        mMinimumFlingVelocity = viewConfiguration.scaledMinimumFlingVelocity
        mMaximumFlingVelocity = viewConfiguration.scaledMaximumFlingVelocity
        log("mMinimumFlingVelocity = $mMinimumFlingVelocity")
        log("mMaximumFlingVelocity = $mMaximumFlingVelocity")
        mNestedScrollingChildHelper.isNestedScrollingEnabled = true
    }


    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                log("onInterceptTouchEvent ACTION_DOWN")
                mCurrentY = (event.y + 0.5f).toInt()
                mCurrentX = (event.x + 0.5f).toInt()
            }
            MotionEvent.ACTION_MOVE -> {
                log("onInterceptTouchEvent ACTION_MOVE")
                val dx = (event.x + 0.5f).toInt() - mCurrentX
                val dy = (event.y + 0.5f).toInt() - mCurrentY
                if (abs(dy) >= mTouchSlop && abs(dy) > abs(dx)) {
                    //针对于子view有处理事件的,这里要进行拦截,子view如果不想被拦截可以调用parent.requestDisallowInterceptTouchEvent(false)
                    return true
                }
            }
        }
        return super.onInterceptTouchEvent(event)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val pointerId = event.getPointerId(event.actionIndex)
        addMovement(event)
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                //所有子view都不处理down touch事件,则MOVE事件不会再向下传递
                //所以要在本次DOWN事件这里返回true, 然后后面的move都直接交给该view onTouchEvent处理
                log("onTouchEvent ACTION_DOWN")
            }
            MotionEvent.ACTION_MOVE -> {
                log("onTouchEvent ACTION_MOVE")
                var dy = mCurrentY - (event.y + 0.5f).toInt() //上滑dy > 0 下滑dy < 0
                dy = if (dy > 0) {
                    max(0, dy - mTouchSlop)
                } else {
                    min(0, dy + mTouchSlop)
                }
                if (abs(dy) > 0) {
                    if (!mStartedNestedScroll) {
                        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH)
                    }
                    dispatchNestedPreScroll(
                        0,
                        dy,
                        null,
                        null,
                        ViewCompat.TYPE_TOUCH
                    )
                }
            }
            MotionEvent.ACTION_UP -> {
                log("onTouchEvent ACTION_UP")
                if (mStartedNestedScroll) {
                    mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity.toFloat())
                    val yVelocity = mVelocityTracker.getYVelocity(pointerId)
                    if (abs(yVelocity) > mMinimumFlingVelocity) {
                        dispatchNestedPreFling(0f, -yVelocity)
                    }
                    stopNestedScroll(ViewCompat.TYPE_TOUCH)
                }
            }
            MotionEvent.ACTION_CANCEL -> {
                log("onTouchEvent ACTION_CANCEL")
                if (mStartedNestedScroll) {
                    stopNestedScroll(ViewCompat.TYPE_TOUCH)
                }
            }
        }
        return true
    }

    private fun addMovement(event: MotionEvent) {
        val evert = MotionEvent.obtain(event)
        evert.setLocation(event.rawX, event.rawY)
        mVelocityTracker.addMovement(evert)
        evert.recycle()
    }

    private fun clearVelocityTracker() {
        mVelocityTracker.clear()
    }

    override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
        log("dispatchNestedPreFling velocityY = $velocityY")
        return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY)
    }

    override fun dispatchNestedScroll(
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        offsetInWindow: IntArray?,
        type: Int,
        consumed: IntArray
    ) {
        mNestedScrollingChildHelper.dispatchNestedScroll(
            dxConsumed,
            dyConsumed,
            dxUnconsumed,
            dyUnconsumed,
            offsetInWindow,
            type
        )
    }

    override fun dispatchNestedScroll(
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        offsetInWindow: IntArray?,
        type: Int
    ): Boolean {
        log("dispatchNestedPreScroll dyConsumed = $dyConsumed dyUnconsumed = $dyUnconsumed")
        return mNestedScrollingChildHelper.dispatchNestedScroll(
            dxConsumed,
            dyConsumed,
            dxUnconsumed,
            dyUnconsumed,
            offsetInWindow,
            type
        )
    }

    override fun dispatchNestedPreScroll(
        dx: Int,
        dy: Int,
        consumed: IntArray?,
        offsetInWindow: IntArray?,
        type: Int
    ): Boolean {
        log("dispatchNestedPreScroll dy = $dy")
        return mNestedScrollingChildHelper.dispatchNestedPreScroll(
            dx,
            dy,
            consumed,
            offsetInWindow,
            type
        )
    }

    override fun stopNestedScroll(type: Int) {
        log("stopNestedScroll")
        mStartedNestedScroll = false
        clearVelocityTracker()
        mNestedScrollingChildHelper.stopNestedScroll(type)
    }

    override fun setNestedScrollingEnabled(enabled: Boolean) {
        log("setNestedScrollingEnabled enabled = $enabled")
        mNestedScrollingChildHelper.isNestedScrollingEnabled = enabled
    }

    override fun isNestedScrollingEnabled(): Boolean {
        log("isNestedScrollingEnabled")
        return mNestedScrollingChildHelper.isNestedScrollingEnabled
    }

    override fun hasNestedScrollingParent(type: Int): Boolean {
        log("hasNestedScrollingParent")
        return mNestedScrollingChildHelper.hasNestedScrollingParent(type)
    }

    override fun startNestedScroll(axes: Int, type: Int): Boolean {
        log("startNestedScroll")
        mStartedNestedScroll = true
        return mNestedScrollingChildHelper.startNestedScroll(axes, type)
    }

    companion object {
        val mTag = InnerNestedScrollViewGroup::class.simpleName
    }


    private fun log(logText: String) {
        Log.d(mTag, logText)
    }
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值