手写Android事件分发

Android事件分发原理搞清楚可以辅助我们解决很多实际项目中遇到的事件冲突等问题

1.进入正题之前,问大家几个事件相关的问题?

标签: dispatchTouchEvent()


Q1: Android点击事件传递规则是怎样的?(下面几步仔细阅读2遍,有助于加深对事件传递的理解)

A1:

Step1: 当点击事件产生后,会由 Activity 首先来处理

	/**
     * 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 (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        // 如果没有任何View处理触摸事件,则转交给Activity的onTouchEvent自己处理
        return onTouchEvent(ev);
    }

	/**
     * Called when a touch screen event was not handled by any of the views
     * under it.  This is most useful to process touch events that happen
     * outside of your window bounds, where there is no view to receive it.
     *
     * @param event The touch screen event being processed.
     *
     * @return Return true if you have consumed the event, false if you haven't.
     * The default implementation always returns false.
     */
    public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

Step2: 上述步骤1传递给 PhoneWindowgetWindow().superDispatchTouchEvent(ev),再传递给 DecorView,最后传递给顶层的ViewGroup

	// This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    ...
    ...

	// PhoneWindow 的 superDispatchTouchEvent 方法
 	@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

	// DecorView 的 superDispatchTouchEvent 方法
	// DecorView 继承 FrameLayout (FrameLayout extends ViewGroup)
	public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

Step3: 对于根 ViewGroup,点击事件首先传递给它的 dispatchTouchEvent() 方法
,如果该 ViewGrouponInterceptTouchEvent() 方法返回true,则表示它要拦截这个事件,这个事件就会交给它的 onTouchEvent() 方法处理。如果 onInterceptTouchEvent() 方法返回 false,则表示它不拦截这个事件,则这个事件会交给它的子元素的 dispatchTouchEvent() 来处理

	// ViewGroup 中 dispatchTouchEvent 方法
	@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
   			......
            // 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;
            }
            ......
            // 如果该 `ViewGroup` 的 `onInterceptTouchEvent()` 方法返回true,则表示它要拦截这个事件,这个事件就会交给它的 `onTouchEvent()` 方法处理
            if (!canceled && !intercepted) {
            	......
            }
 		}
        return handled;
    }

Step4: 依步骤3传递,如此反复下去。如果传递给底层的View,View是没有子View的不再需要拦截了,就会调用View的 dispatchTouchEvent()方法

	/**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;
 		......
        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;
            }
			// 我们平常设置的 setOnTouchListener方法优先级高于 onTouchEvent
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
		......
        return result;
    }

	/**
     * Interface definition for a callback to be invoked when a touch event is
     * dispatched to this view. The callback will be invoked before the touch
     * event is given to the view.
     */
    public interface OnTouchListener {
        /**
         * Called when a touch event is dispatched to a view. This allows listeners to
         * get a chance to respond before the target view.
         *
         * @param v The view the touch event has been dispatched to.
         * @param event The MotionEvent object containing full information about
         *        the event.
         * @return True if the listener has consumed the event, false otherwise.
         */
        boolean onTouch(View v, MotionEvent event);
    }

Step5: 一般情况下最终会调用View的 onTouchEvent() 方法

	// 1. 一般View 长按监听在 点击监听之前被触发

	// 2. 长按监听源代码是在点击按压超过500ms时,默认为长按事件

	// 3. 

	/**
     * Implement this method to handle touch screen motion events.
     * <p>
     * If this method is used to detect click actions, it is recommended that
     * the actions be performed by implementing and calling
     * {@link #performClick()}. This will ensure consistent system behavior,
     * including:
     * <ul>
     * <li>obeying click sound preferences
     * <li>dispatching OnClickListener calls
     * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
     * accessibility features are enabled
     * </ul>
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    public boolean onTouchEvent(MotionEvent event) {
		final float x = event.getX();
        final float y = event.getY();
        final int action = event.getAction();
		......
		switch (action) {
         case MotionEvent.ACTION_UP:
         	  // 抬起,判断是否处理点击事件
		      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();
                      }
                     if (!post(mPerformClick)) {
                         performClickInternal();
                      }
                  }
               }      
           break;
           case MotionEvent.ACTION_DOWN:
			// 按下,处理长按事件
			......
           break;
           case MotionEvent.ACTION_MOVE:
			// 	移动,检测触摸是否划出了控件区域,移除响应事件
           break;
           case MotionEvent.ACTION_CANCEL:
			//  
			......	
           break;
		}

	}


Step6: 接下来讲解点击事件由下而上的传递。当点击事件传给底层的 View 时,如果其 onTouchEvent() 方法返回 true,则事件由底层的View消耗并处理;如果返回false则表示该View不做处理,则传递给父View的 onTouchEvent() 处理;若父View的 onTouchEvent() 仍旧返回false,则继续传递给该父View的父View处理,如此反复下去
在这里插入图片描述


事件分发小结

1. 一个事件序列,从手指触摸屏幕到手指离开屏幕,在这个过程中,以DOWN事件开始,中间含有不定数的MOVE事件,以UP事件结束
2. 正常情况下,一个事件序列,只能被一个View拦截并且消耗
3. 某个View一旦决定拦截,那么这个事件序列都将由它的onTouchEvent处理,并且它的onInterceptTouchEvent不会再调用
4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回false),那么同一事件序列中其他事件都不会再交给它处理。并且重新交由它的父元素处理(父元素onTouchEvent被调用)
5. 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子View中干预父元素的事件分发过程,但ACTION_DOWN除外
6. ViewGroup默认不拦截任何事件,即onInterceptTouchEvent默认返回falseView没有onInterceptTouchEvent方法,一旦有点击事件,传递给它,那么它的onTouchEvent方法就会被调用
7. ViewonTouchEvent默认会消耗事件(返回true),除非它是不可点击的(clickablelongClickable同时为false),View的longClickable默认都为false,clickable要分情况,比如Button的clickable默认为true,TextView的clickable默认为false


Q2: Android事件分发类型有哪几个状态
A2: onTouchDown,onTouchMove,onTouchCancel,onTouchUp


Q3: requestDisallowInterceptTouchEvent属性的作用?
A3: 禁止或允许父View拦截自己的点击事件

	 // 不允许父View 拦截点击事件  
	 // false则反之
	 getParent().requestDisallowInterceptTouchEvent(true);

2. 在琢磨以上几个问题之后,我们通过下面几段伪代码,来模式触摸事件的传递流程

以下通过Java层代码模拟Android中事件分发流程,有助于理解Android事件分发机制,希望能帮助到大家(创建6个java文件,然后以java工程运行打印日志学习)

如果你能静下心来,读完下面6个.java文件代码,我相信你能收获很多

2.1 假Activity
public class Activity {

    public static void main(String[] arg) {

        // 顶级容器ViewGroup(构造函数传递左上,右下坐标)
        ViewGroup viewGroup = new ViewGroup(0, 0, 1080, 1920);
        viewGroup.setName("顶级容器");

        // 二级容器ViewGroup,也是定义两个坐标
        ViewGroup viewGroup1 = new ViewGroup(0, 0, 500, 500);
        viewGroup1.setName("第二级容器");

        // 模拟初始化View以及放置在ViewGroup层级中
        View view = new View(0, 0, 200, 200);
        view.setName("子View");

        viewGroup1.addView(view);
        viewGroup.addView(viewGroup1);

        viewGroup.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                System.out.println("顶级的OnTouch事件");
                return false;
            }
        });

        viewGroup1.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                System.out.println("第二级容器的OnTouch事件");
                return false;
            }
        });

//        view.setOnClickListener(new OnClickListener() {
//            @Override
//            public void onClick(View v) {
//                System.out.println("子iew的onClick事件");
//            }
//        });
//
        view.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                System.out.println("子view的OnTouch事件");
                return false;
            }
        });

        // 模拟事件分发(点击里面View坐标点为:(100,100))
        MotionEvent motionEvent = new MotionEvent(100, 100);
        motionEvent.setActionMasked(MotionEvent.ACTION_DOWN);

        // 顶级容器分发
        viewGroup.dispatchTouchEvent(motionEvent);
    }
}

2.2 假ViewGroup
public class ViewGroup extends View {

    // 子View个数
    private View[] mChildren = new View[0];

    public ViewGroup(int left, int top, int right, int bottom) {
        super(left, top, right, bottom);
    }

    List<View> childList = new ArrayList<>();


    public void addView(View view) {
        if (view == null) {
            return;
        }
        childList.add(view);
        mChildren = childList.toArray(new View[childList.size()]);
    }

    private TouchTarget mFirstTouchTarget;

    // 事件分发的入口
    public boolean dispatchTouchEvent(MotionEvent event) {
        //
        System.out.println(name + " dispatchTouchEvent ");

        boolean handled = false;
        boolean intercepted = onInterceptTouchEvent(event);

        // TouchTarget  模式 内存缓存   move up
        TouchTarget newTouchTarget = null;
        int actionMasked = event.getActionMasked();

        if (actionMasked != MotionEvent.ACTION_CANCEL && !intercepted) {
            // 不拦截情况下,开始处理Down事件
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                final View[] children = mChildren;
                //  遍历ViewGroup中所有子View
                for (int i = children.length - 1; i >= 0; i--) {
                    View child = mChildren[i];
                    // View能够接收到事件
                    if (!child.isContainer(event.getX(), event.getY())) {
                        continue;
                    }
                    // 能够接受事件  child   分发给他
                    if (dispatchTransformedTouchEvent(event, child)) {
                        // View[]  采取了 Message 的方式进行  链表结构
                        handled = true;
                        newTouchTarget = addTouchTarget(child);
                        break;
                    }
                }
            }
            // 当前的ViewGroup  dispatchTransformedTouchEvent
            if (mFirstTouchTarget == null) {
                handled = dispatchTransformedTouchEvent(event, null);
            }
        }
        return handled;
    }

    private TouchTarget addTouchTarget(View child) {
        final TouchTarget target = TouchTarget.obtain(child);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }


    // 回收池策略·
    private static final class TouchTarget {

        public View child;//当前缓存的View

        // 回收池(一个对象)
        private static TouchTarget sRecycleBin;

        private static final Object sRecycleLock = new Object[0];

        public TouchTarget next;

        // size
        private static int sRecycledCount;

        // up事件
        public static TouchTarget obtain(View child) {
            TouchTarget target;
            synchronized (sRecycleLock) {
                if (sRecycleBin == null) {
                    target = new TouchTarget();
                } else {
                    target = sRecycleBin;
                }
                sRecycleBin = target.next;
                sRecycledCount--;
                target.next = null;
            }
            target.child = child;
            return target;
        }

        public void recycle() {

            if (child == null) {
                throw new IllegalStateException("已经被回收过了");
            }
            synchronized (sRecycleLock) {

                if (sRecycledCount < 32) {
                    next = sRecycleBin;
                    sRecycleBin = this;
                    sRecycledCount += 1;
                }
            }
        }
    }

    //分发处理 子控件  View
    private boolean dispatchTransformedTouchEvent(MotionEvent event, View child) {
        boolean handled = false;
        // 当前View消费了
        if (child != null) {
            handled = child.dispatchTouchEvent(event);
        } else {
            handled = super.dispatchTouchEvent(event);
        }
        return handled;
    }

    /**
     * @param ev
     * @return 是否拦截点击事件
     */
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

}

2.3 假View
public class View {

    public String name;

    @Override
    public String toString() {
        return "" + name;
    }

    public void setName(String name) {
        this.name = name;
    }

    private int left;
    private int top;
    private int right;
    private int bottom;

    private OnTouchListener mOnTouchListener;
    private OnClickListener onClickListener;

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

    public void setOnClickListener(OnClickListener onClickListener) {
        this.onClickListener = onClickListener;
    }

    public View() {
    }

    public View(int left, int top, int right, int bottom) {
        this.left = left;
        this.top = top;
        this.right = right;
        this.bottom = bottom;
    }

    /**
     * @param x
     * @param y
     * @return 是否处于View点击区域内
     */
    public boolean isContainer(int x, int y) {
        if (x >= left && x < right && y >= top && y < bottom) {
            return true;
        }
        return false;
    }

    // 接受分发的代码
    public boolean dispatchTouchEvent(MotionEvent event) {
        System.out.println(name + " dispatchTouchEvent ");
        // 消费
        boolean result = false;
        if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        if (!result && onTouchEvent(event)) {
            result = true;
        }

        return result;
    }

    private boolean onTouchEvent(MotionEvent event) {
        System.out.println(name + " onTouchEvent ");

        if (onClickListener != null) {
            onClickListener.onClick(this);
            return true;
        }
        return false;
    }

}
2.4 假MotionEvent
public class 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;

    private int actionMasked;
    private int x;
    private int y;

    public MotionEvent() {
    }

    public MotionEvent(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int getActionMasked() {
        return actionMasked;
    }

    public void setActionMasked(int actionMasked) {
        this.actionMasked = actionMasked;
    }
}
2.5 假OnClickListener
public interface OnClickListener {

    void onClick(View v);

}
2.6 假OnTouchListener
public interface OnTouchListener {

    boolean onTouch(View v, MotionEvent event);

}


3. 通过以上伪代码,我们熟悉了事件传递流程,在实际开发中,我们通常通过以下2种策略来解决事件冲突

3.1 外部拦截法


	// 重写ViewGroup的以下方法,
	@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
    	boolean intercepted = false;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downPoint.x = ev.getX();
                downPoint.y = ev.getY();
                intercepted = false; // 必须为false,否则后续的MOVE,UP不在传递给子View
                break;
            case MotionEvent.ACTION_MOVE:
                CLog.i(TAG, "onTouchEvent ACTION_MOVE ev.getX(): " + ev.getX());
                if (父容器需要当前点击事件) {
                    intercepted = true;
                } else {
                   intercepted = false;
                }
               break;
            case MotionEvent.ACTION_UP:
            	intercepted = false; // 必须为false,否则会影响子View的onClick是否被触发
               break;
        }
        return intercepted;
    }
    // 大家可以参照ScrollView的源码来进一步熟悉外部拦截法的实际应用(以下这段为ScrollView源码片段)
	@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        /*
         * This method JUST determines whether we want to intercept the motion.
         * If we return true, onMotionEvent will be called and we do the actual
         * scrolling there.
         */

        /*
        * Shortcut the most recurring case: the user is in the dragging
        * state and he is moving his finger.  We want to intercept this
        * motion.
        */
        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
            return true;
        }

        if (super.onInterceptTouchEvent(ev)) {
            return true;
        }

        /*
         * Don't try to intercept touch if we can't scroll anyway.
         */
        if (getScrollY() == 0 && !canScrollVertically(1)) {
            return false;
        }

        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_MOVE: {
                /*
                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
                 * whether the user has moved far enough from his original down touch.
                 */
				......
                final int y = (int) ev.getY(pointerIndex);
                final int yDiff = Math.abs(y - mLastMotionY);
                if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
                    mIsBeingDragged = true;
                    mLastMotionY = y;
                    initVelocityTrackerIfNotExists();
                    mVelocityTracker.addMovement(ev);
                    mNestedYOffset = 0;
                    if (mScrollStrictSpan == null) {
                        mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
                    }
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                break;
            }
            case MotionEvent.ACTION_DOWN: {
                final int y = (int) ev.getY();
                if (!inChild((int) ev.getX(), (int) y)) {
                    mIsBeingDragged = false;
                    recycleVelocityTracker();
                    break;
                }
				......
                break;
            }
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                /* Release the drag */
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                recycleVelocityTracker();
                if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
                    postInvalidateOnAnimation();
                }
                stopNestedScroll();
                break;
            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;
        }
        /*
        * The only time we want to intercept motion events is if we are in the
        * drag mode.
        */
        return mIsBeingDragged;
    }

3.2 内部拦截法


	@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
     switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downPoint.x = ev.getX();
                downPoint.y = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                CLog.i(TAG, "onTouchEvent ACTION_MOVE ev.getX(): " + ev.getX());
                if (TextUtils.equals("0", mPos) && downPoint.x < ev.getX()) {
                    return true;
                }
        }
        return super.dispatchTouchEvent(ev);
    }
 
参考
  1. 解惑requestDisallowInterceptTouchEvent
  2. Android TouchEvent之requestDisallowInterceptTouchEvent
  3. 一文解决Android View滑动冲突
  4. Android 经典笔记之四: 事件冲突解决思路与方案
  5. ViewConfiguration 实例获取
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

初心一点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值