Android中onTouch方法的執行過程以及和onClick執行發生衝突的解決辦法

尼古拉斯_趙四發表於2013-12-23

今天在做專案的時候遇到一個問題,就是怎麼讓ListView中的item點選後其內部的內容跟著變色,比如現在我的item佈局中有一個TextView,現在點選item的時候,讓其背景色發生改變,這個我們可以為item佈局背景定義一個selctor.xml就可以了,但是現在的問題是item內容佈局中的TextView中的內容也要跟著變色,這個立馬想到了觸控監聽器onTouch方法,只需要在ACTION_UP和ACTION_DOWN中實現對TextView的字型顏色的改變就可以了,但是現在遇到一個問題,就是當我實現了onTouch方法的時候,ListView的點選事件就沒有了(setOnItemClick方法),上網檢視了很多內容就是說onTouch方法的返回值問題,如果onTouch方法返回true的話,setOnItemClick方法就不執行了,但是我把onTouch的返回值設定成false之後,出現一個問題就是onTouch方法只執行了一次就是ACTION_DOWN中的程式碼,而ACTION_UP中的程式碼並沒有執行,很是糾結的時候最後找到的方法就是在ListView的介面卡中的getView方法中對contentView實現onClick方法就可以了,具體原因下面來分析一下:


問題是解決了,但是內部的原理要弄清楚一下,於是檢視了View和ViewGroup中的關於onTouch,onClick方法的原始碼,問題總算弄明白了:

現在主要有兩個問題:

(1) 第一個問題是控制元件本省的onTouch和onClick方法的執行衝突

(2) 第二個問題是子控制元件和父控制元件之間的onTouch執行過程的分析


首先自定義一個MyLayout繼承LinearLayout,重寫dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent方法,程式碼如下:

package com.bbdtek.demo;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;

public class MyLayout extends LinearLayout {

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e("Demo:","父View的dispatchTouchEvent方法執行了");
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e("Demo:","父View的onInterceptTouchEvent方法執行了");
return super.onInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("Demo:","父View的onTouchEvent方法執行了");
return super.onTouchEvent(event);
}

public MyLayout(Context context) {
super(context);
}

public MyLayout(Context context,AttributeSet attr) {
super(context,attr);
}

}

在這三個方法中列印一段資訊,有助於檢視這三個方法的執行流程


下面是自定義一個MyTextView繼承TextView,重寫dispatchTouchEvent,onTouchEvent方法(這裡沒有onInterceptTouchEvent方法,原因很簡單,就是TextView是一個View沒有子View了,不需要攔截資訊了)程式碼如下:

package com.bbdtek.demo;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.widget.TextView;

public class MyTextView extends TextView{

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e("Demo:","子View的dispatchTouchEvent方法執行了");
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("Demo:","子View的onTouchEvent方法執行了");
return super.onTouchEvent(event);
}

public MyTextView(Context context) {
super(context);
}

public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}

public MyTextView(Context context, AttributeSet attrs,int defStyle) {
super(context, attrs, defStyle);
}

}

在方法中輸出log資訊,便於檢視方法的執行流程


定義的一個佈局檔案:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
        <com.bbdtek.demo.MyLayout 
            android:id="@+id/layout"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:orientation="vertical">
            <com.bbdtek.demo.MyButton 
                android:id="@+id/btn"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="點我"/>
            <com.bbdtek.demo.MyTextView
                android:id="@+id/txt1" 
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="我是個好人我是個好人呢我是啊速度將法律上的經費福建省的雷鋒精神多了爽膚水兩地分居SD卡颶風桑迪快樂分類資料的法律進多少"
                android:textSize="25dp"/>
        </com.bbdtek.demo.MyLayout>
</LinearLayout>


MyLayout中包含一個MyTextView控制元件:


下面來看一下測試程式碼:

package com.bbdtek.demo;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;

public class AndroidDemoActivity extends Activity{

    public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.main);
       MyTextView txt1 = (MyTextView)findViewById(R.id.txt1);
       //MyLayout layout = (MyLayout)findViewById(R.id.layout);
       txt1.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
Log.e("Demo:","點選了txt1");
}});
       //給MyTextView定義一個onTouchListener監聽器
       txt1.setOnTouchListener(new OnTouchListener(){
    @Override
    public boolean onTouch(View v, MotionEvent event) {
    if(event.getAction() == MotionEvent.ACTION_DOWN){
    Log.e("Demo:","txt1按下");
    }else if(event.getAction() == MotionEvent.ACTION_UP){
    Log.e("Demo:","txt1彈起");
    }
    return false;
    }});
       //給MyLayout定義一個onTouchListener監聽器
       /*layout.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
Log.e("Demo:","點選了父layout");
}});*/
       
    }
}

看一下執行結果:


可以看到先執行的是MyTextView的dispatchTouchEvent方法(這個方法是每次觸發onTouch方法都會執行的),然後是執行了onTouch方法中的ACTION_UP中的程式碼,然後執行了MyTextView中的onTouchEvent方法,當使用者彈起手指的時候又一次執行了這樣的一個過程,最後就是執行了onClick方法,在這裡就來看一下onTouchEvent中的原始碼:


因為TextView繼承View,可以檢視View中的onTouchEvent方法,在這個方法中又呼叫了ViewGroup中的onTouchEvent方法,下面在來看一下ViewGroup中的onTouchEvent方法:


在ACTION_UP中的這段程式碼就是執行了onClick方法,具體可以看一下performClick方法解釋:


mOnClickListener就是OnClickListener監聽器,執行了onClick方法,所以上面的onClick方法是在ACTION_UP之後執行了。

下面在來看一下這種情況,現在把onTouch方法的返回值改成true:看一下執行結果:


可以看到首先還是執行了MyTextView的dispatchTouchEvent方法,然後執行了onTouch中的ACTION_DOWN程式碼,同樣當使用者的彈起手指的時候執行了同樣的過程,現在的問題是這樣的執行過程為什麼和上面的不一樣呢?

首先來看一下有哪些不一樣的地方:

第一個不一樣的地方就是”子View的onTouchEvent方法執行了“這句話沒有執行,那就是MyTextView中的onTouchEvent方法沒有執行了,來看一下原始碼:


在View中的dispatchTouchEvent方法中可以看到,是先執行OnTouchListener監聽器中的onTouch方法,如果onTouchListener不為null,並且onTouch方法返回false的時候才執行onTouchEvent方法,現在我們把onTouch方法的返回值變成了true,所以onTouchEvent方法就不執行了。這裡一定要注意onTouchEvent方法和onTouch方法的區別。

第二個不一樣的地方就是onClick方法沒有執行了,對於這樣的問題,在原始碼中沒有找到答案,但是記住一點就可以了那就是如果onTouch方法返回true,說明View這次消費了這次事件,所以就不會再執行後續的onClick方法了,

從上面的例子可以看出來:onClick方法的執行時依賴於onTouch方法的,下面就總結一下onClick和onTouch兩個方法之間的執行關係:

如果該View 是disable 狀態:那麼給它加上pressed標誌位,重繪一下(在螢幕上顯示為灰顯按下去的效果)
如果該View不是disable狀態,並且是clickable,那麼在ACTION_DOWN的時候會加上pressed標誌位,並啟動一個timer(長按事件onLongClickListener)。重繪一下(顯示出按下去的效果)。此時還是MOVE事件監視該View的狀態,如果這個MOVE滑出了該View的範圍,那麼會復位View的click狀態。UP事件時,會檢查是否已經執行過LongPress,如果已經執行了LongPress,那麼就不執行了Click,反之,會取消掉LongPress的Timer,然後在執行Click。
在UP事件的最後,做一下收尾處理,完事。
所以TouchEvent是Click 和 LongPress的底層實現,View的派生類如Button等等就不需要重寫OnTouchEvent。


現在來看一下第二個問題就是:父控制元件和子控制元件的onTouch方法的執行流程:

首先來看一下測試程式碼:

package com.bbdtek.demo;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;

public class AndroidDemoActivity extends Activity{

    public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.main);
       MyTextView txt1 = (MyTextView)findViewById(R.id.txt1);
       MyLayout layout = (MyLayout)findViewById(R.id.layout);
       txt1.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
Log.e("Demo:","點選了txt1");
}});
       //給MyTextView定義一個onTouchListener監聽器
       txt1.setOnTouchListener(new OnTouchListener(){
    @Override
    public boolean onTouch(View v, MotionEvent event) {
    if(event.getAction() == MotionEvent.ACTION_DOWN){
    Log.e("Demo:","txt1按下");
    }else if(event.getAction() == MotionEvent.ACTION_UP){
    Log.e("Demo:","txt1彈起");
    }
    return false;
    }});
     //給MyTextView定義一個onTouchListener監聽器
       layout.setOnTouchListener(new OnTouchListener(){
    @Override
    public boolean onTouch(View v, MotionEvent event) {
    if(event.getAction() == MotionEvent.ACTION_DOWN){
    Log.e("Demo:","父layout按下");
    }else if(event.getAction() == MotionEvent.ACTION_UP){
    Log.e("Demo:","父layout彈起");
    }
    return false;
    }});
       //給MyLayout定義一個onTouchListener監聽器
       layout.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
Log.e("Demo:","點選了父layout");
}});
       
    }
}

點選MyTextView後觀察log資訊如下:


這樣就可以清楚的看到onTouch的執行過程了:


這三種情況就詳細說明了這個執行過程,其中dispatchTouchEvent方法沒有標註,比較簡單,首先會執行父控制元件的dispatchTouchEvent方法,下面在來看一種情況,就是在測試程式碼中將這些程式碼註釋:

/*txt1.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
Log.e("Demo:","點選了txt1");
}});*/

看一下執行結果:


這裡就是把MyTextView的點選監聽器註釋了,但是執行的結果差距很大:我們一步一步來分析一下:

首先來看一下View的setOnClickListener方法的原始碼:


這個方法很簡單就是設定監聽器mOnClickListener,但是之前還有一段程式碼那就是setClick(true);從註釋中可以看到,如果設定了點選監聽器說明這個控制元件就是可以點選的了,再來看一下setClick方法:


這個方法就是設定一下可以點選的標誌變數了,現在再來看一下onTouchEvent方法:


由於onTouchEvent方法的程式碼太長了,不要截圖,但是大體意思是如果這個控制元件是可以點選的,那麼onTouchEvent方法就返回true,當方法onTouchEvent方法返回true的時候說明這次事件被該控制元件消費了,不會再往上傳遞了,所以,我們給txt1新增onClick監聽器的時候,執行結果中可以看到父控制元件的onTouch方法沒有執行,當我們把onClick監聽器刪除的時候,父控制元件的onTouch方法執行了,這次的事件被父控制元件消費了,所以txt1的onTouch中的ACTION_UP中的程式碼就沒有執行了。為了驗證這一點其實很簡單,你可以直接設定txt1.setClickable(false);這個方法通過設定false和true來觀察結果;這裡還有一個重要的資訊是Android中像Button,CheckBox這樣的控制元件預設都是可以點選的,所以不需要setClickable(true)方法來實現了,但是像TextView這樣的控制元件預設是不可點選的,所以要通過setClickable(true)這樣的方法來實現。

當然你也可以通過設定dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent這三個方法的返回值來觀察結果,具體可以檢視我的另外一篇文章:http://blog.csdn.net/jiangwei0910410003/article/details/16986039

至此,上述的兩個問題就解決了,最大的收穫還是學會了怎樣去查詢問題,最好的方式就是看原始碼!現在回到剛開始的地方,就是那個ListView為什麼用setOnItemClick這個方法和onTouch這個方法有衝突:現在來想一想其實很簡單了,因為如果onTouch方法中返回true的話,這次事件就被ListView中的item控制元件消費了,所以不會執行ListVIew的setOnItemClick這個方法了,如果onTouch方法返回false,那麼會執行setOnItemClick方法,同時事件會被ListView消費了,所以onTouch方法只會執行ACTION_DOWN中的程式碼了,這裡也說明了一點就是ListView的setOnItemClick方法和在getView中單獨給item新增onClick方法的效果是不一樣的。

同樣還有另外的一個問題就是ListView中的Item中如果有Button,CheckBox等這樣的元件的話,ListView中的setOnItemClick方法就是失效了,原因是Item沒有獲取焦點,焦點被Button等控制元件預設獲取到了,這裡有兩種解決方法:

第一種就是讓Button控制元件失去焦點,可以在佈局檔案中設定程式碼:android:focusable="false"即可

第二種就是把setOnItemClick方法中的邏輯程式碼方法getView中的contentView的onClick方法中

為什麼要這麼做,有待研究呀!今天就寫到這裡了,頭都寫大了,很糾結,也很開心呀,如果發現有什麼不正確的地方,希望給予批評和指正,本人將不勝感激!


《Android應用安全防護和逆向分析》


點選立即購買:京東  天貓  亞馬遜 噹噹



更多內容:點選這裡

關注微信公眾號,最新技術乾貨實時推送

編碼美麗技術圈
微信掃一掃進入我的"技術圈"世界

掃一掃加小編微信
新增時請註明:“編碼美麗”非常感謝!

相關文章