Android View之事件分发

Android view的事件分发机制是开发中的一个重点,因此掌握它的真正意义是非常必要的。

一.什么是view的事件分发机制呢?

    将点击事件传递到具体某个view处理的整个过程,就叫做事件分发。

 

二.为什么要有事件分发呢?

   因为Android的view是树形结构的,view可能会重叠,当点击的地方多的时候,为了解决点击事件传递给谁的时候,就要用到事件分发了,因此也可以看出,事件分发采用的是责任链的设计模式。

 

三.事件分发的传递对象是哪些?

   主要是Activity->ViewGroup->View的传递过程。

 

四.事件分发中的三个重要方法?

   1.public boolean dispatchTouchEvent(MotionEvent ev) 

     事件传递方法,把事件依照顺序往下传递。

  2.public boolean onInterceptTouchEvent(MotionEvent ev)

     事件拦截方法,该方法是在事件分发的 dispatchTouchEvent 方法内部进行调用。是用来判断在触摸事件传递过程中,是否拦截某个  事件。如果方法true表示将时间拦截,交给当前view的onTouchEvent进行处理,如果返回false不拦截,继续往下传递。

  3.public boolean onTouchEvent(MotionEvent ev)

    事件响应方法,这是用来处理具体事件的,在dispatchTouchEvent中调用。

 

五.了解了事件分发的一些基本概念后,就开始根据事件传递对象的流程来一步步分析事件的分发过程:

     1.Activiy事件分发过程:

      首先按下一个页面的按钮后,事件第一个传递的对象就是Activity,

      因此先分析事件是怎么到达Activity又是怎么往下传递的?

      写个touch事件来看下调用栈:

 

我们知道Android采用handler的消息机制,消息存在messagequeue消息队列中,同过loope轮询的方式来调用消息,我们从调用栈中的最下面的红框中看到,当我们按下按钮时,nativePollOnce()会收到消息,并将事件发送给InputEventReceiverdispatchInputEvent()方法,然后继续往下传,流程是这样的:

nativePollOnce(收到消息)->InputEventReceiver(的dispatchInputEvent)-> ViewRootImpl(view的根节点)的WindowInputEventReceiver

->Activity的dispatchTouchEvent。

到达Activity,接下来分析,事件由activity到达view的过程:

 

这个过程我们从调用栈中就可以很清楚的看出来,流程是这样的:

Activity(dispatchTouchEvent)-> PhoneWindow(superDispatchTouchEvent) -> DecorView(superDispatchTouchEvent) -> ViewGroup(dispatchTouchEvent)

 

为什么中间会经过PhoneWindow和DecorView呢?

因为Activity中持有一个Window对象,Window中包含一个PhoneWindow实例,PhoneWindow中又持有一个DecorView对象。

2.ViewGroup事件分发过程:

我们通过一个例子来看下事件传递过程,以下是自定义的一个 ViewGroup:

public class CustomLinearLayout extends LinearLayout {
    private final String TAG = "CustomLinearLayout";

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

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

    public CustomLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

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

        return super.onTouchEvent(event);
    }

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

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

 使用这个自定义 ViewGroup:

<com.example.linwenbing.demo.CustomLinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    >
    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="50dp"
        android:text="点击"
        />

</com.example.linwenbing.demo.CustomLinearLayout>

点击button:

btn.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG,"onTouch ACTION_DOWN");
                break;
        }
        return false;
    }
});

 

看下log的打印:

 

根据log的打印我们知道,当我们点击button时首先调用ViewGroup的调用流程是:

 ViewGroup的dispatchTouchEvent->ViewGroup的onInterceptTouchEvent->子view的dispatchTouchEvent

现在我们在ViewGroup中对事件进行拦截,即在onInterceptTouchEvent方法中返回true:

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

 

这时看下log的打印:

调用流程:

ViewGroup的dispatchTouchEvent->ViewGroup的onInterceptTouchEvent->ViewGroup的onTouchEvent

说明拦截事件后,事件就不往子view中传递了,就在ViewGroup的onTouchEvent进行处理。这就很好解释了三个方法的作用:

dispatchTouchEvent:首先调用的方法,对事件进行分发

onInterceptTouchEvent:拦截事件,如果拦截返回true,并执行改VierGroup的onTouchEvent,如果返回false则传给下一个view.

onTouchEvent:对事件的处理

搞懂了流程接下来我们来看下源码,这里主要看部分重要的代码:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (mInputEventConsistencyVerifier != null) {

        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);

    }...

// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Throw away all previous state when starting a new touch gesture.
    // The framework may have dropped the up or cancel event for the previous gesture
    // due to an app switch, ANR, or some other state change.
    cancelAndClearTouchTargets(ev);
    resetTouchState();

}

首先我们看dispatchTouchEvent方法中,view按下时的这两个方法,因为ACTION_DOWN是事件的开始,所以cancelAndClearTouchTargets这个方法是进行初始化,而resetTouchState是重置触摸状态,全新开始。

接下来看:

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;

    }

 

这里判断是否拦截事件,这里intercepted = onInterceptTouchEvent(ev);用来敷值,onInterceptTouchEvent默认返回false,不拦截的,所以我们可以重写onInterceptTouchEvent来对事件进行拦截.

if (actionMasked == MotionEvent.ACTION_DOWN
        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
    final int actionIndex = ev.getActionIndex(); // always 0 for down
    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
            : TouchTarget.ALL_POINTER_IDS;

    // Clean up earlier touch targets for this pointer id in case they
    // have become out of sync.
    removePointersFromTouchTargets(idBitsToAssign);

    final int childrenCount = mChildrenCount;
    if (newTouchTarget == null && childrenCount != 0) {
        final float x = ev.getX(actionIndex);
        final float y = ev.getY(actionIndex);
        // Find a child that can receive the event.
        // Scan children from front to back.
        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
        final boolean customOrder = preorderedList == null
                && isChildrenDrawingOrderEnabled();
        final View[] children = mChildren;
        for (int i = childrenCount - 1; i >= 0; i--) {
            final int childIndex = getAndVerifyPreorderedIndex(
                    childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(
                    preorderedList, children, childIndex);

            // If there is a view that has accessibility focus we want it
            // to get the event first and if not handled we will perform a
            // normal dispatch. We may do a double iteration but this is
            // safer given the timeframe.
            if (childWithAccessibilityFocus != null) {
                if (childWithAccessibilityFocus != child) {
                    continue;
                }
                childWithAccessibilityFocus = null;
                i = childrenCount - 1;
            }

            if (!canViewReceivePointerEvents(child)
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                ev.setTargetAccessibilityFocus(false);
                continue;
            }

            newTouchTarget = getTouchTarget(child);
            if (newTouchTarget != null) {
                // Child is already receiving touch within its bounds.
                // Give it the new pointer in addition to the ones it is handling.
                newTouchTarget.pointerIdBits |= idBitsToAssign;
                break;
            }

            resetCancelNextUpFlag(child);
            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;
                }
                mLastTouchDownX = ev.getX();
                mLastTouchDownY = ev.getY();
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;

                break;

 

这一大段代码主要就是当ViewGroup不拦截事件时,把事件分发到子view,循环遍历完所有的子view后,如果点击事件都没有被消耗,ViewGroup就会自己处理点击事件,如下代码:

// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);

} else {

 

3.View事件分发过程:

 这里我们说的view就是最后一层的子view了,所以并不存在拦截不往下分发的方法,这里我们主要分析onTouch中处理事件的分发过程:

btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.e(TAG,"onClick");
    }
});

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

上面的代码我们看一下log的执行(这里点击按钮的时候有稍微移动下):

 

我们看到执行流程是:

ACTION_DOWN->ACTION_MOVE>ACTION_UP>onClick

可以看到ACTION_DOWN最开始执行onClick最后执行,如果我们在onTouch中返回true会发现onClick就不执行了。

这里源码不做具体分析,看源码的时候主要发现MotionEvent中有个MotionEvent.ACTION_CANCEL需要注意一下:

case MotionEvent.ACTION_CANCEL:
    if (clickable) {
        setPressed(false);
    }
    removeTapCallback();
    removeLongPressCallback();
    mInContextButtonPress = false;
    mHasPerformedLongPress = false;
    mIgnoreNextUpEvent = false;
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;

    break;

MotionEvent.ACTION_CANCEL官方分析:

当你的手指(或者其它)移动屏幕的时候会触发这个事件,比如当你的手指在屏幕上拖动一个listView或者一个ScrollView而不是去按上面的按钮时会触发这个事件。

 

 

一个解决冲突的重要方法使用说明:

requestDisallowInterceptTouchEvent方法:

requestDisallowInterceptTouchEvent方法用于影响父元素的事件拦截策略,requestDisallowInterceptTouchEvent(true),表示不允许父元素拦截事件,这样事件就会传递给子View。一般这个方法子View用的多,可以用来处理滑动冲突问题。

如:

(1)在子View的dispatchTouchEvent方法中,对于ACTION_DOWN事件,通过调用requestDisallowInterceptTouchEvent(true)默认不允许父布局拦截事件,这样后续事件都交给子View处理。

(2)在子View的dispatchTouchEvent方法中,对于ACTION_MOVE事件,默认是子View处理,在需要父布局处理时,调用requestDisallowInterceptTouchEvent(false)方法来让父布局拦截事件,交给父布局处理。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值