Android事件分发机制(ViewGroup篇)

上一篇我们介绍了View的事件分发机制,不熟悉的可以先了解一下

上一篇:Android事件分发机制(View篇)

引言

本篇我们接着上一篇,来继续学习一下Android ViewGroup的事件分发机制

本来View的事件分发机制和ViewGroup的事件分发机制是紧密联系在一起的,但是因为其中的原理不是三两句能够说清楚的,也为了方便理解,就先拆开来讲,然后融合起来统一归纳总结,这样结构更清晰,好了,废话不多,我们进入正文。

正文

本篇ViewGroup的事件分发机制的场景和上一篇Android事件分发机制(View篇)开始的场景是一样的,这里不再重复。

上篇也提到过对于ViewGroup我们关注三个方法:

ViewGroup 三个方法:

  • dispatchTouchEvent (MotionEvent event)//负责事件分发
  • onInterCeptTouchEvent(MotionEvent event)//处理是否拦截当前事件
  • onTouchEvent(MotionEvent event)//当前View自己处理当前事件

我们先新建一个project: ViewGroupDemo

- 新建一个MyLinearLayout继承LinearLayout

重写dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent三个方法并打印log

/**
* @author Charay
* @data 2017/10/31
*/

public class MyLinearLayout extends LinearLayout {
private static final String TAG = MyLinearLayout.class.getSimpleName();

public MyLinearLayout(Context context) {
    super(context);
}

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

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            Log.e(TAG,"--MyLinearLayout--dispatchTouchEvent---ACTION_DOWN---");
            break;
        case MotionEvent.ACTION_MOVE:
            Log.e(TAG,"--MyLinearLayout--dispatchTouchEvent---ACTION_MOVE---");
            break;
        case MotionEvent.ACTION_UP:
            Log.e(TAG,"--MyLinearLayout--dispatchTouchEvent---ACTION_UP---");
            break;
    }
    return super.dispatchTouchEvent(ev);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_DOWN---");
            break;
        case MotionEvent.ACTION_MOVE:
            Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_MOVE---");
            break;
        case MotionEvent.ACTION_UP:
            Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_UP---");
            break;
    }
    return super.onInterceptTouchEvent(ev);
//        return true;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            Log.e(TAG,"--MyLinearLayout--onTouchEvent---ACTION_DOWN---");
            break;
        case MotionEvent.ACTION_MOVE:
            Log.e(TAG,"--MyLinearLayout--onTouchEvent---ACTION_MOVE---");
            break;
        case MotionEvent.ACTION_UP:
            Log.e(TAG,"--MyLinearLayout--onTouchEvent---ACTION_UP---");
            break;
    }
    return  super.onTouchEvent(event);
}
}

- 新建一个MyButton继承Button

重写dispatchTouchEvent、onTouchEvent两个方法并打印log

/**
* @author Charay
* @data 2017/10/31
*/

public class MyButton extends Button {
private static final String TAG = MyButton.class.getSimpleName();

public MyButton(Context context) {
    super(context);
}

public MyButton(Context context, AttributeSet attrs) {
    super(context, attrs);
}



@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            Log.e(TAG,"--MyButton--dispatchTouchEvent---ACTION_DOWN---");
            break;
        case MotionEvent.ACTION_MOVE:
            Log.e(TAG,"--MyButton--dispatchTouchEvent---ACTION_MOVE---");
            break;
        case MotionEvent.ACTION_UP:
            Log.e(TAG,"--MyButton--dispatchTouchEvent---ACTION_UP---");
            break;
    }
    return super.dispatchTouchEvent(ev);
}



@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            Log.e(TAG,"--MyButton--onTouchEvent---ACTION_DOWN---");
            break;
        case MotionEvent.ACTION_MOVE:
            Log.e(TAG,"--MyButton--onTouchEvent---ACTION_MOVE---");
            break;
        case MotionEvent.ACTION_UP:
            Log.e(TAG,"--MyButton--onTouchEvent---ACTION_UP---");
            break;
    }
    return  super.onTouchEvent(event);
}


}

在布局文件中添加 MyLinearLayoutMyButton

activity_main.xml




    

        
    

MainActivity中初始化 MyLinearLayoutMyButton并添加setOnTouchListener,然后打印log

public class MainActivity extends Activity {

private static final String TAG = MainActivity.class.getSimpleName();
private MyLinearLayout mMyLinearLayout;
private MyButton mMyButton;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mMyLinearLayout = (MyLinearLayout) findViewById(R.id.my_linearlayout);
    mMyButton = (MyButton) findViewById(R.id.my_button);


    mMyLinearLayout.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            switch (motionEvent.getAction()){
                case MotionEvent.ACTION_DOWN:
                    Log.e(TAG,"--MyLinearLayout--onTouch---ACTION_DOWN");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e(TAG,"--MyLinearLayout--onTouch---ACTION_MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e(TAG,"--MyLinearLayout--onTouch---ACTION_UP");
                    break;
            }


            return false;
        }
    });
    mMyButton.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            switch (motionEvent.getAction()){
                case MotionEvent.ACTION_DOWN:
                    Log.e(TAG,"--mMyButton--onTouch---ACTION_DOWN");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e(TAG,"--mMyButton--onTouch---ACTION_MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e(TAG,"--mMyButton--onTouch---ACTION_UP");
                    break;
            }
            return false;
        }
    });


}
}

为了能完整看到事件分发拦截的整个流程,我们在上面代码中没有更改任何一个方法的返回值,只是打印了log

现在我们运行程序,点击MyButton,打印日志log如下:

11-01 11:54:02.993 E/MyLinearLayout: --MyLinearLayout--dispatchTouchEvent---ACTION_DOWN---
11-01 11:54:02.993 E/MyLinearLayout: --MyLinearLayout--onInterceptTouchEvent---ACTION_DOWN---
11-01 11:54:02.993 E/MyButton: --MyButton--dispatchTouchEvent---ACTION_DOWN---
11-01 11:54:02.994 E/MainActivity: --mMyButton--onTouch---ACTION_DOWN
11-01 11:54:02.994 E/MyButton: --MyButton--onTouchEvent---ACTION_DOWN---

11-01 11:54:03.004 E/MyLinearLayout: --MyLinearLayout--dispatchTouchEvent---ACTION_MOVE---
11-01 11:54:03.004 E/MyLinearLayout: --MyLinearLayout--onInterceptTouchEvent---ACTION_MOVE---
11-01 11:54:03.004 E/MyButton: --MyButton--dispatchTouchEvent---ACTION_MOVE---
11-01 11:54:03.005 E/MainActivity: --mMyButton--onTouch---ACTION_MOVE
11-01 11:54:03.005 E/MyButton: --MyButton--onTouchEvent---ACTION_MOVE---

11-01 11:54:03.028 E/MyLinearLayout: --MyLinearLayout--dispatchTouchEvent---ACTION_UP---
11-01 11:54:03.028 E/MyLinearLayout: --MyLinearLayout--onInterceptTouchEvent---ACTION_UP---
11-01 11:54:03.028 E/MyButton: --MyButton--dispatchTouchEvent---ACTION_UP---
11-01 11:54:03.028 E/MainActivity: --mMyButton--onTouch---ACTION_UP
11-01 11:54:03.029 E/MyButton: --MyButton--onTouchEvent---ACTION_UP---

虽然日志比较长,但是不要怕,上面把ACTION_DOWN、ACTION_MOVE、ACTION_UP都打印了出来,
我们只需要看一组ACTION_DOWN即可,因为一个View一旦消费了ACTION_DOWN事件,那么其他两个事件一定都是这个View消费。

log中我们发现ACTION_DOWN、ACTION_MOVE、ACTION_UP三个手势动作规律是一样的,执行顺序都是从最外层ViewGroup向内层View(或ViewGroup传递):先是MyLinearLayoutdispatchTouchEvent在这个方法中先执行onInterceptTouchEvent判断事都拦截这个事件,如果不拦截(默认不拦截)就传递给MyButton,然后事件分发给MyButtondispatchTouchEvent,执行MyButtondispatchTouchEvent,由于MyButton及其父View没有onInterceptTouchEvent方法,所以直接在dispatchTouchEvent中先判断onTouch的返回值,默认为false,再执行MyButtononTouchEvent

如果我们在MyLinearLayout中拦截了这个事件结果将是怎样呢?

下面我们在MyLinearLayout中的重写的onInterceptTouchEvent中把返回值改为true

onInterceptTouchEvent

 @Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_DOWN---");
            break;
        case MotionEvent.ACTION_MOVE:
            Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_MOVE---");
            break;
        case MotionEvent.ACTION_UP:
            Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_UP---");
            break;
    }
//  return super.onInterceptTouchEvent(ev);
    return true;
}

然后运行程序分别点击MyButton和MyLinearLayout,发现打印的log是一样的:

//点击MyButton
11-02 11:07:14.207 E/MyLinearLayout: --MyLinearLayout--dispatchTouchEvent---ACTION_DOWN---
11-02 11:07:14.207 E/MyLinearLayout: --MyLinearLayout--onInterceptTouchEvent---ACTION_DOWN---
11-02 11:07:14.207 E/MainActivity: --MyLinearLayout--onTouch---ACTION_DOWN
11-02 11:07:14.207 E/MyLinearLayout: --MyLinearLayout--onTouchEvent---ACTION_DOWN---
//点击MyLinearLayout
11-02 11:07:21.667 E/MyLinearLayout: --MyLinearLayout--dispatchTouchEvent---ACTION_DOWN---
11-02 11:07:21.677 E/MyLinearLayout: --MyLinearLayout--onInterceptTouchEvent---ACTION_DOWN---
11-02 11:07:21.677 E/MainActivity: --MyLinearLayout--onTouch---ACTION_DOWN
11-02 11:07:21.677 E/MyLinearLayout: --MyLinearLayout--onTouchEvent---ACTION_DOWN---

一分钟思考一下下面两个问题:

  1. 为什么MyButton没有任何有关log,而且这次还执行了MyLinearLayout的onTouchonTouchEvent方法?
  2. 为什么我们把返回值改为true之前,只执行了MyLinearLayout的dispatchTouchEventonInterceptTouchEvent,而没有执行onTouchonTouchEvent方法?

下面我们来看一下 ViewGroup 中的源码(android-10,即2.3.3的源码)

dispatchTouchEvent

/**
 * {@inheritDoc}
 */
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (!onFilterTouchEventForSecurity(ev)) {
        return false;
    }

    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
        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 = 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;
    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);
}

代码比较多,我们抛开干扰,只看对我们有用的

先看这行boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

disallowIntercept的含义是禁用拦截事件,下面看 if (disallowIntercept || !onInterceptTouchEvent(ev))判断,
进入判断条件的情况有两种:

  1. disallowIntercepttrue,即禁用拦截事件,这时候即使拦截事件onInterceptTouchEvent(ev)返回值为true,也不会拦截
  2. disallowInterceptfalse,即允许拦截,但是不拦截onInterceptTouchEvent(ev)返回值为false

默认情况下是允许拦截的,即disallowInterceptfalse,只有当我们调用mMyLinearLayout.requestDisallowInterceptTouchEvent(true);的时候,即禁止拦截,disallowIntercept的值才为true.
进入到if判断中后,我们看

for (int i = count - 1; i >= 0; i--) {
     final View child = children[i];

        ......

        if (child.dispatchTouchEvent(ev))  {
              // Event handled, we have a target now.
              mMotionTarget = child;
              return true;
                        }
        ......
}

这时遍历子View,把事件传给子ViewdispatchTouchEvent,如果子ViewViewGroup,那就继续遍历,直到遍历到子ViewView类型的,然后调用调用这个子ViewdispatchTouchEvent,之后的判断就干我们上一篇的Android事件分发机制(View篇)原理一致了。

而我们刚才的代码中把onInterceptTouchEvent(ev)返回值改为true,则不符合上面的两种情况,因此进入不到if判断中,事件被拦截了,因此事件到不了MyButtondispatchTouchEvent方法,也就不会有MyButton的任何有log

下面我们看这几行代码:

// The event wasn't an ACTION_DOWN, dispatch it to our target if
    // we have one.
    final View target = mMotionTarget;
    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);
    }

虽然我们在onInterceptTouchEvent(ev)中返回true拦截了这个事件,但这并不代表我们当前MyLinearLayout就是消费当前事件的消费者,因为我们没有target,所以进入了上面代码中,最后一行return super.dispatchTouchEvent(ev);即进入了ViewdispatchTouchEvent(ev),同样跟我们上一篇Android View的事件分发机制(上)原理一致,虽然和上面for循环中一样也是在View中,但是是有区别的,这个是MyLinearLayout这个(ViewGroup)继承的View,其操作是针对MyLinearLayout的,即之后所走的onTouchonTouchEvent也都是针对MyLinearLayout的;而for循环中的是MyButton这个(View)继承的View,其操作是针对MyMutton的,即之后所走的onTouchonTouchEvent也都是针对MyBUtton的。

既然我们在onInterceptTouchEvent(ev)中返回true拦截了这个事件,拦截后,就走到了MyLinearLayout的onTouch方法,默认返回false,然后执行了MyLinearLayoutonTouchEvent方法,所以刚好符合我们上面的log

总结

下面我们总结一下ViewGroup的事件分发机制的关键点,以及和View的事件分发机制的异同

  • ViewGroup比View多了一个拦截事件的方法onInterceptTouchEvent(ev)

  • ViewGroup比View中最先执行的方法,都是dispatchTouchEvent方法,然后View执行了onTouch方法

  • 但是ViewGroup中在执行onTouch方法之前多了一个onInterceptTouchEvent(ev)判断,这个判断决定了事件在本ViewGroup中消费,还是将事件继续分发给它的子View.

  • 而View是没有onInterceptTouchEvent(ev)的,所以没有拦截,事件能不能在View中消费掉,关键是看这个View中的onTouch方法或者onTouchEvent的返回值,如果返回值都是false,那就会将事件返回给其父View的onTouch和onTouchEvent方法,直到找到一个能消费此次事件的ViewGroup。

好了关与Android中View和ViewGroup的事件分发机制两篇都讲完了,如果有不足或者不理解的地方可在评论中回复,我们共同进步。
最后附上ViewGroupDemo源码:github下载

上一篇:Android View的事件分发机制(上)

你可能感兴趣的:(Android事件分发机制(ViewGroup篇))