前言

两年前我曾经写过一篇点击事件的原理博客,在今年重新翻看的时候发现文章的结构不好,且没有总结,让人不容易理解,所以重新整理了一下再写一次。

1. 原理总结

注意:正文中虽然说的都是点击事件,实际上他并不是指我们常用语境中的onClick或者onLongClick,而是任意类型的事件,只是用点击事件来形容比较让人容易理解,实际上视图的事件分发是包括按下,抬起,移动这三个部分的。

MotionEvent.ACTION_DOWN按下View(所有事件的开始)
MotionEvent.ACTION_UP抬起View(与DOWN对应)
MotionEvent.ACTION_MOVE移动View
MotionEvent.ACTION_CANCEL结束事件(非人为原因)

再我看完点击事件分发的原理之后,我会用三个词来形容点击事件的全部原理:

  1. View树:

    首先我们需要知道的是,在Android中我们所写的视图代码,无论是xml还是通过代码手动添加的,其数据结构展现出来的就是一个树,一个一对多的存储关系的集合。

    在代码中我们表现这个树状数据结构的方式:在ViewGroup中设置了一个子View的List,通过让最上层的父节点DevorView(他也是一个ViewGroup)—持有好几个ViewGroup,里面的ViewGroup中每个又持有多个ViewGroup,层层嵌套到最底层的View为止。

  2. 深度搜索优先dfs:

    在用户触发任意一个点击事件的时候,我们是通过深度搜索优先的方式去寻找可以消费该点击事件的视图,从树的最深层开始处理点击事件。

    这个代表了什么意思呢?如果一个ViewGroup和它的子View同时都设置了OnClickListener,那么我们在点击它们之间重合的部分时,只会触发子View的点击事件而不会触发父View的点击事件。

    在代码表现这个深度搜索的方式:处理分发事件的时候,会先用for循环把所有的子View遍历,尝试调用子View的方法来处理该点击事件,只有确认所有子View都不能处理该点击事件之后,才会调用自己的点击事件处理方法。

  3. 逆序遍历:

    触发点击事件搜索子View的时候,总有个搜索的顺序,这个顺序是逆序,也就是从最后一个添加的子View开始查找和处理点击事件。

    其原理和添加VIew是相关联的,我们知道,在一个ViewGroup的两个子View中,如果这两个子View有重合的部分,那么一般而言总是后添加的视图会覆盖掉前面添加的视图的部分。

    点击事件也是同理,当用户点击他们重合的部分时,一般而言用户总是希望点击到用户本身可以看见的那个视图。所以我们的点击事件分发就和添加视图的顺序相反,从最后添加的视图开始遍历。
    在这里插入图片描述

2.1 时序图总结

为了方便我们看完源码之后以后复习方便,我先将点击事件分发的全部流程放到这里,在看的过程中有需要可以翻回来看:
在这里插入图片描述

2.2 流程图总结

由于时序图一般而言没有办法很好的展示我们深度优先搜索的思想,所以我额外又补充了一张流程图,这个流程图也画出了点击事件分发的原理:
在这里插入图片描述

2. 源码解读

2.1 Activity到ViewGroup

任何的事件源头都是从我们底层的SurfaceFlinger进程来的,他直接管理着用户可以看到的窗口,但是我们这里不用去深究那么底层的原理。只要知道从底层来的点击事件第一个触发的是Activity的DispatchTouchEvent就足够了。

public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2,
        Window.OnWindowDismissedCallback,
        ContentCaptureManager.ContentCaptureClient {

	/**
     * 调用以处理触摸屏事件。您可以重写此方法,在将所有触摸屏事件发送到窗口之前拦截它们。
     * 请确保为应该正常处理的触摸屏事件调用此实现。
     * 
     * @param ev 点击事件本体
     * @return boolean 如果事件被消费了会return true
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        // 重点是这行
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
}

点击事件就这样从Activity手上分出去了,接下来看看Window类是如何处理的,顺便一提,Window本身是一个抽象类,作为他承载的实体一般而言是PhoneWindow类。

public class PhoneWindow extends Window implements MenuBuilder.Callback {

	private DecorView mDecor;
	
	@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
}

事件就这样直接转到了DecorView。

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
	@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : /* 重点看这部分 */super.dispatchTouchEvent(ev);
    }
}

DecorView作为最上层的特殊View,他在处理事件的时候会有特殊的窗口判断,但是一般而言是不会触发的,我们不去理他,重点看super.dispatchTouchEvent(ev),这个就代表了点击事件的真正起点。

2.2 ViewGroup

接下来就进入到我们真正的主角,ViewGroup了,我会将他的源码切成好几段一点点的说明。

事件中断

在正式开始点击事件之前,ViewGroup会通过onInterceptTouchEvent这个方法对点击事件做一个中断判断,如果被中断了就不会处理后续的流程了

onInterceptTouchEvent这个方法一般而言都是会返回false,也就是不中断。如果你有业务上的需求需要中断的话,可以返回true。这样事件就不会往下面的View分发,只会由自己进行处理。

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
	@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    	boolean handled = false;

		// 检查事件是否被中断
        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);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
		
		// 如果被拦截,就会跳过分发的流程
		if (!canceled && !intercepted) {
			// ...正式分发点击事件
		}

		// 自己处理点击事件
		
		return handled;
    }


	public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getXDispatchLocation(0), ev.getYDispatchLocation(0))) {
            return true;
        }
        return false;
    }
}

逆序搜索

开始正式处理点击事件,会先逆序遍历所有的子View,然后进行一些判断

  1. 该子View能否点击。canReceivePointerEvents
  2. 用户点击的位置是否和该View重合。isTransformedTouchPointInView

两个判断条件都符合后,就会尝试在该子View中处理点击事件
注意,子View也有可能是一个ViewGroup,所以调用子View的dispatchTouchEvent后,有可能会实际上调用的还是ViewGroup.dispatchTouchEvent。

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
	@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    	boolean handled = false;

		// 检查事件是否被中断
        final boolean intercepted;
		
		TouchTarget newTouchTarget = null;
		if (!canceled && !intercepted) {
			// ...正式处理点击事件

			final int childrenCount = mChildrenCount;
			for (int i = childrenCount - 1; i >= 0; i--) {
				// 不用关注他的原理,我们只需要他掏出了一个View即可。
                final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
				
				// 对View做合法性判断,如果合法就可以继续点击
				if (!child.canReceivePointerEvents()
						|| !isTransformedTouchPointInView(x, y, child, null)) {
                    ev.setTargetAccessibilityFocus(false);
                    continue;
                }

				newTouchTarget = getTouchTarget(child);
				// 转换为点击事件,注意child这个入参我们是有传值的
				if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
					//...处理了一些逻辑
					break; //然后直接跳出循环
				}
			}

			// 下面又开始处理其他逻辑
		}
		
		return handled;
    }

	/**
	 * 分发转换为点击事件
	 */ 
	private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            // 注意child这个入参,我们此时传入的child不为空,所以走下面
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
    }
}

自己处理点击事件

在遍历了所有子View都没有处理掉该事件之后,ViewGroup会尝试自己来处理该事件。

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
	@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    	boolean handled = false;

        
		TouchTarget newTouchTarget = null;
		if (!canceled && !intercepted) {
			// ...正式处理点击事件
			TouchTarget newTouchTarget = null;

			for (int i = childrenCount - 1; i >= 0; i--) {
				// 刚才的for循环,用来表示代码的相对位置
			}

			// 如果点击事件之前被子View给处理了,
			// 那么代码到这里之后newTouchTarget就不为空,或者mFirstTouchTarget不为空
			if (newTouchTarget == null && mFirstTouchTarget != null) {
                newTouchTarget = mFirstTouchTarget;
                while (newTouchTarget.next != null) {
                    newTouchTarget = newTouchTarget.next;
                }
                newTouchTarget.pointerIdBits |= idBitsToAssign;
            }
		}

		// 这里和上面是连着的,如果mFirstTouchTarget== null其实就代表着点击事件没有被子View给处理
		if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
        	// 处理其他逻辑
        }
		
		return handled;
    }

	/**
	 * 分发转换为点击事件
	 */ 
	private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            // 注意child这个入参,我们此时传入的child为空,所以走上面
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
    }
}

ViewGroup总结

要不然就是通过某个子View层层遍历,走到最深层的某个View的dispatchTouchEvent
要不然就是没有子View,调用自己的super,dispatchTouchEvent,还是View的dispatchTouchEvent

总而言之,代码就会通过这两种方式走到View这个类里面,
ViewGroup的dispatchTouchEvent这个方法的功能也很明显了:找一个可以处理该点击事件的View(可能是自己),将点击事件(TouchEvent)分发(Dispatch)给它(View)。

2.3 View

OnTouchListener

点击事件到View之后,入口还是DispatchTouchEvent,他会先检查是否有TouchListener,有的话先执行它。

这里有两个点,第一个点就是在View里面,OnClickListener和OnTouchListener是完全不同的东西。
第二个点就是OnTouchListener这个方法的return是有开发者自己控制的,换句话说,开发者可以自行控制事件是否要停在onTouch这里

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
	public boolean dispatchTouchEvent(MotionEvent event) {
	
		boolean result = false;
		
		// 这个if一般而言都是通过的
		if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    // 重点看这里
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

			// 如果事件没有被onTouch处理掉,就会进入事件处理流程
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
		
		return result;
	}

	public interface OnTouchListener {
        boolean onTouch(View v, MotionEvent event);
    }
}

onTouchEvent

这里就是单个事件真正的处理方法,但是对于我们而言我们反而不需要太关注这个方法的处理逻辑。
第一是本文主要关注事件时如何分发到这里的。
第二是该方法无非就是对我们常用的一些逻辑,如focus,onClickListener,onLongClick等内容进行判断,有的话这个方法就会return true,没有的话就return false

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {

	public boolean onTouchEvent(MotionEvent event) {
		final int action = event.getAction();
		if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
			 switch (action) {
			 	case MotionEvent.ACTION_UP:
			 		// 一堆判断之后会走到这里,我们就不看这些判断了
			 		performClickInternal();
			 		break;
			 	case MotionEvent.ACTION_DOWN:
			 		break;
			 	case MotionEvent.ACTION_CANCEL:
			 		break;
			 	case MotionEvent.ACTION_MOVE:
			 		break;
			 }

			return true;
		}
		return false;
	}

	private boolean performClickInternal() {
        notifyAutofillManagerOnClick();
        return performClick();
    }

	public boolean performClick() {
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            // 这里就是我们设置的点击事件了,OnClickListener
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }
}

3. 附录:时序图uml代码

@startuml

participant Activity
participant PhoneWindow
participant DecorView
participant ViewGroup as vg
participant View as v
participant TouchListener as tl
participant "子View\nViewGroup" as cvg

activate Activity
Activity -> PhoneWindow : dispatchTouchEvent
activate PhoneWindow
	PhoneWindow -> DecorView : dispatchTouchEvent
	activate DecorView
		DecorView -> vg : dispatchTouchEvent\n进入View树处理点击事件
		activate vg 
			vg -> vg : onInterceptTouchEvent\n判断事件是否被拦截
			activate vg 
				vg --> vg : return boolean
			deactivate vg

			alt true
				vg --> vg : return\n不进行任何点击事件的处理\n流程结束
			end
			
			loop 逆序遍历子View
				vg -> cvg : isTransformedTouchPointInView\n判断是否可以点击
				activate cvg
					cvg --> vg : return boolean\ntrue代表可以点击
				deactivate cvg

				alt 不能点击
					vg -> vg : continue\n搜索下一个子View	
				else 可以点击
					vg -> vg : dispatchTransformedTouchEvent\n将分发事件转变为点击事件
					activate vg
						vg -> cvg : dispatchTouchEvent\n重复该ViewGroup的行为,继续往下分发
						activate cvg
						break 点击事件被消费
								cvg --> vg : return boolean\n告知点击事件是否被消费
						end
						deactivate cvg
					deactivate vg
				end
			end

			alt 点击事件没被消费
				vg -> vg : dispatchTransformedTouchEvent\n将分发事件转变为点击事件
				activate vg
					vg -> v :dispatchTouchEvent
					activate v
						alt 该View有TouchListener
							v -> tl : onTouch
							activate tl
								tl --> v : return boolean\n告知是否继续往下处理
							deactivate tl
						end 

						alt return true
								v --> vg : return true\n告知点击事件已经被处理
						else return false
								v -> v :onTouched
								activate v
									v --> v : return boolean\n告知是否处理了点击事件
								deactivate v
								v --> vg : return boolean\n告知是否处理了点击事件
						end
						
					deactivate v
				deactivate vg
			end

		vg --> DecorView : return boolean\n告知是否处理了点击事件
		deactivate vg

		DecorView --> PhoneWindow : return boolean\n告知是否处理了点击事件
	deactivate DecorView

PhoneWindow --> Activity : return boolean\n告知是否处理了点击事件
deactivate PhoneWindow
deactivate Activity

@enduml

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部