1. 程式人生 > >Android--Touch事件

Android--Touch事件

【轉】Android onTouch()和onTouchEvent()區別

1、onTouch()方法:

  onTouch方式是View的OnTouchListener介面中定義的方法。

  當一個View綁定了OnTouchListener後,當有Touch事件觸發時,就會呼叫onTouch方法。

  (當把手放到View上後,onTouch方法被一遍一遍的呼叫)

 

2、onTouchEvent()方法:

  onTouchEvent方法時過載的Activity的方法

  重寫了Acitivity的onTouchEvent方法後,當螢幕有Touch事件時,此方法就會被呼叫。

  (當把手放到Activity上時,onTouchEvent方法會一遍一遍的被呼叫)

 

3、Touch事件的傳遞:

  在一個Activity裡面放一個TextView的例項tv,並且這個tv的屬性設定為march_parent

  在這種情況下,當手放到螢幕上的時候,首先會是tv響應Touch事件,執行onTouch方法。

 

  如果onTouch返回值為true,表示這個Touch事件被onTouch方法處理完畢,不會把Touch事件再傳遞給Activity

  也就是說onTouchEvent方法不會被呼叫

  (手放到螢幕上後,onTouch方法會被一遍一遍的呼叫)

 

 

  如果onTouch返回值為false,表示這個Touch事件沒有被tv完全處理,onTouch返回以後,Touch事件被傳遞給Activity,

  onTouchEvent方法呼叫

  (當把手放到螢幕上後,onTouch方法呼叫一次後,onTouchEvent方法被一遍一遍的呼叫)

 

測試:

1、MyLinearLayout繼承LinearLayout,並重寫onTouchEvent方法

 

複製程式碼
 1 import android.content.Context;
 2 import android.util.AttributeSet;
 3 import android.util.Log;
 4 import android.view.MotionEvent;
 5 import android.widget.LinearLayout;
 6 
 7 public class MyLinearLayout extends LinearLayout{
 8 
 9     public MyLinearLayout(Context context, AttributeSet attrs) {
10         super(context, attrs);
11     }
12 
13     @Override
14     public boolean onTouchEvent(MotionEvent event) {
15         switch (event.getAction()) {
16         case MotionEvent.ACTION_DOWN://0
17             Log.e("TAG", "LinearLayout onTouchEvent 按住");
18             break;
19         case MotionEvent.ACTION_UP://1
20             Log.e("TAG", "LinearLayout onTouchEvent onTouch擡起");
21             break;
22         case MotionEvent.ACTION_MOVE://2
23             Log.e("TAG", "LinearLayout onTouchEvent 移動");
24             break;    
25         }
26         return super.onTouchEvent(event);
27     }
28     
29     @Override
30     public boolean dispatchTouchEvent(MotionEvent ev) {
31         return super.dispatchTouchEvent(ev);
32     }
33     
34     @Override
35     public void setOnTouchListener(OnTouchListener l) {
36         super.setOnTouchListener(l);
37     }
38     
39 }
複製程式碼

 

 

2、在MainActivity中宣告MyLinearLayout,並設定觸控監聽和點選監聽,同時MainActivity自己也有onTouchEvent方法,重寫看結果

複製程式碼
 1 import android.app.Activity;
 2 import android.os.Bundle;
 3 import android.util.Log;
 4 import android.view.MotionEvent;
 5 import android.view.View;
 6 import android.view.View.OnClickListener;
 7 import android.view.View.OnTouchListener;
 8 
 9 public class MainActivity extends Activity {
10 
11     MyLinearLayout ll;
12     @Override
13     protected void onCreate(Bundle savedInstanceState) {
14         super.onCreate(savedInstanceState);
15         setContentView(R.layout.activity_main);
16         
17         ll = (MyLinearLayout) findViewById(R.id.ll);
18         //觸控監聽
19         ll.setOnTouchListener(new OnTouchListener() {
20             
21             @Override
22             public boolean onTouch(View v, MotionEvent event) {
23                 //Log.e("TAG", event.getX()+" "+event.getY());
24                 
25                 switch (event.getAction()) {
26                 case MotionEvent.ACTION_DOWN://0
27                     Log.e("TAG", "LinearLayout onTouch按住");
28                     break;
29                 case MotionEvent.ACTION_UP://1
30                     Log.e("TAG", "LinearLayout onTouch擡起");
31                     break;
32                 case MotionEvent.ACTION_MOVE://2
33                     Log.e("TAG", "LinearLayout onTouch移動");
34                     break;    
35                 }
36                 //事件分發
37                 //1、setOnTouchListener單獨使用的時候返回值需要true,這樣才能保證移動的時候獲取相應的監聽,而非一次監聽(即只有按下事件)
38                     //返回false,表示沒有被處理,將向父View傳遞。只能監聽到view的"按下"事件,"移動"和"擡起"都不能監聽到。因為down事件未結束
39                     //返回true,消耗此事件,表示正確接收並處理,不在分發。"按下""擡起""移動"都能監聽到了
40                 
41                 //2、setOnTouchListener和setOnClickListener同時使用時,
42                     //返回true,事件被onTouch消耗掉了,因而不會在繼續向下傳遞。只能監聽"按下""擡起""移動",不能監聽到"點選";
43                     //返回false,"按下""擡起""移動""點選"都能監聽
44                  
45                 return false;
46                 /**
47                  * onTouch是優先於onClick的,並且執行了兩次,一次是ACTION_DOWN,一次是ACTION_UP(可能還會有多次ACTION_MOVE),
48                  * 因此事件傳遞的順序是先經過OnTouch,再傳遞給onClick
49                  *
50                  */
51             }
52         });
53         
54         ll.setOnClickListener(new OnClickListener() {
55             @Override
56             public void onClick(View v) {
57                 Log.e("TAG", "onClick點選事件");
58             }
59         });
60     }
61     
62     //事件分發:public boolean dispatchTouchEvent(MotionEvent ev)
63     //當監聽到事件時,首先由Activity的捕獲到,進入事件分發處理流程,無論是Activity還是View,事件分發自身也具有消費能力
64     //如果事件分發返回true,表示該事件在本層不再進行分發且已經已經在事件分發自身中被消費了。
65     //如果你不想Activity中的任何控制元件具有任何的事件消費能力,可以直接重寫Activity的dispatchTouchEvent方法,返回true就可以了
66     @Override
67     public boolean dispatchTouchEvent(MotionEvent ev) {
68         // TODO Auto-generated method stub
69         return super.dispatchTouchEvent(ev);
70     }
71     
72     
73     @Override
74     public boolean onTouchEvent(MotionEvent event) {
75         switch (event.getAction()) {
76         case MotionEvent.ACTION_DOWN://0
77             Log.e("TAG", "Activity onTouchEvent按住");
78             break;
79         case MotionEvent.ACTION_UP://1
80             Log.e("TAG", "Activity onTouchEvent擡起");
81             break;
82         case MotionEvent.ACTION_MOVE://2
83             Log.e("TAG", "Activity onTouchEvent移動");
84             break;    
85         }
86         return super.onTouchEvent(event);
87     }
88 
89     
90 }
複製程式碼

 

3、由於MyLinearLayout繼承了LinearLayou,但是XML並不認識他,所以在xml中搭建佈局的時候,一定要包名帶類名

複製程式碼
 1 <com.example.lesson6_ontouch.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 2        android:id="@+id/ll"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent">
 5 
 6        <TextView
 7            android:id="@+id/tv"
 8            android:layout_width="match_parent"
 9            android:layout_height="match_parent"
10            android:text="hsss"/>
11 
12 </com.example.lesson6_ontouch.MyLinearLayout>
複製程式碼

 

測試結果

4、下面貼一位博主的分析 Android事件分發機制完全解析,帶你從原始碼的角度徹底理解(上) 

首先你需要知道一點,只要你觸控到了任何一個控制元件,就一定會呼叫該控制元件的dispatchTouchEvent方法。那當我們去點選按鈕的時候,就會去呼叫Button類裡的dispatchTouchEvent方法,可是你會發現Button類裡並沒有這個方法,那麼就到它的父類TextView裡去找一找,你會發現TextView裡也沒有這個方法,那沒辦法了,只好繼續在TextView的父類View裡找一找,這個時候你終於在View裡找到了這個方法,示意圖如下:

            

然後我們來看一下View中dispatchTouchEvent方法的原始碼:

複製程式碼
1 public boolean dispatchTouchEvent(MotionEvent event) {  
2     if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
3             mOnTouchListener.onTouch(this, event)) {  
4         return true;  
5     }  
6     return onTouchEvent(event);  
7 }  
複製程式碼

我們可以看到,在這個方法內,首先是進行了一個判斷,如果mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED和mOnTouchListener.onTouch(this, event)這三個條件都為真,就返回true,否則就去執行onTouchEvent(event)方法並返回。

先看一下第一個條件,mOnTouchListener這個變數是在哪裡賦值的呢?我們尋找之後在View裡發現瞭如下方法:

 

?
1 2 3 public void setOnTouchListener(OnTouchListener l) {       mOnTouchListener = l; 

mOnTouchListener正是在setOnTouchListener方法裡賦值的,也就是說只要我們給控制元件註冊了touch事件,mOnTouchListener就一定被賦值了。

 

第二個條件(mViewFlags & ENABLED_MASK) == ENABLED是判斷當前點選的控制元件是否是enable的,按鈕預設都是enable的,因此這個條件恆定為true。

第三個條件就比較關鍵了,mOnTouchListener.onTouch(this, event),其實也就是去回撥控制元件註冊touch事件時的onTouch方法。也就是說如果我們在onTouch方法裡返回true,就會讓這三個條件全部成立,從而整個方法直接返回true。如果我們在onTouch方法裡返回false,就會再去執行onTouchEvent(event)方法。

 

現在我們可以結合前面的例子來分析一下了,首先在dispatchTouchEvent中最先執行的就是onTouch方法,因此onTouch肯定是要優先於onClick執行的,也是印證了剛剛的列印結果。而如果在onTouch方法裡返回了true,就會讓dispatchTouchEvent方法直接返回true,不會再繼續往下執行。而列印結果也證實瞭如果onTouch返回true,onClick就不會再執行了。

 

 根據以上原始碼的分析,從原理上解釋了我們前面例子的執行結果。而上面的分析還透漏出了一個重要的資訊,那就是onClick的呼叫肯定是在onTouchEvent(event)方法中的!那我們馬上來看下onTouchEvent的原始碼,如下所示:

 

複製程式碼
  1 public boolean onTouchEvent(MotionEvent event) {
  2         final float x = event.getX();
  3         final float y = event.getY();
  4         final int viewFlags = mViewFlags;
  5         final int action = event.getAction();
  6 
  7         if ((viewFlags & ENABLED_MASK) == DISABLED) {
  8             if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
  9                 setPressed(false);
 10             }
 11             // A disabled view that is clickable still consumes the touch
 12             // events, it just doesn't respond to them.
 13             return (((viewFlags & CLICKABLE) == CLICKABLE
 14                     || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
 15                     || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
 16         }
 17         if (mTouchDelegate != null) {
 18             if (mTouchDelegate.onTouchEvent(event)) {
 19                 return true;
 20             }
 21         }
 22 
 23         if (((viewFlags & CLICKABLE) == CLICKABLE ||
 24                 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
 25                 (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
 26             switch (action) {
 27                 case MotionEvent.ACTION_UP:
 28                     boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
 29                     if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
 30                         // take focus if we don't have it already and we should in
 31                         // touch mode.
 32                         boolean focusTaken = false;
 33                         if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
 34                             focusTaken = requestFocus();
 35                         }
 36 
 37                         if (prepressed) {
 38                             // The button is being released before we actually
 39                             // showed it as pressed.  Make it show the pressed
 40                             // state now (before scheduling the click) to ensure
 41                             // the user sees it.
 42                             setPressed(true, x, y);
 43                        }
 44 
 45                         if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
 46                             // This is a tap, so remove the longpress check
 47                             removeLongPressCallback();
 48 
 49                             // Only perform take click actions if we were in the pressed state
 50                             if (!focusTaken) {
 51                                 // Use a Runnable and post this rather than calling
 52                                 // performClick directly. This lets other visual state
 53                                 // of the view update before click actions start.
 54                                 if (mPerformClick == null) {
 55                                     mPerformClick = new PerformClick();
 56                                 }
 57                                 if (!post(mPerformClick)) {
 58                                     performClick();
 59                                 }
 60                             }
 61                         }
 62 
 63                         if (mUnsetPressedState == null) {
 64                             mUnsetPressedState = new UnsetPressedState();
 65                         }
 66 
 67                         if (prepressed) {
 68                             postDelayed(mUnsetPressedState,
 69                                     ViewConfiguration.getPressedStateDuration());
 70                         } else if (!post(mUnsetPressedState)) {
 71                             // If the post failed, unpress right now
 72                             mUnsetPressedState.run();
 73                         }
 74 
 75                         removeTapCallback();
 76                     }
 77                     mIgnoreNextUpEvent = false;
 78                     break;
 79 
 80                 case MotionEvent.ACTION_DOWN:
 81                     mHasPerformedLongPress = false;
 82 
 83                     if (performButtonActionOnTouchDown(event)) {
 84                         break;
 85                     }
 86 
 87                     // Walk up the hierarchy to determine if we're inside a scrolling container.
 88                     boolean isInScrollingContainer = isInScrollingContainer();
 89 
 90                     // For views inside a scrolling container, delay the pressed feedback for
 91                     // a short period in case this is a scroll.
 92                     if (isInScrollingContainer) {
 93                         mPrivateFlags |= PFLAG_PREPRESSED;
 94                         if (mPendingCheckForTap == null) {
 95                             mPendingCheckForTap = new CheckForTap();
 96                         }
 97                         mPendingCheckForTap.x = event.getX();
 98                         mPendingCheckForTap.y = event.getY();
 99                         postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
100                     } else {
101                         // Not inside a scrolling container, so show the feedback right away
102                         setPressed(true, x, y);
103                         checkForLongClick(0, x, y);
104                     }
105                     break;
106 
107                 case MotionEvent.ACTION_CANCEL:
108                     setPressed(false);
109                     removeTapCallback();
110                     removeLongPressCallback();
111                     mInContextButtonPress = false;
112                     mHasPerformedLongPress = false;
113                     mIgnoreNextUpEvent = false;
114                     break;
115 
116                 case MotionEvent.ACTION_MOVE:
117                     drawableHotspotChanged(x, y);
118 
119                     // Be lenient about moving outside of buttons
120                     if (!pointInView(x, y, mTouchSlop)) {
121                         // Outside button
122                         removeTapCallback();
123                         if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
124                             // Remove any future long press/tap checks
125                             removeLongPressCallback();
126 
127                             setPressed(false);
128                         }
129                     }
130                     break;
131             }
132 
133             return true;
134         }
135 
136         return false;
137     }
複製程式碼

 

相較於剛才的dispatchTouchEvent方法,onTouchEvent方法複雜了很多,不過沒關係,我們只挑重點看就可以了。

 

首先在第23行我們可以看出,如果該控制元件是可以點選的就會進入到第26行的switch判斷中去,而如果當前的事件是擡起手指,則會進入到MotionEvent.ACTION_UP這個case當中。在經過種種判斷之後,會執行到第58行的performClick()方法,那我們進入到這個方法裡瞧一瞧:

複製程式碼
1 public boolean performClick() {  
2     sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);  
3     if (mOnClickListener != null) {  
4         playSoundEffect(SoundEffectConstants.CLICK);  
5         mOnClickListener.onClick(this);  
6         return true;  
7     }  
8     return false;  
9 }  
複製程式碼

可以看到,只要mOnClickListener不是null,就會去呼叫它的onClick方法,那mOnClickListener又是在哪裡賦值的呢?經過尋找後找到如下方法:

 

複製程式碼
1 public void setOnClickListener(OnClickListener l) {  
2     if (!isClickable()) {  
3         setClickable(true);  
4     }  
5     mOnClickListener = l;  
6 } 
複製程式碼

 

 一切都是那麼清楚了!當我們通過呼叫setOnClickListener方法來給控制元件註冊一個點選事件時,就會給mOnClickListener賦值。然後每當控制元件被點選時,都會在performClick()方法裡回撥被點選控制元件的onClick方法。

 

這樣View的整個事件分發的流程就讓我們搞清楚了!不過別高興的太早,現在還沒結束,還有一個很重要的知識點需要說明,就是touch事件的層級傳遞。我們都知道如果給一個控制元件註冊了touch事件,每次點選它的時候都會觸發一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。這裡需要注意,如果你在執行ACTION_DOWN的時候返回了false,後面一系列其它的action就不會再得到執行了。簡單的說,就是當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,才會觸發後一個action。

說到這裡,很多的朋友肯定要有巨大的疑問了。這不是在自相矛盾嗎?前面的例子中,明明在onTouch事件裡面返回了false,ACTION_DOWN和ACTION_UP不是都得到執行了嗎?其實你只是被假象所迷惑了,讓我們仔細分析一下,在前面的例子當中,我們到底返回的是什麼。

參考著我們前面分析的原始碼,首先在onTouch事件裡返回了false,就一定會進入到onTouchEvent方法中,然後我們來看一下onTouchEvent方法的細節。由於我們點選了按鈕,就會進入到第23行這個if判斷的內部,然後你會發現,不管當前的action是什麼,最終都一定會走到第133行,返回一個true。

是不是有一種被欺騙的感覺?明明在onTouch事件裡返回了false,系統還是在onTouchEvent方法中幫你返回了true。就因為這個原因,才使得前面的例子中ACTION_UP可以得到執行。

 

那我們可以換一個控制元件,將按鈕替換成ImageView,然後給它也註冊一個touch事件,並返回false。如下所示:

複製程式碼
1 imageView.setOnTouchListener(new OnTouchListener() {  
2     @Override  
3     public boolean onTouch(View v, MotionEvent event) {  
4         Log.d("TAG", "onTouch execute, action " + event.getAction());  
5         return false;  
6     }  
7 }); 
複製程式碼

執行一下程式,點選ImageView,你會發現結果如下:

 

 

在ACTION_DOWN執行完後,後面的一系列action都不會得到執行了。這又是為什麼呢?因為ImageView和按鈕不同,它是預設不可點選的,因此在onTouchEvent的第14行判斷時無法進入到if的內部,直接跳到第91行返回了false,也就導致後面其它的action都無法執行了。

好了,關於View的事件分發,我想講的東西全都在這裡了。現在我們再來回顧一下開篇時提到的那三個問題,相信每個人都會有更深一層的理解。

1. onTouch和onTouchEvent有什麼區別,又該如何使用?

從原始碼中可以看出,這兩個方法都是在View的dispatchTouchEvent中呼叫的,onTouch優先於onTouchEvent執行。如果在onTouch方法中通過返回true將事件消費掉,onTouchEvent將不會再執行。

另外需要注意的是,onTouch能夠得到執行需要兩個前提條件,第一mOnTouchListener的值不能為空,第二當前點選的控制元件必須是enable的。因此如果你有一個控制元件是非enable的,那麼給它註冊onTouch事件將永遠得不到執行。對於這一類控制元件,如果我們想要監聽它的touch事件,就必須通過在該控制元件中重寫onTouchEvent方法來實現。

2. 為什麼給ListView引入了一個滑動選單的功能,ListView就不能滾動了?

如果你閱讀了Android滑動框架完全解析,教你如何一分鐘實現滑動選單特效 這篇文章,你應該會知道滑動選單的功能是通過給ListView註冊了一個touch事件來實現的。如果你在onTouch方法裡處理完了滑動邏輯後返回true,那麼ListView本身的滾動事件就被遮蔽了,自然也就無法滑動(原理同前面例子中按鈕不能點選),因此解決辦法就是在onTouch方法裡返回false。

3. 為什麼圖片輪播器裡的圖片使用Button而不用ImageView?

提這個問題的朋友是看過了Android實現圖片滾動控制元件,含頁籤功能,讓你的應用像淘寶一樣炫起來 這篇文章。當時我在圖片輪播器裡使用Button,主要就是因為Button是可點選的,而ImageView是不可點選的。如果想要使用ImageView,可以有兩種改法。第一,在ImageView的onTouch方法裡返回true,這樣可以保證ACTION_DOWN之後的其它action都能得到執行,才能實現圖片滾動的效果。第二,在佈局檔案裡面給ImageView增加一個android:clickable="true"的屬性,這樣ImageView變成可點選的之後,即使在onTouch裡返回了false,ACTION_DOWN之後的其它action也是可以得到執行的。

今天的講解就到這裡了,相信大家現在對Android事件分發機制又有了進一步的認識,在後面的文章中我會再帶大家一起探究Android中ViewGroup的事件分發機制。