android touch事件分發

本文以實例形式講述了Android Touch事件分發過程,對於深入理解與掌握Android程序設計有很大的幫助作用。具體分析如下:

首先,從一個簡單示例入手:

先看一個示例如下圖所示:

佈局文件 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  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文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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);
  }
}

當用戶點擊按鈕時會輸出如下Log:

1
2
3
4
5
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中的一些變量定義吧:

1
2
3
4
5
6
7
8
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// 取消
 // 代碼省略
}

可以看到,代表按下的事件爲0,擡起事件爲1,也證實了我們上面所說的。

在看另外兩個場景:

1、我們點擊按鈕外的區域,輸出Log如下 :

1
2
08-31 03:04:45.408: D/(1560): ### activity dispatchTouchEvent08-31 
03:04:45.512: D/(1560): ### activity dispatchTouchEvent

2、我們在onTouch函數中返回true, 輸出Log如下 :

1
2
3
4
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類型的事件,我們看看源碼 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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);
      // 代碼省略  
    }
  }
  // 代碼省略

經過層層迷霧,不管代碼7處的mView是DecorView還是非窗口界面的根視圖,其本質都是ViewGroup,即觸摸事件最終被根視圖ViewGroup進行分發!!!
        我們就以Activity爲例來分析這個過程,我們知道顯示出來的Activity有一個頂層窗口,這個窗口的實現類是PhoneWindow, PhoneWindow中的內容區域是一個DecorView類型的View,這個View這就是我們在手機上看到的內容,這個DecorView是FrameLayout的子類,Activity的的dispatchTouchEvent實際上就是調用PhoneWindow的dispatchTouchEvent,我們看看源代碼吧,進入Activity的dispatchTouchEvent函數 :

1
2
3
4
5
6
7
8
9
10
11
12
13
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() {
 }

可以看到,如果事件爲按下事件,則會進入到onUserInteraction()這個函數,該函數爲空實現,我們暫且不管它。繼續看,發現touch事件的分發調用了getWindow().superDispatchTouchEvent(ev)函數,getWindow()獲取到的實例的類型爲PhoneWindow類型,你可以在你的Activity類中使用如下方式查看getWindow()獲取到的類型:

1
Log.d("", "### Activiti中getWindow()獲取的類型是 : " + this.getWindow()) ;

輸出:

1
08-31 03:40:17.036: D/(1688): ### Activiti中getWindow()獲取的類型是 : com.android.internal.policy.impl.PhoneWindow@5287fe38

OK,廢話不多說,我們還是繼續看PhoneWindow中的superDispatchTouchEvent函數吧。

1
2
3
4
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
  return mDecor.superDispatchTouchEvent(event);
}

恩,調用的是mDecor的superDispatchTouchEvent(event)函數,這個mDecor就是我們上面所說的DecorView類型,也就是我們看到的Activity上的所有內容的一個頂層ViewGroup,即整個ViewTree的根節點。看看它的聲明吧。

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

DecorView

那麼我繼續看看DecorView到底是個什麼玩意兒吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
  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);
    }
// 代碼省略
}

可以看到,DecorView繼承自FrameLayout, 它對於touch事件的分發( dispatchTouchEvent )、處理都是交給super類來處理,也就是FrameLayout來處理,我們在FrameLayout中沒有看到相應的實現,那繼續跟蹤到FrameLayout的父類,即ViewGroup,我們看到了dispatchTouchEvent的實現,那我們就先看ViewGroup (Android 2.3 源碼)是如何進行事件分發的吧。

ViewGroup的Touch事件分發

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/**
 * {@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處可以看到一個條件判斷,如果disallowIntercept和!onInterceptTouchEvent(ev)兩者有一個爲true,就會進入到這個條件判斷中。disallowIntercept是指是否禁用掉事件攔截的功能,默認是false,也可以通過調用requestDisallowInterceptTouchEvent方法對這個值進行修改。那麼當第一個值爲false的時候就會完全依賴第二個值來決定是否可以進入到條件判斷的內部,第二個值是什麼呢?onInterceptTouchEvent就是ViewGroup對事件進行攔截的一個函數,返回該函數返回false則表示不攔截事件,反之則表示攔截。第二個條件是是對onInterceptTouchEvent方法的返回值取反,也就是說如果我們在onInterceptTouchEvent方法中返回false,就會讓第二個值爲true,從而進入到條件判斷的內部,如果我們在onInterceptTouchEvent方法中返回true,就會讓第二個值的整體變爲false,從而跳出了這個條件判斷。例如我們需要實現ListView滑動刪除某一項的功能,那麼可以通過在onInterceptTouchEvent返回true,並且在onTouchEvent中實現相關的判斷邏輯,從而實現該功能。

進入代碼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函數:

View的Touch事件分發

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 * 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設置的。

1
2
3
4
5
6
7
/**
 * 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;
}

如果mOnTouchListener.onTouch(this, event)返回false則繼續執行onTouchEvent(event);如果mOnTouchListener.onTouch(this, event)返回true,則表示該事件被消費了,不再傳遞,因此也不會執行onTouchEvent(event)。這也驗證了我們上文中留下的場景2,當onTouch函數返回true時,點擊按鈕,但我們的點擊事件沒有執行。那麼我們還是先來看看onTouchEvent(event)函數到底做了什麼吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
 * 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;
}

我們看到,在onTouchEvent函數中就是對ACTION_UP、ACTION_DOWN、ACTION_MOVE等幾個事件進行處理,而最重要的就是UP事件了,因爲這個裏面包含了對用戶點擊事件的處理,或者是說對於用戶而言相對重要一點,因此放在了第一個case中。在ACTION_UP事件中會判斷該view是否enable、是否clickable、是否獲取到了焦點,然後我們看到會通過post方法將一個PerformClick對象投遞給UI線程,如果投遞失敗則直接調用performClick函數執行點擊事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * 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類吧。

1
2
3
4
5
private final class PerformClick implements Runnable {
  public void run() {
    performClick();
  }
}

可以看到,其內部就是包裝了View類中的performClick()方法。再看performClick()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
 * 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方法,因此點擊事件也不會被執行。

粗略的流程圖如下 :

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章