尽管网络上已经有很多关于这个话题的优秀文章了,但还是写了这篇文章,主要还是为了加强自己的记忆吧,自己过一遍总比看别人的分析要深刻得多,那就走起吧。
简单示例
先看一个示例 :
布局文件 :
- <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_gravity="center"
- tools:context="com.example.touch_event.MainActivity"
- tools:ignore="MergeRootFrame" >
- <Button
- android:id="@+id/my_button"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/hello_world" />
- </FrameLayout>
MainActivity文件:
- public class MainActivity extends Activity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- Button mBtn = (Button) findViewById(R.id.my_button);
- mBtn.setOnTouchListener(new OnTouchListener() {
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- Log.d("", "### onTouch : " + event.getAction());
- return false;
- }
- });
- mBtn.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- Log.d("", "### onClick : " + v);
- }
- });
- }
- @Override
- public boolean dispatchTouchEvent(MotionEvent ev) {
- Log.d("", "### activity dispatchTouchEvent");
- return super.dispatchTouchEvent(ev);
- }
- }
- 08-31 03:03:56.116: D/(1560): ### activity dispatchTouchEvent
- 08-31 03:03:56.116: D/(1560): ### onTouch : 0
- 08-31 03:03:56.196: D/(1560): ### activity dispatchTouchEvent
- 08-31 03:03:56.196: D/(1560): ### onTouch : 1
- 08-31 03:03:56.196: D/(1560): ### onClick : android.widget.Button{52860d98 VFED..C. ...PH... 0,0-1080,144 #7f05003d app:id/my_button}
我们可以看到首先执行了Activity中的dispatchTouchEvent方法,然后执行了onTouch方法,然后再是dispatchTouchEvent --> onTouch, 最后才是执行按钮的点击事件。这里我们可能有个疑问,为什么dispatchTouchEvent和onTouch都执行了两次,而onClick才执行了一次 ? 为什么两次的Touch事件的action不一样,action 0 和 action 1到底代表了什么 ?
覆写过onTouchEvent的朋友知道,一般来说我们在该方法体内都会处理集中touch类型的事件,有ACTION_DOWN、ACTION_MOVE、ACTION_UP等,不过上面我们的例子中并没有移动,只是单纯的按下、抬起。因此,我们的触摸事件也只有按下、抬起,因此有2次touch事件,而action分别为0和1。我们看看MotionEvent中的一些变量定义吧:
- public final class MotionEvent extends InputEvent implements Parcelable {
- // 代码省略
- 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; // 取消
- // 代码省略
- }
在看另外两个场景:
1、我们点击按钮外的区域,输出Log如下 :
- 08-31 03:04:45.408: D/(1560): ### activity dispatchTouchEvent08-31
- 03:04:45.512: D/(1560): ### activity dispatchTouchEvent
2、我们在onTouch函数中返回true, 输出Log如下 :
- 08-31 03:06:04.764: D/(1612): ### activity dispatchTouchEvent
- 08-31 03:06:04.764: D/(1612): ### onTouch : 0
- 08-31 03:06:04.868: D/(1612): ### activity dispatchTouchEvent
- 08-31 03:06:04.868: D/(1612): ### onTouch : 1
Android Touch事件分发
那么整个事件分发的流程是怎样的呢 ?
简单来说就是用户触摸手机屏幕会产生一个触摸消息,最终这个触摸消息会被传送到ViewRoot ( 看4.2的源码时这个类改成了ViewRootImpl )的InputHandler,ViewRoot是GUI管理系统与GUI呈现系统之间的桥梁,根据ViewRoot的定义,发现它并不是一个View类型,而是一个Handler。InputHandler是一个接口类型,用于处理KeyEvent和TouchEvent类型的事件,我们看看源码 :
- public final class ViewRoot extends Handler implements ViewParent,
- View.AttachInfo.Callbacks {
- // 代码省略
- private final InputHandler mInputHandler = new InputHandler() {
- public void handleKey(KeyEvent event, Runnable finishedCallback) {
- startInputEvent(finishedCallback);
- dispatchKey(event, true);
- }
- public void handleMotion(MotionEvent event, Runnable finishedCallback) {
- startInputEvent(finishedCallback);
- dispatchMotion(event, true); // 1、handle 触摸消息
- }
- };
- // 代码省略
- // 2、分发触摸消息
- private void dispatchMotion(MotionEvent event, boolean sendDone) {
- int source = event.getSource();
- if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
- dispatchPointer(event, sendDone); // 分发触摸消息
- } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
- dispatchTrackball(event, sendDone);
- } else {
- // TODO
- Log.v(TAG, "Dropping unsupported motion event (unimplemented): " + event);
- if (sendDone) {
- finishInputEvent();
- }
- }
- }
- // 3、通过Handler投递消息
- private void dispatchPointer(MotionEvent event, boolean sendDone) {
- Message msg = obtainMessage(DISPATCH_POINTER);
- msg.obj = event;
- msg.arg1 = sendDone ? 1 : 0;
- sendMessageAtTime(msg, event.getEventTime());
- }
- @Override
- public void handleMessage(Message msg) { // ViewRoot覆写handlerMessage来处理各种消息
- switch (msg.what) {
- // 代码省略
- case DO_TRAVERSAL:
- if (mProfile) {
- Debug.startMethodTracing("ViewRoot");
- }
- performTraversals();
- if (mProfile) {
- Debug.stopMethodTracing();
- mProfile = false;
- }
- break;
- case DISPATCH_POINTER: { // 4、处理DISPATCH_POINTER类型的消息,即触摸屏幕的消息
- MotionEvent event = (MotionEvent) msg.obj;
- try {
- deliverPointerEvent(event); // 5、处理触摸消息
- } finally {
- event.recycle();
- if (msg.arg1 != 0) {
- finishInputEvent();
- }
- if (LOCAL_LOGV || WATCH_POINTER) Log.i(TAG, "Done dispatching!");
- }
- } break;
- // 代码省略
- }
- // 6、真正的处理事件
- private void deliverPointerEvent(MotionEvent event) {
- if (mTranslator != null) {
- mTranslator.translateEventInScreenToAppWindow(event);
- }
- boolean handled;
- if (mView != null && mAdded) {
- // enter touch mode on the down
- boolean isDown = event.getAction() == MotionEvent.ACTION_DOWN;
- if (isDown) {
- ensureTouchMode(true); // 如果是ACTION_DOWN事件则进入触摸模式,否则为按键模式。
- }
- if(Config.LOGV) {
- captureMotionLog("captureDispatchPointer", event);
- }
- if (mCurScrollY != 0) {
- event.offsetLocation(0, mCurScrollY); // 物理坐标向逻辑坐标的转换
- }
- if (MEASURE_LATENCY) {
- lt.sample("A Dispatching TouchEvents", System.nanoTime() - event.getEventTimeNano());
- }
- // 7、分发事件,如果是窗口类型,则这里的mView对应的就是PhonwWindow中的DecorView,否则为根视图的ViewGroup。
- handled = mView.dispatchTouchEvent(event);
- // 代码省略
- }
- }
- // 代码省略
- }
我们就以Activity为例来分析这个过程,我们知道显示出来的Activity有一个顶层窗口,这个窗口的实现类是PhoneWindow, PhoneWindow中的内容区域是一个DecorView类型的View,这个View这就是我们在手机上看到的内容,这个DecorView是FrameLayout的子类,Activity的的dispatchTouchEvent实际上就是调用PhoneWindow的dispatchTouchEvent,我们看看源代码吧,进入Activity的dispatchTouchEvent函数 :
- public boolean dispatchTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- onUserInteraction();
- }
- if (getWindow().superDispatchTouchEvent(ev)) { // 1、调用的是PhoneWindow的superDispatchTouchEvent(ev)
- return true;
- }
- return onTouchEvent(ev);
- }
- public void onUserInteraction() {
- }
- Log.d("", "### Activiti中getWindow()获取的类型是 : " + this.getWindow()) ;
输出:
- 08-31 03:40:17.036: D/(1688): ### Activiti中getWindow()获取的类型是 : com.android.internal.policy.impl.PhoneWindow@5287fe38
- @Override
- public boolean superDispatchTouchEvent(MotionEvent event) {
- return mDecor.superDispatchTouchEvent(event);
- }
- // This is the top-level view of the window, containing the window decor.
- private DecorView mDecor;
那么我继续看看DecorView到底是个什么玩意儿吧。
- private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
- /* package */int mDefaultOpacity = PixelFormat.OPAQUE;
- /** The feature ID of the panel, or -1 if this is the application's DecorView */
- private final int mFeatureId;
- private final Rect mDrawingBounds = new Rect();
- private final Rect mBackgroundPadding = new Rect();
- private final Rect mFramePadding = new Rect();
- private final Rect mFrameOffsets = new Rect();
- private boolean mChanging;
- private Drawable mMenuBackground;
- private boolean mWatchingForMenu;
- private int mDownY;
- public DecorView(Context context, int featureId) {
- super(context);
- mFeatureId = featureId;
- }
- @Override
- public boolean dispatchKeyEvent(KeyEvent event) {
- final int keyCode = event.getKeyCode();
- // 代码省略
- return isDown ? PhoneWindow.this.onKeyDown(mFeatureId, event.getKeyCode(), event)
- : PhoneWindow.this.onKeyUp(mFeatureId, event.getKeyCode(), event);
- }
- @Override
- public boolean dispatchTouchEvent(MotionEvent ev) {
- final Callback cb = getCallback();
- return cb != null && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super
- .dispatchTouchEvent(ev);
- }
- @Override
- public boolean dispatchTrackballEvent(MotionEvent ev) {
- final Callback cb = getCallback();
- return cb != null && mFeatureId < 0 ? cb.dispatchTrackballEvent(ev) : super
- .dispatchTrackballEvent(ev);
- }
- public boolean superDispatchKeyEvent(KeyEvent event) {
- return super.dispatchKeyEvent(event);
- }
- public boolean superDispatchTouchEvent(MotionEvent event) {
- return super.dispatchTouchEvent(event);
- }
- public boolean superDispatchTrackballEvent(MotionEvent event) {
- return super.dispatchTrackballEvent(event);
- }
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- return onInterceptTouchEvent(event);
- }
- // 代码省略
- }
- /**
- * {@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)) // 1、是否禁用拦截、是否拦截事件
- // 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--) // 2、迭代所有子view,查找触摸事件在哪个子view的坐标范围内
- final View child = children[i];
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
- || child.getAnimation() != null) {
- child.getHitRect(frame); // 3、获取child的坐标范围
- if (frame.contains(scrolledXInt, scrolledYInt)) // 4、判断发生该事件坐标是否在该child坐标范围内
- // 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)) // 5、child处理该事件
- // 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);
- }
进入代码1内部的if后,有一个for循环,遍历了当前ViewGroup下的所有子child view,如果触摸该事件的坐标在某个child view的坐标范围内,那么该child view来处理这个触摸事件,即调用该child view的dispatchTouchEvent。如果该child view是ViewGroup类型,那么继续执行上面的判断,并且遍历子view;如果该child view不是ViewGroup类型,那么直接调用的是View中的dispatchTouchEvent方法,除非这个child 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) {
- if (!onFilterTouchEventForSecurity(event)) {
- return false;
- }
- if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
- mOnTouchListener.onTouch(this, event)) {
- return true;
- }
- return onTouchEvent(event);
- }
该函数中,首先判断该事件是否符合安全策略,然后判断该view是否是enable的 ,以及是否设置了Touch Listener,mOnTouchListener即我们通过setOnTouchListener设置的。
- /**
- * Register a callback to be invoked when a touch event is sent to this view.
- * @param l the touch listener to attach to this view
- */
- public void setOnTouchListener(OnTouchListener l) {
- mOnTouchListener = l;
- }
- /**
- * Implement this method to handle touch screen motion events.
- *
- * @param event The motion event.
- * @return True if the event was handled, false otherwise.
- */
- public boolean onTouchEvent(MotionEvent event) {
- final int viewFlags = mViewFlags;
- if ((viewFlags & ENABLED_MASK) == DISABLED) // 1、判断该view是否enable
- // A disabled view that is clickable still consumes the touch
- // events, it just doesn't respond to them.
- return (((viewFlags & CLICKABLE) == CLICKABLE ||
- (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
- }
- if (mTouchDelegate != null) {
- if (mTouchDelegate.onTouchEvent(event)) {
- return true;
- }
- }
- if (((viewFlags & CLICKABLE) == CLICKABLE ||
- (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) // 2、是否是clickable或者long clickable
- switch (event.getAction()) {
- case MotionEvent.ACTION_UP: // 抬起事件
- boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
- if ((mPrivateFlags & 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(); // 获取焦点
- }
- if (!mHasPerformedLongPress) {
- // 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)) // post
- performClick(); // 3、点击事件处理
- }
- }
- }
- if (mUnsetPressedState == null) {
- mUnsetPressedState = new UnsetPressedState();
- }
- if (prepressed) {
- mPrivateFlags |= PRESSED;
- refreshDrawableState();
- postDelayed(mUnsetPressedState,
- ViewConfiguration.getPressedStateDuration());
- } else if (!post(mUnsetPressedState)) {
- // If the post failed, unpress right now
- mUnsetPressedState.run();
- }
- removeTapCallback();
- }
- break;
- case MotionEvent.ACTION_DOWN:
- if (mPendingCheckForTap == null) {
- mPendingCheckForTap = new CheckForTap();
- }
- mPrivateFlags |= PREPRESSED;
- mHasPerformedLongPress = false;
- postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
- break;
- case MotionEvent.ACTION_CANCEL:
- mPrivateFlags &= ~PRESSED;
- refreshDrawableState();
- removeTapCallback();
- break;
- case MotionEvent.ACTION_MOVE:
- final int x = (int) event.getX();
- final int y = (int) event.getY();
- // Be lenient about moving outside of buttons
- int slop = mTouchSlop;
- if ((x < 0 - slop) || (x >= getWidth() + slop) ||
- (y < 0 - slop) || (y >= getHeight() + slop)) {
- // Outside button
- removeTapCallback();
- if ((mPrivateFlags & PRESSED) != 0) {
- // Remove any future long press/tap checks
- removeLongPressCallback();
- // Need to switch from pressed to not pressed
- mPrivateFlags &= ~PRESSED;
- refreshDrawableState();
- }
- }
- break;
- }
- return true;
- }
- return false;
- }
- /**
- * Causes the Runnable to be added to the message queue.
- * The runnable will be run on the user interface thread.
- *
- * @param action The Runnable that will be executed.
- *
- * @return Returns true if the Runnable was successfully placed in to the
- * message queue. Returns false on failure, usually because the
- * looper processing the message queue is exiting.
- */
- public boolean post(Runnable action) {
- Handler handler;
- if (mAttachInfo != null) {
- handler = mAttachInfo.mHandler;
- } else {
- // Assume that post will succeed later
- ViewRoot.getRunQueue().post(action);
- return true;
- }
- return handler.post(action);
- }
我们看看PerformClick类吧。
- private final class PerformClick implements Runnable {
- public void run() {
- performClick();
- }
- }
- /**
- * Register a callback to be invoked when this view is clicked. If this view is not
- * clickable, it becomes clickable.
- *
- * @param l The callback that will run
- *
- * @see #setClickable(boolean)
- */
- public void setOnClickListener(OnClickListener l) {
- if (!isClickable()) {
- setClickable(true);
- }
- mOnClickListener = l;
- }
- /**
- * Call this view's OnClickListener, if it is defined.
- *
- * @return True there was an assigned OnClickListener that was called, false
- * otherwise is returned.
- */
- public boolean performClick() {
- sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
- if (mOnClickListener != null) {
- playSoundEffect(SoundEffectConstants.CLICK);
- mOnClickListener.onClick(this);
- return true;
- }
- return false;
- }
代码很简单,主要就是调用了mOnClickListener.onClick(this);方法,即执行用户通过setOnClickListener设置进来的点击事件处理Listener。
总结
用户触摸屏幕产生一个触摸消息,系统底层将该消息转发给ViewRoot ( ViewRootImpl ),ViewRoot产生一个DISPATCHE_POINTER的消息,并且在handleMessage中处理该消息,最终会通过deliverPointerEvent(MotionEvent event)来处理该消息。在该函数中会调用mView.dispatchTouchEvent(event)来分发消息,该mView是一个ViewGroup类型,因此是ViewGroup的dispatchTouchEvent(event),在该函数中会遍历所有的child view,找到该事件的触发的左边与每个child view的坐标进行对比,如果触摸的坐标在该child view的范围内,则由该child view进行处理。如果该child view是ViewGroup类型,则继续上一步的查找过程;否则执行View中的dispatchTouchEvent(event)函数。在View的dispatchTouchEvent(event)中首先判断该控件是否enale以及mOnTouchListent是否为空,如果mOnTouchListener不为空则执行mOnTouchListener.onTouch(event)方法,如果该方法返回false则再执行View中的onTouchEvent(event)方法,并且在该方法中执行mOnClickListener.onClick(this, event) ;方法; 如果mOnTouchListener.onTouch(event)返回true则不会执行onTouchEvent方法,因此点击事件也不会被执行。
粗略的流程图如下 :