View体系学习之事件传递

1 源码分析

1.1 事件分发所要经过的对象

首先从三个常见的对象开始分析:

Activity ==> Window ==> ViewGroudp ==> View

手指触摸屏幕首先碰到的是Activity,然后再一步步传下去。每一步都有可能被拦截并且返回。

有个比较形象的比喻就是Activity是老板,Window是技术经理(本身也是技术能手),ViewGroup像是组长(本身也是技术能手),View是具体的员工。

老板发布任务,技术经理查看并交给组长,组长查看再交给员工。中间随时都有可能被拦截完成任务后返回给老板。

1.2 MotionEvent 触摸事件

  • ACTION_DOWN 手指刚接触屏幕
  • ACTION_MOVE 手指在屏幕滑动
  • ACTION_UP 手指离开屏幕

1.3 事件传递经历的方法

  • public boolean dispachTouchEvent(MotionEvent event)

    该方法用于分发触摸事件

  • public boolean onIntercepTouchEvent(MotionEvent event)

    该方法用于拦截触摸式见,在View中是没有该方法的。应为View直接就是末端了,没必要拦截。

  • public boolean onTouchEvent(MotionEvent event)

    该方法用于消费事件

1.4 Activity

具体先看先老板也就是Activity内部是怎么拦截的,先看下Activity的dispatchTouchEvent方法:

/**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        。。。
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

翻译的大概意思是:改方法被用于屏幕触摸事件。你可以重写该方法,用于拦截所有的触摸事件在他们分发给window之前。对于应该正常处理的触摸屏事件,请务必调用此实现。也就是说该方法是事件分发的起始点。

这边有两个分支getWindow().superDispatchTouchEvent(ev)和onTouchEvent(ev)。

先看下第一个分支,他是将分发事件通过getWindow委托给PhoneWindow去处理,也就是说核心看看superDispatchTouchEvent方法:

//@PhoneWindow:
//mDecor这个是左右界面的祖先,他是一个FrameLayout也就是ViewGroup
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

//@PhoneWindow:
//由于ViewGroup的父类是View,所以最终还是调用到了ViewGroup的dispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
	return super.dispatchTouchEvent(event);
}

所以,第一步是先跑到ViewGroup的dispatchTouchEvent方法判断是否被拦截。

如果第一步没有拦截,那么接下来执行第二部onTouchEvent(ev):

public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

//@Window
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
      final boolean isOutside =
                event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
                || event.getAction() == MotionEvent.ACTION_OUTSIDE;
        if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
            //在边界外
            return true;
        }
    	//在边界内,则表示没有被消费。返回false
        return false;
    }  

shouldCloseOnTouch是Window自带的方法,意思就是在边界内则是没有被消费返回false,在边界外则是被消费了(被判断不可反馈)。

从上可知大概分为两个步骤。1 -> 抛给DecorView的super.dispatchTouchEvent(event)分发。由于DecorView是所有界面的起点,所以该分发开始传递直到有View消费了触摸事件。2 -> 如果发现没有View消费触摸事件则交给自身的onTouchEvent处理。他会判断在边界内还是边界外。边界内的话表示确实没人处理,返回false。

1.6 ViewGroup

这边承接上面的Activity,点击DecorView之后会调用ViewGroup的dispatchTouchEvent分发:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        。。。

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            
            。。。
            // onInterceptTouchEvent分发判断是否需要拦截
            // Check for interception.
            final boolean intercepted;
            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;
            }
            
            。。。
                    
        return handled;
    }

首先注意到intercepted = onInterceptTouchEvent(ev)这行代码。他用来判断ViewGroup是否要拦截点击事件。

再往看会有个while循环,再它里面有个while循环遍历所有子View或者ViewGroup。

这边 onInterceptTouchEvent(ev)默认是返回false,当然如果有需求可以通过配置让ViewGroup去拦截。

这里有个关键的方法:dispatchTransformedTouchEvent,他用于分发事件给子类。接下来分析下dispatchTouchEvent里该方法的三个调用地方:

有三个步骤会去调用dispatchTransformedTouchEvent方法:

1.在Event是Down的情况下。在遍历子View的过程中,如果事件被消费,那么会通过往链表TouchTarget中填充新的target。

//如果被拦截的情况下就不会进入该函数
if (!canceled && !intercepted){
    。。。
    for (int i = childrenCount - 1; i >= 0; i--){
        //如果子类不能接收触摸事件或者不在点击范围内。那么继续查找
        if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
			continue;
		}
        。。。
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                。。。
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
    }
    。。。
}

//这边是往链表中添加target方法,也就是说如果有子View消费了事件。那么mFirstTouchTarget为非空
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

2.如果没有子View或者被拦截的情况下会调用,实际是调用View的事件处理函数。

3.在Event不是Down的情况下。将点击事件直接分发给第一点中列表所涉及到的TouchTarget链表。

// Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                // 如果没有子View消费或者被ViewGroup拦截的情况
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    // 如果第一个步骤有子类View消费,那么alreadyDispatchedToNewTouchTarget为TRUE
                    // target == newTouchTarget,第一次触摸的View和新的触摸View 为同一个View.
                    // 同时满足这两个条件,也就是说之是事件类型是down的情况
                    if (alreadyDispatchedToNewTouchTarget是 && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        //如果ViewGroup拦截的话cancelChild会变成true,也就是会被子View当成cancel事件
                        //如果没有被拦截的话,就是说碰到DOWN以外的事件类型的话。直接把事件传给对应的View。
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

接下来看看dispatchTransformedTouchEvent里面是怎么实现的:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
    。。。
 }

很显然如果child为null的时候,表示没有子类拦截。那么就调用ViewGroup的dispatchTouchEvent方法。还有一点就是cancel这个标志位。上面说了,如果事件被ViewGroup拦截,那么cancel为true,所以会执行event.setAction(MotionEvent.ACTION_CANCEL)。也就是说该事件会被当成cancel事件传入子View。

1.7 View

第一步分析dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent event) {
       	。。。

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        。。。

        return result;
    }

public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }

首先会判断mOnTouchListener.onTouch,判断返回如果为true。如果为true的话表示被消费了,那么接下来if (!result && onTouchEvent(event))就不会执行到onTouchEvent,也就是说触摸事件的优先级是onTouch > onTouchEvent.

接下来看看onTouchEvent里是怎么实现的:

public boolean onTouchEvent(MotionEvent event) {
    switch (action) {
                case MotionEvent.ACTION_UP:
            		。。。
                    if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
            		。。。
                    break;

                case MotionEvent.ACTION_DOWN:
                    。。。
                    break;

                case MotionEvent.ACTION_CANCEL:
                    。。。
                    break;

                case MotionEvent.ACTION_MOVE:break;
            }

            return true;
        }

        return false;
}

public boolean performClick() {
        。。。
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
		。。。

        return result;
    }

在ACTION_UP的时候会调用post(mPerformClick)方法,该方法最终会调用到li.mOnClickListener.onClick方法。

所以总结下调用的优先级流程是 onTouch > onTouchEvent > onClick。

2 滑动冲突的分类与解决

滑动冲突分为两种:

  1. 外部和内部的滑动方向一致

  2. 外部和内部的滑动方向不一致

滑动冲突本质上是外部和内部的滑动各管各的,而实际的交互是内部外部要有个统一的交互。所以具体的解决方法应该是将交互统一个外部或者内部来处理。也就是说具体有两种方式:

  1. 交给外部统一改动,以下是伪代码:

    //默认的情况是false,也就是父类不会去拦截
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int)event.getX();
        int y = (int)event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
               break;
           }
           case MotionEvent.ACTION_MOVE: {
               if (需要外部处理的条件) {
                    intercepted = true;
               } else {
                    intercepted = false;
               }
               break;
           }
           case MotionEvent.ACTION_UP: {
               intercepted = false;
               break;
           }
           default:
               break;
           }
           return intercepted;
    }
    
  2. 交给内部统一改动,以下是伪代码:

public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
            	// 禁止parent拦截down事件
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (希望父类拦截的条件) { 
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }
        return super.dispatchTouchEvent(event);
    }

3. 参考资料

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值