天天看點

Android自定義View的長按事件的思考

    在寫自定義view的時候經常會遇到需要為其添加長按事件的情況,當然,這裡分幾種情況,比如該自定義view如果是繼承自listView或者是gridView的話,可以直接為其建立一個長按監聽器:

listview.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {  
    @Override  
    public boolean onItemClick(AdapterView<?> parent, View view,int position, long id) {  
      
    // Do something  
    return true.  
}
           

    但是,如果設立長按監聽之後會與短按監聽沖突,這裡有兩種情形,第一種是如果長按監聽是彈出一個dialog之類的視窗視圖的話,那麼此時不會再進入端按監聽的形式,此時可以不用對監聽沖突進行處理。當然,第二種情形就是視窗視圖之外的操作,這時候可以設定一個布爾型的全局變量,因為長按時一般會先觸發長按事件,然後再觸發短按事件,我們在二者中分别給布爾變量賦予不用的值并在短按事件裡做個判斷就行了,僞代碼如下:

gridView.setOnItemLongClickListener(new OnItemLongClickListener(){//設定事件監聽(長按)
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view,
int position, long id) {
longPress=true;
//執行操作
}
}
//設定事件監聽(短按)
gridView.setOnItemClickListener(new OnItemClickListener(){
@Override
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
If(longPress)
{
longPress=false;
break;
}
else
{
longPress=false;
//執行操作
}
}
}
           

    可是,在實際操作中,比如公司希望你開發一個公用的自定義view,并且因為業務需求剛好需要用到長按事件的話,此時如果還是采用需要人為的設立監聽器的方式,會違背了公用view的基本原則,是以比較妥當的方式是在自定義view的分發過程中對其長按操作進行判斷,而長按事件的發生可以采用runnable和handler(view裡有自帶的handler)的形式,這是目前通用的處理方式,也是本文的重點。本文還涉及到view的分發機制,如果有不了解的同學可以先自行百度哈,過一段時間我也會寫部落格來對其分發事件進行分析

    先來總結一下幾種常見的長按實作,第一種形式:

public class LongPressView1 extends View{  
   private int mLastMotionX, mLastMotionY;  
   //是否移動了  
        private boolean isMoved;  
       //是否釋放了  
   private boolean isReleased;  
   //長按的runnable  
   private Runnable mLongPressRunnable;  
   //移動的門檻值  
   private static final int TOUCH_SLOP = 20;  
 
   public LongPressView1(Context context) {  
       super(context);  
       mLongPressRunnable = new Runnable() {  
             
           @Override  
           public void run() {  
             performLongClick();  
           }  
       };  
   }  

 public boolean dispatchTouchEvent(MotionEvent event) {  
      int x = (int) event.getX();  
          int y = (int) event.getY();  
       switch(event.getAction()) {  
      case MotionEvent.ACTION_DOWN:  
            mLastMotionX = x;  
            mLastMotionY = y;  
           
            isReleased = false;  
            isMoved = false;  
 postDelayed(mLongPressRunnable,ViewConfiguration.getLongPressTimeout());  
            break;  
        case MotionEvent.ACTION_MOVE:  
            if(isMoved) break;  
            if(Math.abs(mLastMotionX-x) > TOUCH_SLOP   
                    || Math.abs(mLastMotionY-y) > TOUCH_SLOP) {  
                //移動超過門檻值,則表示移動了  
                isMoved = true; 
                removeCallbacks(mLongPressRunnable);  
            }  
            break;  
        case MotionEvent.ACTION_UP:  
            //釋放了  
            isReleased = true;  
            removeCallbacks(mLongPressRunnable);  
            break;  
        }  
        return true;  
    }  
}
           

     其實原理很簡單,首先先實作一個runnable接口作為長按發生之後的處理時間。然後就來到了view的事件分發機制的起點dispatchTouchEvent方法,該方法實作了分發view的三種類型的點選事件:手指觸摸,移動,和手指擡起。很容易聯想到長按事件是手指按住某個view長時間不動,是以主要的處理邏輯在MotionEvent.ACTION_DOWN,其中使用了postDelayed來實作連續觸摸給定的值之後觸發之前寫好的 mLongPressRunnable 事件,postDelayed的操作實作跟view内部的handler和消息隊列有關,具體的原理可以參見我的前幾篇部落格。然後在MotionEvent.ACTION_MOVE,MotionEvent.ACTION_UP中調用 removeCallbacks來取消長按任務的排隊。具體的運作原理是如果點選了view,隻是到了DOWN的流程,這時候postDelayed開始執行, mLongPressRunnable開始在消息隊列中排隊,預定執行時間是ViewConfiguration.getLongPressTimeout()之後,如果,此時在小于ViewConfiguration.getLongPressTimeout()的時間裡使用者移動view或者手指擡起,那麼就會到ACTION_MOVE或者ACTION_UP下處理,這是直接調用 removeCallbacks将還沒到執行時間的任務mLongPressRunnable給取消了,具體思路就是這樣。

    當然長按事件也可以采用第二種方式實作:

public class LongPressView1 extends View{  
   private int mLastMotionX, mLastMotionY;  
   //是否移動了  
        private boolean isMoved;  
       //是否釋放了  
   private boolean isReleased;  
   //長按的runnable  
   private Runnable mLongPressRunnable;  
   //移動的門檻值  
   private static final int TOUCH_SLOP = 20;  
 
   public LongPressView1(Context context) {  
       super(context);  
       mLongPressRunnable = new Runnable() {  
             
           @Override  
           public void run() {  
          if(isReleased || isMoved) return;  
               performLongClick();  
           }  
       };  
   }  

 public boolean dispatchTouchEvent(MotionEvent event) {  
      int x = (int) event.getX();  
          int y = (int) event.getY();  
       switch(event.getAction()) {  
      case MotionEvent.ACTION_DOWN:  
            mLastMotionX = x;  
            mLastMotionY = y;  
           
            isReleased = false;  
            isMoved = false;  
 postDelayed(mLongPressRunnable,ViewConfiguration.getLongPressTimeout());  
            break;  
        case MotionEvent.ACTION_MOVE:  
            if(isMoved) break;  
            if(Math.abs(mLastMotionX-x) > TOUCH_SLOP   
                    || Math.abs(mLastMotionY-y) > TOUCH_SLOP) {  
                //移動超過門檻值,則表示移動了  
                isMoved = true; 
                removeCallbacks(mLongPressRunnable);  
            }  
            break;  
        case MotionEvent.ACTION_UP:  
            //釋放了  
            isReleased = true;  
            removeCallbacks(mLongPressRunnable);  
            break;  
        }  
        return true;  
    }  
}
           

   這裡很簡單,就不分析了。

  此外,還有最後一點疑問,就是判斷長按事件是一定要放在dispatchTouchEvent方法裡的,對于view的分發過程中的另外兩個方法,比如onTouchEvent和onInterceptTouchEvent方法。為啥放在這兩個方法裡會不合适呢?其實在《Android開發藝術探索》裡的一段僞代碼已經解釋的很清楚了:

Android自定義View的長按事件的思考

    上述代碼是viewGroup中獨有的,在view裡并沒有onInterceptTouchEven,因為攔不攔截是由viewGroup來決定的。viewGroup中的onInterceptTouchEven的預設值是false,隻有這樣才能實作每次都将事件下發給View去處理,同時ViewGroup的onTouchEvent預設值是false,而在view裡,onTouchEvent傳回預設值是true.這樣才能執行多次touch事件,否則的話在view裡一旦傳回來false,那麼以後的事件都不會再下發給該view了。

    如果我們在onInterceptTouchEven裡來判斷目前的事件是不是長按事件,那麼,如果在DOWN中我們判斷了該事件為長按事件,那麼講道理,會調用上面例子中的 postDelayed()方法來實作某固定時間之後的操作,此時任務就在消息隊列裡排隊了。

如果我們在onInterceptTouchEven裡傳回了false,那麼此時後續的MOVE,UP事件也會先傳遞給該viewGroup,但是已經不會再交由onInterceptTouchEven了,而是将三種觸摸事件湊齊之後一起發給view的onTouchEvent去處理(參見僞代碼)。是以此時在MOVE,UP裡寫的removeCallbacks都本就沒有被調用。。是以長按事件判斷失敗。

另一種情況,我們在onInterceptTouchEven裡傳回了true,那麼我們首先會在onInterceptTouchEven裡接收DOWN事件,此時依然會調用上面例子中的 postDelayed()方法來實作某固定時間之後的操作,可是由于傳回的是true,由僞代碼可知後續的MOVE,UP等事件都不會再傳遞給onInterceptTouchEven,而是和DOWN事件一樣直接傳遞給viewGroup的onTouchEvent()處理,是以此時下面的子view将接收不到任何事件。故而簡單的可以認為onInterceptTouchEven隻會攔截DOWN事件,其他的兩個時間實際上和它沒有關系了。

   這裡再補充一點,如果最終需要處理事件的view的onTouchEvent()傳回了false,那麼該事件會被交還給分發給它的viewGroup,由viewGroup決定是繼續往下分發給其他子view還是交給activity處理。如果最終需要處理事件的view 的onTouchEvent()傳回了true,那麼後續事件都将繼續傳遞給該view的onTouchEvent()處理。

   是以得出最重要的總結,如果我們想要在自定義view的特殊事件進行處理的話,比如例子中的長按判斷,最好是将其寫在dispatchTouchEvent中,這樣才能保證每次事件到來時都能對其進行分類處理,因為onInterceptTouchEven和onTouchEvent并不是每次都會調用的。