Android系统之View事件分发机制

我们对屏幕的点击,滑动,抬起等一系的动作都会被封装成若干个MotionEvent对象。MotionEvent可以分为以下三种事件类型:
a. ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件
b. ACTION_MOVE:手指在屏幕上移动时候产生该事件
c. ACTION_UP:手指从屏幕上松开的瞬间产生该事件
Android系统时如何将用户的输入事件MotionEvent分发给具体的View进行处理呢?

1. Activity分发输入事件

Activity.java (frameworks\base\core\java\android\app)	321500	2017/8/25
public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2,
        Window.OnWindowDismissedCallback, WindowControllerCallback,
        AutofillManager.AutofillClient {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
}

getWindow()获得的是PhoneWindow,首先将输入事件分发到PhoneWindow处理,如果PhoneWindow调用superDispatchTouchEvent返回true则表示事件已经被处理了则直接返回true;如果返回false则调用Activity的onTouchEvent处理事件
即事件先分发给PhoneWindow处理,如果PhoneWindow没有处理事件,则会由Activity处理事件

PhoneWindow.java (frameworks\base\core\java\com\android\internal\policy)	143971	2017/8/25
public class PhoneWindow extends Window implements MenuBuilder.Callback {
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
}

PhoneWindow会将输入事件分发给DecoreView处理

DecorView.java (frameworks\base\core\java\com\android\internal\policy)	107205	2017/8/25
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
}

ViewGroup.java (frameworks\base\core\java\android\view)	327264	2017/8/25
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {	//清除之前的View事件
                // 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);		//如果输入事件的事件类型是ACTION_DOWN,则会重置FLAG_DISALLOW_INTERCEPT标志位并且将mFirstTouchTarget置为null
                resetTouchState();
            }

            // Check for interception.
            final boolean intercepted;		//是否拦截事件的标志位
            if (actionMasked == MotionEvent.ACTION_DOWN		//如果事件由子View处理(mFirstTouchTarget指向处理View事件的子View,如果不为空表示此事件由子View处理)
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;		//判断是否允许当前的ViewGroup拦截事件
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);		//如果没有设置标志位FLAG_DISALLOW_INTERCEPT,表示ViewGroup允许拦截事件,则调用onInterceptTouchEvent判断是否需要拦截
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;	//如果设置了标志位FLAG_DISALLOW_INTERCEPT,表示ViewGroup不允许拦截事件,则将intercepted置为false,表示当前的ViewGroup没有拦截这个View事件
                }
            } else {	//如果事件不由子View处理,则表示事件由当前的ViewGroup处理
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
			...
		}
	}
}

结论:
a. 子View可以通过requestDisallowInterceptTouchEvent方法设置FLAG_DISALLOW_INTERCEPT标志位不让父View拦截除ACTION_DOWN以外的事件
b. intercepted为true时输入事件由父View处理,intercepted为false时输入事件由子View处理
如果输入事件为ACTION_DOWN,则在执行cancelAndClearTouchTargets的时候FLAG_DISALLOW_INTERCEPT标志位会被重置,故FLAG_DISALLOW_INTERCEPT标志位不可干预ACTION_DOWN事件
如果设置FLAG_DISALLOW_INTERCEPT为true表示父View不会拦截事件(输入事件由子View处理),否则FLAG_DISALLOW_INTERCEPT为false时父View会调用onInterceptTouchEvent判断是否需要拦截事件(如果父View会拦截输入事件则输入事件由父View处理,否则由子View处理)

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    public boolean dispatchTouchEvent(MotionEvent ev) {
            if (!canceled && !intercepted) {
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    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();
						//从前到后对子View进行遍历,查找子View处理事件
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(		//child即为当前需要判断是否可以接收此事件的View
                                    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;
                            }
							//判断1. View可见并且没有播放动画。2. 点击事件的坐标落在View的范围内
							//如果上述两个条件有一项不满足则continue继续循环下一个View
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
							//如果有子View处理即newTouchTarget不为null则跳出循环。
                            newTouchTarget = getTouchTarget(child);		//当事件还未分发到子View时,getTouchTarget返回null
                            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);
							//dispatchTransformedTouchEvent第三个参数child这里不为null
							//child不为null,因此调用的是child的dispatchTouchEvent方法,child.dispatchTouchEvent返回true表示子View处理了事件;返回false表示子View没有处理事件,进入下一次循环判断下一个子View
                            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();
								//当child处理点击事件,那么在addTouchTarget设置newTouchTarget = mFirstTouchTarget = child = 处理此事件的View
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
								//子View处理了事件,然后就跳出了for循环
                                break;
                            }
                        }
                    }
                }
            }
        }
        return handled;
    }
}

如果intercepted为false表示输入事件由子View处理,继续分发输入事件

分析getTouchTarget

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    private TouchTarget getTouchTarget(@NonNull View child) {
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            if (target.child == child) {
                return target;
            }
        }
        return null;
    }
}

当mFirstTouchTarget不为null的时候并且target.child就为我们当前遍历的child的时候,那么返回的newTouchTarget就不为null,则跳出循环。由于此时事件还未确定分发到哪个子View进行处理,所以此时的getTouchTarget返回null

分析dispatchTransformedTouchEvent

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }
            handled = child.dispatchTouchEvent(transformedEvent);	//传入的child为需要判断是否可以处理事件的子View,不为null。如果child可以处理事件则dispatchTouchEvent返回true,进而dispatchTransformedTouchEvent返回true
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
}

dispatchTransformedTouchEvent传入的child为需要判断是否可以处理事件的子View,不为null。如果child可以处理事件则child.dispatchTouchEvent返回true,进而dispatchTransformedTouchEvent返回true
dispatchTransformedTouchEvent如果返回false表示事件没有分发给当前子View,会继续遍历下一个子View;如果返回true表示事件分发给了当前子View,然后调用addTouchTarget将当前子View赋值给newTouchTarget,最后break循环
newTouchTarget = mFirstTouchTarget = 处理此事件的子View,此时找到了处理此事件的View

结论:
ViewGroup会遍历所有子View去寻找能够处理点击事件的子View,判断子View能否处理此事件的前提:没有播放动画,点击事件坐标落在子View内部
满足以上两个前提之后调用子View的dispatchTouchEvent方法处理事件,如果返回false表示子View没有处理掉此事件,进入下一次循环判断下一个子View是否能够处理此事件;如果返回true表示子View处理了此事件
如果ViewGroup并没有子View或者子View的dispatchTouchEvent返回了false,那么ViewGroup会去处理这个事件,即会调用ViewGroup的dispatchTouchEvent处理事件

在这里插入图片描述
在这里插入图片描述

View事件传递过程总结:
a. Activity首先会将输入事件交给PhoneWindow处理,如果PhoneWindow处理不了Activity则会调用onTouchEvent处理
b. PhoneWindow会将输入事件分发给DecoreView处理,DecoreView会调用dispatchTouchEvent将事件分配给对应的View
c. 父View调用dispatchTouchEvent时,子View中可以设置标志位FLAG_DISALLOW_INTERCEPT处理滑动冲突。当子View设置了此标志位,则intercepted会被置为false;否则父View调用onInterceptTouchEvent判断当前是否需要拦截事件,拦截时intercepted置为true,否则置为false
d. 当标志位intercepted为true时,输入事件由父View处理,调用ViewGroup的dispatchTouchEvent处理事件
e. 当标志位intercepted为false时,ViewGroup会遍历所有子View去寻找能够处理点击事件的子View,判断子View能否处理此事件的前提:没有播放动画,点击事件坐标落在子View内部
以上两个前提之后调用子View的dispatchTouchEvent方法处理事件,如果返回false表示子View没有处理掉此事件,进入下一次循环判断下一个子View是否能够处理此事件;如果返回true表示子View处理了此事件

2. View处理输入事件

View.java (frameworks\base\core\java\android\view)	1045455	2017/8/25
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    public boolean dispatchTouchEvent(MotionEvent event) {
		//按下屏幕时actioin = ACTION_DOWN,此时会调用stopNestedScroll清除滚动效果
        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {		//如果窗口没有被遮盖
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
			//需要特别注意这个判断当中的li.mOnTouchListener.onTouch(this, event)条件
            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;
    }
}

View在调用dispatchTouchEvent处理事件时首先会判断View是否设置了OnTouchListener,如果View设置了OnTouchListener则会调用OnTouchListener中的onTouch处理事件;如果没有设置OnTouchListener则会调用View的onTouchEvent处理事件
首先来看如果没有设置OnTouchListener的情况

View.java (frameworks\base\core\java\android\view)	1045455	2017/8/25
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
		//如果View是设置成不可用的(DISABLED)仍然会消费点击事件,只是不会做出任何响应
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
		//只要进入此if则会返回true表示消费掉了这个事件,因此CLICKABLE和LONG_CLICKABLE只要有一个为true就消费这个事件;否则返回false表示没有消费掉此事件
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_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();
                        }
						//让View显示被点击的状态
                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // 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();
                                }
								//调用performClick处理事件
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }
                    }
                    break;

                case MotionEvent.ACTION_DOWN:
					//点击事件一般在ACTION_UP中完成,ACTION_DOWN中基本上不做响应
                    break;

                case MotionEvent.ACTION_CANCEL:
					...
                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }
					//如果手指移动出了View则直接break
                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }
            return true;
        }

        return false;
    }
}

View的onTouchEvent方法默认都会消费掉事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)

View.java (frameworks\base\core\java\android\view)	1045455	2017/8/25
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        return result;
    }
}

View在onTouchEvent处理ACTION_UP事件时会调用performClick,在performClick中会判断View有没有设置OnClickListener。
如果View设置了OnClickListener则会调用OnClickListener中的onClick方法,并返回true;如果没有设置OnClickListener则会返回false。无论返回true或者false都处理了此事件,onTouchEvent都返回true。

示例:

public class MotionEventActivity extends BaseActivity {
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_motion_event);
        mButton = (Button) findViewById(R.id.button);
        mButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        e("MotionEvent: ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        e("MotionEvent: ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        e("MotionEvent: ACTION_UP");
                        break;
                }
                return false;
            }
        });
    }
}

这里给View设置了OnTouchListener,因此点击Button的时候会调用onTouch方法

View事件处理过程总结:
a. View在调用dispatchTouchEvent处理事件时首先会判断View是否设置了OnTouchListener
如果View设置了OnTouchListener则会调用OnTouchListener中的onTouch处理事件;如果没有设置OnTouchListener则会调用View的onTouchEvent处理事件
b. View在onTouchEvent处理ACTION_UP事件时会调用performClick,在performClick中会判断View有没有设置OnClickListener
如果View设置了OnClickListener则会调用OnClickListener中的onClick方法,并返回true;如果没有设置OnClickListener则会返回false。无论返回true或者false都处理了此事件,onTouchEvent都返回true
优先级:onTouch > onTouchEvent > onClick
c. 如果View是unclickable则View不会处理事件,否则View是clickable或者longClickable都会处理事件
setClickable失效的情况:将View的clickable设置成false,然后又给View设置了OnClickListener(原因:setOnClickListener会默认将View的clickable设置成true)

好文:
https://www.jianshu.com/p/238d1b753e64

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android 中,如果想要实现 View 穿透效果,可以使用以下两种方式: 1. 使用 WindowManager 可以使用 WindowManager 来添加一个可以穿透的 View,将其添加到 WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY 类型的窗口中,并设置 WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,这样就可以让该 View 穿透其他 View 响应触摸事件。 示例代码: ``` // 创建一个可以穿透的 View View view = new View(context); view.setBackgroundColor(Color.TRANSPARENT); WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, PixelFormat.TRANSPARENT); WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); windowManager.addView(view, layoutParams); ``` 2. 使用 ViewGroup 在布局文件中使用一个继承自 ViewGroup 的自定义 View,重写 dispatchTouchEvent 方法,将事件分发给子 View 和自身,这样就可以实现子 View 穿透效果。 示例代码: ``` public class TouchThroughViewGroup extends ViewGroup { public TouchThroughViewGroup(Context context) { super(context); } public TouchThroughViewGroup(Context context, AttributeSet attrs) { super(context, attrs); } public TouchThroughViewGroup(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (child.getVisibility() == View.VISIBLE) { child.dispatchTouchEvent(ev); } } super.dispatchTouchEvent(ev); return true; } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); child.layout(l, t, r, b); } } } ``` 在布局文件中使用该自定义 View: ``` <com.example.TouchThroughViewGroup android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="TextView"/> </com.example.TouchThroughViewGroup> ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值