android 事件分發,解決由於listview中實時重新整理,導致子view點選事件失效
近期由於個人的某些因素作怪,導致沒有很好地總結和積累,主要是最近一段時間,大多數接觸的都是第三方的sdk ,在一些介面問題上造成了很多困擾,很是麻煩,並且說明文件也不詳細,所以每每遇到一些問題都要等待很久才能解決。
好了,廢話不多說了。下面開始今天的正文。(最近發現這個問題好像網上解決的並不多,囉嗦太多不好意思哈,想知道解決辦法,可以直接看最後一段)android 之事件分發機制。並且結合本人開發中遇到的實際場景來說明一下解決辦法。
本人近期在做檔案的上傳和下載,這個必定會用到progressBar 進度條,因為這個是描述下載和上傳進度的最顯著的體現。我將每條記錄放入listview的item中,所以每個item中必定需要包含進度條。進度條這個東西當然是實時更新的。更新頻率也非常的高,所以每次進度有更新我都會進行adapter.notifySetDatachange 這樣才能實時看到介面變化,那麼問題來了。我們在上傳下載過程中肯定需要對這個任務進行暫停或開始的操作,這些按鈕也是在每個item中都存在的。那麼在任務進行中的時候,我想點選暫停按鈕,把任務暫停。但是發現無論怎麼點選,onclick中的程式碼都不會執行,這個讓我感到很奇怪。排除了一些基本的問題,我想到了會不會是由於實時重新整理頁面導致點選事件失效。
看一下效果圖吧:
想到這裡我就想寫一個demo來測試一下。
首先我們都知道安卓的觸屏事件其實都是通過
public boolean dispatchTouchEvent(MotionEvent ev) {}
這個事件分發機制來實現。首先這個分發都是從activity中的windowManager來開始的,
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TGA,"dispatchTouchEvent_ACTION_DOWN" );
// return false;
break;
case MotionEvent.ACTION_MOVE:
Log.e(TGA,"dispatchTouchEvent_ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TGA,"dispatchTouchEvent_ACTION_UP");
// return false;
case MotionEvent.ACTION_CANCEL:
Log.e(TGA,"dispatchTouchEvent_ACTION_CANCEL");
break;
}
return super.dispatchTouchEvent(ev);
}
事件的從actionDown開始的,如果在activity 中有View來接受這個down事件,那麼這個事件就會傳給view的分發,其中還有ViewGroup ,因為它有子View 所以此時又會有事件傳遞,檢視是否有子View接受這個事件,如果沒有那就自己消費掉。假設就是一個button那麼就不用分發了,直接自己消費就可以,自己消費的話就是在View的ontouchEvent中進行觸發。
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TGA,"ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TGA,"ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TGA,"ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TGA,"ACTION_CANCEL");
break;
}
return false;
}
});
所以很容易理解,事件就是一件一件傳遞下去那麼我們的Onclick事件是什麼時候觸發呢,這個就是在MotionEvent 中的Action_UP之後便會觸發,就是當你的收擡起來之後click事件才真正執行。首先我們看一下demo的原始碼:
package com.example.szh.motioneventtest;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ListView;
public class MainActivity extends AppCompatActivity {
private final String TGA=MainActivity.this.getClass().getName();
private Context mContext;
private Button button;
private ListView listView;
private ListAdapter adapter;
private Handler mHandler=new Handler(){
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContext=this;
initData();
findViews();
bindViews();
setListener();
}
private void initData() {
adapter=new ListAdapter();
}
private void findViews() {
button=(Button)findViewById(R.id.button);
listView=(ListView)findViewById(R.id.listview);
}
private void bindViews() {
listView.setAdapter(adapter);
new Thread(new Runnable() {
@Override
public void run() {
while(true){
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
adapter.notifyDataSetChanged();
}
},10);
}
}
}).start();
}
private void setListener() {
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TGA,"onClick()");
}
});
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TGA,"ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TGA,"ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TGA,"ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TGA,"ACTION_CANCEL");
break;
}
return false;
}
});
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Log.e(TGA,"onItemClick");
}
});
listView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TGA,"listView_ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TGA,"listView_ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TGA,"listView_ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TGA,"listView_ACTION_CANCEL");
break;
}
return false;
}
});
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TGA,"dispatchTouchEvent_ACTION_DOWN");
// return false;
break;
case MotionEvent.ACTION_MOVE:
Log.e(TGA,"dispatchTouchEvent_ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TGA,"dispatchTouchEvent_ACTION_UP");
// return false;
case MotionEvent.ACTION_CANCEL:
Log.e(TGA,"dispatchTouchEvent_ACTION_CANCEL");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TGA,"Activity_ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TGA,"Activity_ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TGA,"Activity_ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TGA,"Activity_ACTION_CANCEL");
break;
}
return super.onTouchEvent(event);
}
class ListAdapter extends BaseAdapter{
@Override
public int getCount() {
return 5;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if(convertView ==null){
holder=new ViewHolder();
convertView= LayoutInflater.from(MainActivity.this).inflate(R.layout.tem_list,null);
holder.itemBT=(Button)convertView.findViewById(R.id.button);
holder.itemBT.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TGA,"Item_ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TGA,"Item_ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TGA,"Item_ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TGA,"Item_ACTION_CANCEL");
break;
}
return false;
}
});
holder.itemBT.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TGA,"Item_onClick");
}
});
convertView.setTag(holder);
}else{
holder=(ViewHolder) convertView.getTag();
}
return convertView;
}
}
class ViewHolder{
Button itemBT;
}
}
頁面效果是這樣的:
很簡單上面的5個button是在listview的item中的,而最後一個button是直屬於activity。
其中為了模擬實時重新整理,通過一個while迴圈,然後只做重新整理的事件。
接下來我們看當我們按下activity中的button的日誌:
08-12 02:00:26.526 19966-dispatchTouchEvent_ACTION_DOWN
08-12 02:00:26.526 19966-
ACTION_DOWN
08-12 02:00:26.646 19966-
dispatchTouchEvent_ACTION_UP
08-12 02:00:26.646 19966-dispatchTouchEvent_ACTION_CANCEL
08-12 02:00:26.646 19966-
ACTION_UP
08-12 02:00:26.726 19966-
onClick()
從日誌中我們可以很明顯的分析出,每一個操作執行的順序。這個日誌看起好像沒有影響,實則不然,因為當我多次測試就會發現onclick的執行有時會在Action_UP之後500ms以後才會執行,這個是很致命的。實際開發中其實不允許這種現象出現的。可以看下面我的測試日誌:08-12 02:05:44.966 19966-dispatchTouchEvent_ACTION_DOWN
可以看出來時間差了800ms。
08-12 02:05:44.966 19966-
ACTION_DOWN
08-12 02:05:45.046 19966-
dispatchTouchEvent_ACTION_UP
08-12 02:05:45.046 19966-dispatchTouchEvent_ACTION_CANCEL
08-12 02:05:45.046 19966-
ACTION_UP
08-12 02:05:45.846 19966-
onClick()
而在listview的子View中這個現象就更明顯了。此時我們點選item中的button 日誌顯示
08-12 02:11:33.776 19966-dispatchTouchEvent_ACTION_DOWN
08-12 02:11:33.776 19966-
Item_ACTION_DOWN
08-12 02:11:33.846 19966-
dispatchTouchEvent_ACTION_UP
08-12 02:11:33.846 19966-dispatchTouchEvent_ACTION_CANCEL
08-12 02:11:33.846 19966-
Item_ACTION_CANCEL
很明顯Item中的事件走了down 就直接cancle ,沒有走up 當然也不會執行onclick ,所以說,會導致我們的點選事件失效。但是通過日誌我們其實發現,down是一定會執行的,所以針對這個問題,我們的解決辦法就是,可以把點選事件寫在down的事件中,這樣就能確保可以執行了,但其實這樣的效果不好,讓使用者覺得不像是點選事件。針對這個問題我的處理方式,是將notifysetDataChange 這個呼叫放在一個if(isclick){}中,每次我們down執行的時候我們將改變這個isclick 的值,讓更新頁面無法執行,然後通過handler.sendMsgdelay(what,500);這樣再恢復isclick的值,這樣在這500ms的時間中已經足夠down執行到click中了。確保了click中的事件執行完畢,當然之後一切又恢復了,介面依然可以實時重新整理,在這500ms介面會短暫的不重新整理,但由於時間相對可以接受,所以並不影響體驗。這樣就完美解決我們的問題啦。
接下來我把while迴圈去掉不讓介面實時刷險,我們看一下點選事件日誌是怎麼走的。明白為什麼實時重新整理會影響我們的點選事件。