【自定义View系列】View的事件分发机制

来源:互联网 发布:mesos 大数据 编辑:程序博客网 时间:2024/05/29 19:35

  本部分介绍View的一个核心知识点:事件分发机制。事件分发机制不仅仅是核心知识点更是难点,不少初学者甚至中级开发者面对这个问题都会觉得困惑。另外,View的另一大难题滑动冲突,它的解决方法的理论基础就是事件分发机制,因此掌握好View的事件分发机制是十分重要的。

一.为什么需要事件分发机制

http://blog.csdn.net/aigestudio/article/details/44260301
http://blog.csdn.net/aigestudio/article/details/44746625

  aige已经写过两篇相关的博客,非常清晰的解释了为何有事件分发机制,这里就不再赘述了。其实,不止android,一切有界面的系统都有自己的事件分发机制,如windows系统、osx系统,ios系统等等。

二.事件分发机制的类比案例

  假设点击事件是一个难题,经理把这个难题分给组长去处理,组长又分给程序员处理,程序员解决不了,只能交给组长,组长来解决,组长解决不了,那只能继续往上交给经理,经理来解决。从这个角度看,事件分发机制还是很贴近现实的。

三.MotionEvent

常量:
public static final int ACTION_DOWN = 0;单点触摸按下动作
public static final int ACTION_UP = 1;单点触摸离开动作
public static final int ACTION_MOVE = 2;触摸点移动动作
public static final int ACTION_CANCEL = 3;触摸动作取消
ACTION_MASK = 0X000000ff 动作掩码 为了得到action

方法:
getAction() 返回值:int 使用掩码后可获得上面的对应常量值
getX(),getY() 相对当前view的坐标
getRawX(),getRawY() 相对屏幕的坐标

四.依照惯例,我们仍然从栈帧分析入手

  栈帧清晰的帮我们列出了方法的调用流程,是阅读源码非常主要的工具。

1.栈帧

这里写图片描述

布局:

<?xml version="1.0" encoding="utf-8"?><LinearLayout    xmlns:android="http://schemas.android.com/apk/res/android"    android:orientation="horizontal"    android:layout_width="match_parent"    android:layout_height="match_parent">    <com.ht.androidstudy.view.DebugTextView        android:id="@+id/debugView"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="dddd"        /></LinearLayout>

DebugTextView:

public class DebugTextView extends TextView {    public DebugTextView(Context context) {        super(context);    }    public DebugTextView(Context context, AttributeSet attrs) {        super(context, attrs);    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        Log.d("dd", "");        super.onMeasure(widthMeasureSpec, heightMeasureSpec);    }    @Override    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {        Log.d("dd", "");        super.onLayout(changed, left, top, right, bottom);    }    @Override    protected void onDraw(Canvas canvas) {        Log.d("dd", "");        super.onDraw(canvas);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        Log.d("dd", "");        return super.onTouchEvent(event);    }}

  onTouchEvent方法的 Log.d(“dd”, “”);加上断点debug后的栈帧如上图,我们抛开系统对UI的影响,只分析LinearLayout和DebugTextView。

2.Activity对点击事件的分发过程

  点击事件用MotionEvent来表示,当一个点击操作发生时,事件最先传递给当前Activity,由Activity的dispatchtouchevent来进行事件派发,具体的工作是由Activity内部的Window完成的。Window会将事件传递给DecorView,DecorView一般就是当前界面的底层容器,即SetContentView所设置的View的父容器),通过Activity.getWindow().getDecorView()可以获得。我们从Activity的dispatchtouchevent开始分析。源码如下:

    public boolean dispatchTouchEvent(MotionEvent ev) {        if (ev.getAction() == MotionEvent.ACTION_DOWN) {            onUserInteraction();        }        // 主要是走的这个分支,getWindow就是PhoneWindow的一个对象        // PhoneWindow里的superDispatchTouchEvent走的是        // mDecor.superDispatchTouchEvent(event);        // 所以最终走到了mDecor的superDispatchTouchEvent方法        if (getWindow().superDispatchTouchEvent(ev)) {            return true;        }        return onTouchEvent(ev);    }

mDecor的superDispatchTouchEvent源码如下:

        public boolean superDispatchTouchEvent(MotionEvent event) {            return super.dispatchTouchEvent(event);        }

因为mDecor继承自FrameLayout,所以走的是FrameLayout的dispatchTouchEvent方法。

3.顶级View(即LinearLayout)对点击事件的分发过程

我们跳过mDecor的分发看LinearLayout
LinearLayout的dispatchTouchEvent源码如下:

   @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        final int action = ev.getAction();        final float xf = ev.getX();        final float yf = ev.getY();        final float scrolledXFloat = xf + mScrollX;        final float scrolledYFloat = yf + mScrollY;        final Rect frame = mTempRect;        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;        if (action == MotionEvent.ACTION_DOWN) {            if (mMotionTarget != null) {                // this is weird, we got a pen down, but we thought it was                // already down!                // XXX: We should probably send an ACTION_UP to the current                // target.                mMotionTarget = null;            }            // If we're disallowing intercept or if we're allowing and we didn't            // intercept            // 首先走onInterceptTouchEvent方法,判断是否拦截            // 不拦截的话,就会进入if里面,做事件分发,主要是调用子View的dispatchTouchEvent            if (disallowIntercept || !onInterceptTouchEvent(ev)) {                // reset this event's action (just to protect ourselves)                ev.setAction(MotionEvent.ACTION_DOWN);                // We know we want to dispatch the event down, find a child                // who can handle it, start with the front-most child.                final int scrolledXInt = (int) scrolledXFloat;                final int scrolledYInt = (int) scrolledYFloat;                final View[] children = mChildren;                final int count = mChildrenCount;                for (int i = count - 1; i >= 0; i--) {                    final View child = children[i];                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE                            || child.getAnimation() != null) {                        child.getHitRect(frame);                        // 点击事件的坐标是否落在子元素的区域内                        if (frame.contains(scrolledXInt, scrolledYInt)) {                            // offset the event to the view's coordinate system                            final float xc = scrolledXFloat - child.mLeft;                            final float yc = scrolledYFloat - child.mTop;                            ev.setLocation(xc, yc);                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;                            if (child.dispatchTouchEvent(ev))  {                                // Event handled, we have a target now.                                // mMotionTarget什么情况下会被赋值,并终止这个循环?                                // 如果子元素的dispatchTouchEvent方法返回true                                // 那么mMotionTarget就会被赋值,并跳出for循环                                mMotionTarget = child;                                return true;                            }                            // The event didn't get handled, try the next view.                            // Don't reset the event's location, it's not                            // necessary here.                        }                    }                }            }        }        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||                (action == MotionEvent.ACTION_CANCEL);        if (isUpOrCancel) {            // Note, we've already copied the previous state to our local            // variable, so this takes effect on the next event            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;        }        // The event wasn't an ACTION_DOWN, dispatch it to our target if        // we have one.        final View target = mMotionTarget;        // 如果遍历所有的子元素后事件都没有被合适的处理,这包含了两种情况:        // 1. ViewGroup没有子元素        // 2. 子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是        // 因为子元素在onTouchEvent中返回了false。        // 这两种情况下就会走View的dispatchTouchEvent方法,然后自己来处理        if (target == null) {            // We don't have a target, this means we're handling the            // event as a regular view.            ev.setLocation(xf, yf);            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {                ev.setAction(MotionEvent.ACTION_CANCEL);                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;            }            return super.dispatchTouchEvent(ev);        }        // if have a target, see if we're allowed to and want to intercept its        // events        if (!disallowIntercept && onInterceptTouchEvent(ev)) {            final float xc = scrolledXFloat - (float) target.mLeft;            final float yc = scrolledYFloat - (float) target.mTop;            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;            ev.setAction(MotionEvent.ACTION_CANCEL);            ev.setLocation(xc, yc);            if (!target.dispatchTouchEvent(ev)) {                // target didn't handle ACTION_CANCEL. not much we can do                // but they should have.            }            // clear the target            mMotionTarget = null;            // Don't dispatch this event to our own view, because we already            // saw it when intercepting; we just want to give the following            // event to the normal onTouchEvent().            return true;        }        if (isUpOrCancel) {            mMotionTarget = null;        }        // finally offset the event to the target's coordinate system and        // dispatch the event.        final float xc = scrolledXFloat - (float) target.mLeft;        final float yc = scrolledYFloat - (float) target.mTop;        ev.setLocation(xc, yc);        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {            ev.setAction(MotionEvent.ACTION_CANCEL);            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;            mMotionTarget = null;        }        return target.dispatchTouchEvent(ev);    }

具体的解释看代码注释即可。

3.LinearLayout分发就到了DebugTextView的dispatchTouchEvent方法

走的是View的dispatchTouchEvent方法

    public boolean dispatchTouchEvent(MotionEvent event) {        // View对点击事件的处理过程就比较简单了,因为View是一个单独的元素,他没有子元素        // 因此无法向下传递事件,所以只能自己处理事件。        // 从下面可见,View对点击事件的处理过程,它会首先判断有没有设置onTouchListener,        // 如果onTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见        // onTouchListener中的onTouch方法优先级高于onTouchEvent。        // 这样做的好处是方便我们在外界处理点击事件,就像自定义view总会留出setxxx接口一样        // 其实这里是同样的道理        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&                mOnTouchListener.onTouch(this, event)) {            return true;        }        return onTouchEvent(event);    }

4.继续看DebugTextView onTouchEvent方法

 public boolean onTouchEvent(MotionEvent event) {        final int viewFlags = mViewFlags;        if ((viewFlags & ENABLED_MASK) == DISABLED) {            // A disabled view that is clickable still consumes the touch            // events, it just doesn't respond to them.            return (((viewFlags & CLICKABLE) == CLICKABLE ||                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));        }        if (mTouchDelegate != null) {            if (mTouchDelegate.onTouchEvent(event)) {                return true;            }        }        // 从这里可以看出,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗        // 这个事件,即onTouchEvent返回true        if (((viewFlags & CLICKABLE) == CLICKABLE ||                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {            // 这里是ontouchevent中点击事件的具体处理            switch (event.getAction()) {                case MotionEvent.ACTION_UP:                    boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;                    if ((mPrivateFlags & PRESSED) != 0 || prepressed) {                        // take focus if we don't have it already and we should in                        // touch mode.                        boolean focusTaken = false;                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {                            focusTaken = requestFocus();                        }                        if (!mHasPerformedLongPress) {                            // This is a tap, so remove the longpress check                            removeLongPressCallback();                            // Only perform take click actions if we were in the pressed state                            if (!focusTaken) {                                // Use a Runnable and post this rather than calling                                // performClick directly. This lets other visual state                                // of the view update before click actions start.                                if (mPerformClick == null) {                                    mPerformClick = new PerformClick();                                }                                if (!post(mPerformClick)) {                                    // 当ACTION_UP事件发生时,会触发performClick方法,如果                                    // View设置了onClickLinstener,那么performClick方法内部会调用                                    // 它的onClick方法                                    performClick();                                }                            }                        }                        if (mUnsetPressedState == null) {                            mUnsetPressedState = new UnsetPressedState();                        }                        if (prepressed) {                            mPrivateFlags |= PRESSED;                            refreshDrawableState();                            postDelayed(mUnsetPressedState,                                    ViewConfiguration.getPressedStateDuration());                        } else if (!post(mUnsetPressedState)) {                            // If the post failed, unpress right now                            mUnsetPressedState.run();                        }                        removeTapCallback();                    }                    break;                case MotionEvent.ACTION_DOWN:                    if (mPendingCheckForTap == null) {                        mPendingCheckForTap = new CheckForTap();                    }                    mPrivateFlags |= PREPRESSED;                    mHasPerformedLongPress = false;                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());                    break;                case MotionEvent.ACTION_CANCEL:                    mPrivateFlags &= ~PRESSED;                    refreshDrawableState();                    removeTapCallback();                    break;                case MotionEvent.ACTION_MOVE:                    final int x = (int) event.getX();                    final int y = (int) event.getY();                    // Be lenient about moving outside of buttons                    int slop = mTouchSlop;                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||                            (y < 0 - slop) || (y >= getHeight() + slop)) {                        // Outside button                        removeTapCallback();                        if ((mPrivateFlags & PRESSED) != 0) {                            // Remove any future long press/tap checks                            removeLongPressCallback();                            // Need to switch from pressed to not pressed                            mPrivateFlags &= ~PRESSED;                            refreshDrawableState();                        }                    }                    break;            }            return true;        }        return false;    }

至此,对这个栈帧的分析已经结束,下面会做提取结论,做以总结。

五.总结

1.隧道式向下分发,然后冒泡式向上处理。

  当一个点击事件发生的时候,它的传递过程遵循如下顺序:Activity-Window-ViewGroup-View。
  处理按照是否消费的返回值,从下到上返回,即如果View的onTouchEvent返回false,将会向上传给它的parent的ViewGroup,如果ViewGroup的onTouchEvent也返回false,,将会一直向上返回到Activity,即activity的onTouchEvent方法会被调用。
  activity和window的拦截方法我们一般不修改,因为不具备应用价值。

2.ViewGRoup的事件传递机制

1)dispatchTouchEvent

 @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        final int action = ev.getAction();        final float xf = ev.getX();        final float yf = ev.getY();        final float scrolledXFloat = xf + mScrollX;        final float scrolledYFloat = yf + mScrollY;        final Rect frame = mTempRect;        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;        if (action == MotionEvent.ACTION_DOWN) {            if (mMotionTarget != null) {                // this is weird, we got a pen down, but we thought it was                // already down!                // XXX: We should probably send an ACTION_UP to the current                // target.                mMotionTarget = null;            }            // If we're disallowing intercept or if we're allowing and we didn't            // intercept            // 首先走onInterceptTouchEvent方法,判断是否拦截            // 不拦截的话,就会进入if里面,做事件分发,主要是调用子View的dispatchTouchEvent            if (disallowIntercept || !onInterceptTouchEvent(ev)) {                // reset this event's action (just to protect ourselves)                ev.setAction(MotionEvent.ACTION_DOWN);                // We know we want to dispatch the event down, find a child                // who can handle it, start with the front-most child.                final int scrolledXInt = (int) scrolledXFloat;                final int scrolledYInt = (int) scrolledYFloat;                final View[] children = mChildren;                final int count = mChildrenCount;                for (int i = count - 1; i >= 0; i--) {                    final View child = children[i];                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE                            || child.getAnimation() != null) {                        child.getHitRect(frame);                        // 点击事件的坐标是否落在子元素的区域内                        if (frame.contains(scrolledXInt, scrolledYInt)) {                            // offset the event to the view's coordinate system                            final float xc = scrolledXFloat - child.mLeft;                            final float yc = scrolledYFloat - child.mTop;                            ev.setLocation(xc, yc);                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;                            if (child.dispatchTouchEvent(ev))  {                                // Event handled, we have a target now.                                // mMotionTarget什么情况下会被赋值,并终止这个循环?                                // 如果子元素的dispatchTouchEvent方法返回true                                // 那么mMotionTarget就会被赋值,并跳出for循环                                mMotionTarget = child;                                return true;                            }                            // The event didn't get handled, try the next view.                            // Don't reset the event's location, it's not                            // necessary here.                        }                    }                }            }        }        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||                (action == MotionEvent.ACTION_CANCEL);        if (isUpOrCancel) {            // Note, we've already copied the previous state to our local            // variable, so this takes effect on the next event            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;        }        // The event wasn't an ACTION_DOWN, dispatch it to our target if        // we have one.        final View target = mMotionTarget;        // 如果遍历所有的子元素后事件都没有被合适的处理,这包含了两种情况:        // 1. ViewGroup没有子元素        // 2. 子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是        // 因为子元素在onTouchEvent中返回了false。        // 这两种情况下就会走View的dispatchTouchEvent方法,然后自己来处理        if (target == null) {            // We don't have a target, this means we're handling the            // event as a regular view.            ev.setLocation(xf, yf);            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {                ev.setAction(MotionEvent.ACTION_CANCEL);                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;            }            return super.dispatchTouchEvent(ev);        }        // if have a target, see if we're allowed to and want to intercept its        // events        if (!disallowIntercept && onInterceptTouchEvent(ev)) {            final float xc = scrolledXFloat - (float) target.mLeft;            final float yc = scrolledYFloat - (float) target.mTop;            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;            ev.setAction(MotionEvent.ACTION_CANCEL);            ev.setLocation(xc, yc);            if (!target.dispatchTouchEvent(ev)) {                // target didn't handle ACTION_CANCEL. not much we can do                // but they should have.            }            // clear the target            mMotionTarget = null;            // Don't dispatch this event to our own view, because we already            // saw it when intercepting; we just want to give the following            // event to the normal onTouchEvent().            return true;        }        if (isUpOrCancel) {            mMotionTarget = null;        }        // finally offset the event to the target's coordinate system and        // dispatch the event.        final float xc = scrolledXFloat - (float) target.mLeft;        final float yc = scrolledYFloat - (float) target.mTop;        ev.setLocation(xc, yc);        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {            ev.setAction(MotionEvent.ACTION_CANCEL);            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;            mMotionTarget = null;        }        return target.dispatchTouchEvent(ev);    }    /**     * {@inheritDoc}     */    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {            // We're already in this state, assume our ancestors are too            return;        }        if (disallowIntercept) {            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;        } else {            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;        }        // Pass it up to our parent        if (mParent != null) {            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);        }    }

2)onInterceptTouchEvent:是否拦截

如果父布局在拦截方法中重写了down\move\up这些事件的拦截,那么每次都会走down\move\up的拦截,然后才会走ontouchevent。

3)onTouchEvent

采用的是View的onEvent方法。长按事件的响应,点击事件的响应都在这个方法中。

3.View的事件传递机制

1)dispatchTouchEvent

    public boolean dispatchTouchEvent(MotionEvent event) {        // View对点击事件的处理过程就比较简单了,因为View是一个单独的元素,他没有子元素        // 因此无法向下传递事件,所以只能自己处理事件。        // 从下面可见,View对点击事件的处理过程,它会首先判断有没有设置onTouchListener,        // 如果onTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见        // onTouchListener中的onTouch方法优先级高于onTouchEvent。        // 这样做的好处是方便我们在外界处理点击事件,就像自定义view总会留出setxxx接口一样        // 其实这里是同样的道理        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&                mOnTouchListener.onTouch(this, event)) {            return true;        }        return onTouchEvent(event);    }

可以看到dispatchTouchEvent方法的返回值是受onInterceptTouchEvent和onTouchEvent影响的,并不是彼此不相干的。

2)无这个方法onInterceptTouchEvent

3)onTouchEvent,上面已经分析过

长按事件的响应,点击事件的响应都在这个方法中。

4.处理滑动冲突

1)外部拦截法

重写ViewGroup的onInterceptTouchEvent方法.
在onDown不拦截(否则无法再向下传递),需要拦截其他事件时返回true
伪代码:

public boolean onInterceptTouchEvent(MotionEvent ev){boolean intercepted=false;switch(ev.getAction()){case MotionEvent.ACTION_DOWN:intercepted=false;break;case MotionEvent.ACTION_MOVE:if(xxx) intercepted=true;  //拦截else intercepted=false; //不拦截break;case MotionEvent.ACTION_UP:intercepted=false;break;}return intercepted;}

2)内部拦截法.

在view中控制viewGroup来实现,view通过改变FLAG_DISALLOW_INTERCEPT的状态来控制ViewGroup是否拦截后续事件
伪代码:

public boolean dispatchTouchEvent(MotionEvent event){    switch(event.getAction()){        case MotionEvent.ACTION_DOWN:            parent.requestDisallowInterceptTouchEvent(true);            break;        case MotionEvent.ACTION_MOVE:            if(需要上级拦截){                parent.requestDisallowInterceptTouchEvent(false);            }            break;        default:            break;    }    return super.dispatchTouchEvent(event);}

5.涉及类和方法的类图结构

6.应用场合

  • 自定义view
    一般也是改变返回值,具体的代码实现几乎不会改动

  • 滑动冲突(事件分发机制是它的理论基础)
    产生场合
    如何解决

0 0
原创粉丝点击