按鈕點擊事件詳解
最近一個項目需要給應用初始界面上的動態按鈕添加在不同狀態的變換效果,如點擊(俗一點也可稱為按壓)后實現背景圖的更換或者圖標的縮放等效果。由於按鈕點擊的時間有長有短,所以采用OnTouchListener監聽器對點擊事件進行監聽,並利用對應的onTouch(View v, MotionEvent event)方法來實現按鈕圖標的變換效果(背景圖更換或者圖標縮放)。但是項目中除了利用Touch事件來處理按鈕基本的變換外,還需要響應LongClick或者Click事件來為用戶做進一步的響應,即Touch和Click事件分別完成不同的任務。
那么問題來了,表面上看Touch、LongClick及Click這三個事件的關系很普通(均可由用戶點擊組件觸發),在一般的應用中對它們中的個別進行監聽也不太會遇到什么奇怪的現象。但是深入研究與測試之后,會發現當它們一起用的時候,有太多地方需要注意,否則很容易出錯。下面就來看看有哪些平時不太注意卻可能出現意外的地方。
讀書期間一直用VC++,現在轉為Android,個人覺得在對於點擊事件的監聽即響應的實現這個點上很多語言間非常相似,即解決問題的思路是相通的。
本文的測試案例是用Android進行實現,先看下面兩段代碼。
1、xml布局文件
1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:tools="http://schemas.android.com/tools" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:gravity="center" 6 tools:context="com.example.eventtest.MainActivity" > 7 8 <ImageView android:id="@+id/imageView" 9 android:layout_width="wrap_content" 10 android:layout_height="wrap_content" 11 android:src="@drawable/ic_launcher" /> 12 13 </LinearLayout>
由於只是觀察Touch、LongClick及Click三者之間對點擊事件的響應關系,所以整個界面布局中僅僅放置了一個ImageView組件。
2、Java代碼實現
1 package com.example.eventtest; 2 3 import android.support.v7.app.ActionBarActivity; 4 import android.os.Bundle; 5 import android.util.Log; 6 import android.view.Menu; 7 import android.view.MenuItem; 8 import android.view.MotionEvent; 9 import android.view.View; 10 import android.widget.ImageView; 11 12 13 public class MainActivity extends ActionBarActivity { 14 15 private ImageView imageView = null; 16 private String TAG = "MainActivity"; 17 @Override 18 protected void onCreate(Bundle savedInstanceState) { 19 super.onCreate(savedInstanceState); 20 setContentView(R.layout.activity_main); 21 22 imageView = (ImageView)findViewById(R.id.imageView); 23 imageView.setOnTouchListener(mOnTouchListener); 24 imageView.setOnLongClickListener(mOnLongClickListener); 25 imageView.setOnClickListener(mOnClickListener); 26 } 27 28 View.OnTouchListener mOnTouchListener = new View.OnTouchListener() { 29 30 @Override 31 public boolean onTouch(View arg0, MotionEvent event) { 32 // TODO Auto-generated method stub 33 if(event.getAction() == MotionEvent.ACTION_DOWN){ 34 Log.d(TAG, "touch down"); 35 return false; //1 FALSE 36 } 37 else if(event.getAction() == MotionEvent.touch move){ 38 Log.d(TAG, "touch move"); 39 return false; //2 FALSE 40 } 41 else if(event.getAction() == MotionEvent.ACTION_UP){ 42 Log.d(TAG, "touch up"); 43 return false; //3 FALSE 44 } 45 return false; 46 } 47 }; 48 49 50 View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() { 51 52 @Override 53 public boolean onLongClick(View arg0) { 54 // TODO Auto-generated method stub 55 Log.d(TAG, "long click"); 56 return false; //4 FALSE 57 } 58 }; 59 60 View.OnClickListener mOnClickListener = new View.OnClickListener() { 61 62 @Override 63 public void onClick(View arg0) { 64 // TODO Auto-generated method stub 65 Log.d(TAG, "click"); //5 NULL 66 } 67 }; 68 69 @Override 70 public boolean onCreateOptionsMenu(Menu menu) { 71 // Inflate the menu; this adds items to the action bar if it is present. 72 getMenuInflater().inflate(R.menu.main, menu); 73 return true; 74 } 75 76 @Override 77 public boolean onOptionsItemSelected(MenuItem item) { 78 // Handle action bar item clicks here. The action bar will 79 // automatically handle clicks on the Home/Up button, so long 80 // as you specify a parent activity in AndroidManifest.xml. 81 int id = item.getItemId(); 82 if (id == R.id.action_settings) { 83 return true; 84 } 85 return super.onOptionsItemSelected(item); 86 } 87 }
從上面給出的Java代碼可以看出:
1、分別為點擊ImageView組件后的Touch、LongClick及Click事件設置了OnTouchListener、OnLongClickListener及OnClickListener監聽器,並且在對應的響應方法onTouch()、onLongClick()及onClick()中進行了日志輸出函數的調用與返回值的設定。其中返回值的設定是本文的關鍵,某個響應方法返回的是true還是false,對於后續的響應方法的影響非常大,后面會慢慢地進行分析。
2、為了觀察組件點擊后的一系列響應情況,利用Log類的d(Tag, msg)方法來輸出運行日志,由於系統本身在應用運行時會輸出很多我們不必要查看的信息,所以最好在logcat中設置一個日志信息輸出過濾器,名稱要和程序中的TAG字串相同。如此處的TAG為主類名字串“MainActicity”,那么Filter的名稱也要取為“MainActicity”,設置完之后保存,然后在logcat中選擇debug選項(上述的d()方法對應debug,e()對應error(),還有幾個類別感興趣的朋友可以自己研究),那窗口中就會只輸出我們設定的打印信息了。如下圖中的輸出結果,看着簡單、舒服。

3、說明,java代碼行35、39、43、56及65的注釋是方法返回值的說明,由於onClick()方法無返回值,所以用NULL表示。在測試中會對各種方法的返回值進行改變,形成不同的組合,然后通過輸出的日志信息觀察它們的響應情況。由於Android中對點擊事件的響應順序為touch down-touch move-long click-touch up-click(當然,不是每次點擊均會產生所有的事件,這只是完整的流程描述),所以描述返回值組合時也是按照這個順序。如上述代碼中方法的返回值組合為false-false-false-false-null,而且對組件的短按、長按、移開(短按+移開或長按+移開,算兩種不同的狀態)(移開表示最終沒有觸發Click事件,但有可能觸發LongClick事件)這四種點擊狀態都進行了測試,對於是否執行點擊狀態用yes/no表示,那么上面代碼運行后組件的長按+移開的完整組合(返回值和操作標記)就是false-false-false-false-null-no-yes-yes。
3、常規測試結果
說了這么多,所謂有圖有真相,不給出結果怎么說得清呢。如果表述有不恰當或者內容有缺陷的地方還希望朋友您能夠指出,謝謝!
按照上面的返回值組合(各方法均返回false),四種點擊狀態的打印日志信息如下:
1、短按(組合:false-false-false-false-null-yes-no-no)

短按其實專業點說是手指(或者觸筆,有些手機和平板會配備)在應用界面的組件上輕觸,結果為touch down-touch up-click,有時候輸出結果會是touch down-touch move-touch up-click(產生touch move事件但還至於到long click事件)。
2、長按(組合:false-false-false-false-null-no-yes-no)

這里就產生了long click事件,輸出為結果為touch down-touch move-long click-touch move-touch up-click,其實touch move事件的產生與否、個數和手指按壓與抬起的速度有關,不必深究(后面還會提到不必深究的原因)。
3、短按移開(組合:false-false-false-false-null-yes-no-yes)

由於短按不會產生long click事件,而按壓組件並最終移開后不會產生click事件,所以結果為touch down-touch move-touch up。
4、長按移開(組合:false-false-false-false-null-no-yes-yes)

到這里,就不要多解釋了,日志輸出結果為touch down-touch move-long click-touch move-touch up。
4、測試結果分析
上面的測試結果相信大部分人都預想到了,是不是覺得挺簡單的呢?
但是得注意一個細節:上述測試結果是在所有事件響應方法的返回值均為false(onClick()除外,為void,后面會用null表示)的情況下得到的。這意味着什么,如果各方法的返回值不全是false又會怎么樣呢?下面先從全局層面來簡單解釋一下Android對於點擊事件的響應流程吧。
之前提到過,點擊事件的一般響應流程為touch down-touch move-long click-touch move-touch up-click。這是整體響應情況的描述,前提就是像上面程序中設置的那樣——返回值均為false。從順序的角度出發,如果touch down事件對應的模塊在執行完自身的實現后返回false,即它還想把當前的點擊狀態繼續向上(上層類,一般為父類)傳遞,而不是扼殺在自己的搖籃里;按住一段時間后,touch move事件觸發;隨后long click事件產生,返回false,繼續交給上層處理;這中間還會有touch move事件產生;當手指抬起時(不是移開),touch up事件觸發,返回false;最終到了click事件,本次點擊事件結束。
還有,組件按壓並移開,是不會觸發click事件的,如果按壓時間夠久,會有long ckick事件發生。
好了,簡單的測試與描述就到這里。下面該挑戰復雜的返回值組合了,希望大家還保持着清醒的頭腦,之后的描述不會那么啰嗦,組合值及輸出日志將以表格形式進行簡潔明了的展現。代碼中進行的相應改變也不再給出,感興趣的朋友自己實現一下吧。
1、
| touch down | touch move | touch up | long click | click | 輕觸 | 長按 | 按住移出 | 響應結果 |
| FALSE | BOTH | FALSE | FALSE | NULL | YES | NO | NO | down-move-up-click |
| FALSE | BOTH | FALSE | FALSE | NULL | NO | YES | NO | down-move...-long click-move..-up-click |
| FALSE | BOTH | FALSE | FALSE | NULL | YES | NO | YES | down-move...-up |
| FALSE | BOTH | FALSE | FALSE | NULL | NO | YES | YES | down-move...-long click-move...-up |
上面描述過的測試狀態的返回值組合和輸出日志信息也重新以表格形式給出,touch down簡化為down,其他類似,move...表示多個touch move事件的輸出信息。
2、
| touch down | touch move | touch up | long click | click | 輕觸 | 長按 | 按住移出 | 響應結果 |
| FALSE | BOTH | FALSE | TRUE | NULL | YES | NO | NO | down-move-up-click |
| FALSE | BOTH | FALSE | TRUE | NULL | NO | YES | NO | down-move...-long click-move..-up |
| FALSE | BOTH | FALSE | TRUE | NULL | YES | NO | YES | down-move...-up |
| FALSE | BOTH | FALSE | TRUE | NULL | NO | YES | YES | down-move...-long click-move...-up |
發現不同之處了嗎?將onLongClick()方法返回值設置為true,相應地,組件長按並抬起后,click事件並沒有觸發。即點擊事件到touch up事件后就不在往上傳遞觸發消息了。到此,可以解釋一下touch move事件了,可以看到將其對應的響應模塊的返回值設置為true或者false,對執行結果不會產生任何影響,它只需要完成自己的任務就好,其他的模塊和它並沒有多大的關系。注意,表格中標紅的部分是以第1種測試狀態的組合和輸出做為基礎的,下同。
3、
| touch down | touch move | touch up | long click | click | 輕觸 | 長按 | 按住移出 | 響應結果 |
| TRUE | BOTH | FALSE | FALSE | NULL | YES | NO | NO | down-move-up |
| TRUE | BOTH | FALSE | FALSE | NULL | NO | YES | NO | down-move...-up |
| TRUE | BOTH | FALSE | FALSE | NULL | YES | NO | YES | down-move...-up |
| TRUE | BOTH | FALSE | FALSE | NULL | NO | YES | YES | down-move...-up |
將touch down事件的返回值設置為true,long click和click事件居然都沒有觸發,結合點擊事件的響應流程就容易理解了。即touch down返回true之后,表示不再需要上層類的協助(這里指long click和click事件)。沒有特殊的情況下,touch down-move-up是組件touch事件的一個完整流程。當然,后面會出現特殊的情況。
4、
| touch down | touch move | touch up | long click | click | 輕觸 | 長按 | 按住移出 | 響應結果 |
| FALSE | BOTH | TRUE | BOTH | NULL | YES | NO | NO | down-move-up-long click |
| FALSE | BOTH | TRUE | BOTH | NULL | NO | YES | NO | down-move...-long lick-move...-up |
| FALSE | BOTH | TRUE | BOTH | NULL | YES | NO | YES | down-move...-up |
| FALSE | BOTH | TRUE | BOTH | NULL | NO | YES | YES | down-move...-long lick-move...-up |
將touch up事件的返回值設置為true,組件輕觸並抬起時,出現了奇怪的現象:觸發了long click事件,並且long click事件處理完之后,不管返回值是false還是true,均沒有再出現touch move和touch up事件。
其實,結合點擊事件的流程,在touch up事件返回true之后,click事件不會觸發很容易理解,但為什么還會產生long click呢?這是目前沒有搞懂的一個疑問!!!
5、
| touch down | touch move | touch up | long click | click | 輕觸 | 長按 | 按住移出 | 響應結果 |
| FALSE | BOTH | BOTH | 無 | 無 | YES | NO | NO | down |
| FALSE | BOTH | BOTH | 無 | 無 | NO | YES | NO | down |
| FALSE | BOTH | BOTH | 無 | 無 | YES | NO | YES | down |
| FALSE | BOTH | BOTH | 無 | 無 | NO | YES | YES | down |
這里主要是對組件ImageView監聽器的設置進行了改變,“無”表示沒有設置對應的監聽器,即將java代碼的24、25行注釋掉。從響應結果可以看出,在touch down事件返回false時,會希望上層類的long click或者click事件來處理,而此時組件只設置了OnTouchListener監聽器,所以會一直等待,永遠不會觸發touch move和touch up事件了。
6、
| touch down | touch move | touch up | long click | click | 輕觸 | 長按 | 按住移出 | 響應結果 |
| TRUE | BOTH | BOTH | 無 | 無 | YES | NO | NO | down-move-up |
| TRUE | BOTH | BOTH | 無 | 無 | NO | YES | NO | down-move...-up |
| TRUE | BOTH | BOTH | 無 | 無 | YES | NO | YES | down-move...-up |
| TRUE | BOTH | BOTH | 無 | 無 | NO | YES | YES | down-move...-up |
有了以上的基礎,這種情況就很好理解了其實和第3種測試狀態是類似的(甚至相同)。在touch down事件返回true后,不管有沒有設置long click和click事件的監聽器,都不會再觸發了,而touch move-up事件正常了。
5、總結
本篇文章通過對Touch、Long Click及Click事件的響應方法設置各種不同返回值組合,測試組件點擊狀態的響應情況。對於帶問好的標題,可以確定的是:點擊組件后產生什么事件,做怎樣的實現,是由編程者進行控制。
遺留問題,即上述測試第4種情況:輕觸組件,在touch up事件返回true之后,為什么會觸發long click?
更新,針對這個問題查了一些資料,自己也思考了很久,目前個人的理解是:
當touch down返回false時,是希望有上一層的click或者long click監聽器來處理當前傳遞的點擊事件;
由於是輕觸,在touch move到touch up過程中不會觸發long click事件,理論上up后會由click監聽器處理;
但是偏偏touch up返回的是true,即click監聽器不會捕捉該事件,繼續傳遞;
那這時候,能夠處理該事件的只有路過的long click監聽器了;
