事件分发机制与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

伪代码为:

扫描二维码关注公众号,回复: 12726358 查看本文章
	 @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提供的嵌套滚动工具类
public class MyNestedScrollingChild extends LinearLayout implements NestedScrollingChild {
    
    
    private NestedScrollingChildHelper mNestedScrollingChildHelper;
    private int[] offset = new int[2];
    private int[] consumed = new int[2];
    private int lastY;

    public MyNestedScrollingChild(Context context) {
    
    
        this(context, null);
    }

    public MyNestedScrollingChild(Context context, @Nullable AttributeSet attrs) {
    
    
        super(context, attrs, 0);
    }

    public MyNestedScrollingChild(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    
    
        super(context, attrs, defStyleAttr);
        mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
    }

    //需要重写onTouchEvent在需要的时候将滚动传给父容器,问他要消耗多少,剩下自己用多少
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    
    
        switch (event.getAction()) {
    
    
            case MotionEvent.ACTION_DOWN:
                lastY = (int) event.getRawY();
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_VERTICAL;
                //down的时候开始嵌套滚动
                startNestedScroll(nestedScrollAxis);
                break;
            case MotionEvent.ACTION_MOVE:
                int y = (int) (event.getRawY());
                int dy = y - lastY;
                lastY = y;
                //滑动中
                if (dispatchNestedPreScroll(0, dy, consumed, offset)) {
    
    
                    int remain = dy - consumed[1];
                    dispatchNestedScroll(0, consumed[1], 0, remain, offset);
                } else {
    
    
                    scrollBy(0, -dy);
                }
                break;
            case MotionEvent.ACTION_UP:
                stopNestedScroll();
                break;
        }
        return true;
    }

    /**
     * 设置嵌套滑动是否能用
     *
     * @param enabled
     */
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
    
    
        mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
    }

    /**
     * 判断嵌套滑动是否可用
     *
     * @return
     */
    @Override
    public boolean isNestedScrollingEnabled() {
    
    
        return true;
    }

    /**
     * 开始嵌套滑动,会查找有没有嵌套滑动的父view,如果有调用父view的onStartNestedScroll和onNestedScrollAccepted
     *
     * @param axes
     * @return
     */
    @Override
    public boolean startNestedScroll(int axes) {
    
    
        return mNestedScrollingChildHelper.startNestedScroll(axes);
    }

    /**
     * 停止嵌套滑动 会回调父view的onNestStopScroll
     */
    @Override
    public void stopNestedScroll() {
    
    
        mNestedScrollingChildHelper.stopNestedScroll();
    }

    /**
     * 判断是否有父view支持嵌套滑动
     *
     * @return
     */
    @Override
    public boolean hasNestedScrollingParent() {
    
    
        return mNestedScrollingChildHelper.hasNestedScrollingParent();
    }

    /**
     * 在开始滑动后,调用该方法通知父view滑动的距离,会回调父view的onNestPreScroll和onNestScroll方法
     *
     * @param dx
     * @param dy
     * @param consumed
     * @param offsetInWindow
     * @return
     */
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
    
    
        return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    /**
     * 子view于父view后滑动
     *
     * @param dxConsumed
     * @param dyConsumed
     * @param dxUnconsumed
     * @param dyUnconsumed
     * @param offsetInWindow
     * @return
     */
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
    
    
        return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }


    /**
     * 在开始滑行后,调用该方法通知父view滑动的距离,会回调父view的onNestPreFling和onNestFling方法
     *
     * @param velocityX
     * @param velocityY
     * @return
     */
    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
    
    
        return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }

    /**
     * 子view于父view后滑行
     *
     * @param velocityX
     * @param velocityY
     * @param consumed
     * @return
     */
    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
    
    
        return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }
}
public class MyNestedScrollingParent extends LinearLayout implements NestedScrollingParent {
    
    
    private NestedScrollingParentHelper mNestedScrollingParentHelper;

    public MyNestedScrollingParent(Context context) {
    
    
        this(context, null);
    }

    public MyNestedScrollingParent(Context context, @Nullable AttributeSet attrs) {
    
    
        super(context, attrs, 0);
    }

    public MyNestedScrollingParent(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    
    
        super(context, attrs, defStyleAttr);
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    
    
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int axes) {
    
    
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    
    
        //这里需要根据实际情况进行消耗滑动的距离
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    
    
        //这里需要根据实际情况进行消耗滑动的距离
    }

    @Override
    public int getNestedScrollAxes() {
    
    
        return mNestedScrollingParentHelper.getNestedScrollAxes();
    }

    @Override
    public void onStopNestedScroll(View target) {
    
    
        mNestedScrollingParentHelper.onStopNestedScroll(target);
    }
}

猜你喜欢

转载自blog.csdn.net/qq_33666539/article/details/85005331
今日推荐