Android觸摸事件分發(fā)機制(1)之View

記得大家剛開始接觸安卓的時候,一個setOnClickListener就能實現(xiàn)一個View的點擊,當時是如此的激動~。這大概就是大家對Android觸摸事件最初的接觸吧。今天我們來聊下Android重要的觸摸事件分發(fā)機制。

例子

我們來舉一個栗子吧~

MyButton.java
public class MyButton extends Button  
{   
  
    @Override  
    public boolean onTouchEvent(MotionEvent event)  
    {  
        int action = event.getAction();  
        switch (action)  
        {  
        case MotionEvent.ACTION_DOWN:  
            Log.e("w", "onTouchEvent ACTION_DOWN");  
            break;  
        case MotionEvent.ACTION_MOVE:  
            Log.e("w", "onTouchEvent ACTION_MOVE");  
            break;  
        case MotionEvent.ACTION_UP:  
            Log.e("w", "onTouchEvent ACTION_UP");  
            break;  
        default:  
            break;  
        }  
        return super.onTouchEvent(event);  
    }  
  
    @Override  
    public boolean dispatchTouchEvent(MotionEvent event)  
    {  
        int action = event.getAction();  
  
        switch (action)  
        {  
        case MotionEvent.ACTION_DOWN:  
            Log.e("w", "dispatchTouchEvent ACTION_DOWN");  
            break;  
        case MotionEvent.ACTION_MOVE:  
            Log.e("w", "dispatchTouchEvent ACTION_MOVE");  
            break;  
        case MotionEvent.ACTION_UP:  
            Log.e("w", "dispatchTouchEvent ACTION_UP");  
            break;  
        default:  
            break;  
        }  
        return super.dispatchTouchEvent(event);  
    }  
} 

在onOutchEvent和disatchTouchEvent中打印日志

main_activity.xml
<LinearLayout 
    android:layout_width="match_parent"  
    android:layout_height="match_parent"  
    >  
  
    <io.weimu.caoyang.MyButton  
        android:id="@+id/id_btn"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:text="click me" />  
  
</LinearLayout>  
MainActivity.java
public class MainActivity extends Activity  
{  
    private Button mButton ;  
    
    @Override  
    protected void onCreate(Bundle savedInstanceState)  
    {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
      
    mButton = (Button) findViewById(R.id.id_btn); 
     
    mButton.setOnTouchListener(new OnTouchListener()  
    {  
        @Override  
        public boolean onTouch(View v, MotionEvent event)  
        {  
            int action = event.getAction();  
            switch (action)  
            {  
            case MotionEvent.ACTION_DOWN:  
                Log.e(“w”, "onTouch ACTION_DOWN");  
                break;  
            case MotionEvent.ACTION_MOVE:  
                Log.e(“w”, "onTouch ACTION_MOVE");  
                break;  
            case MotionEvent.ACTION_UP:  
                Log.e(“w”, "onTouch ACTION_UP");  
                break;  
            default:  
                break;  
            }  
              
            return false;  
        }  
    });  
    }  
}  

我們在MyButton設置了OnTouchListener監(jiān)聽,

我們看一下Log的打印

標志 消息
E/MyButton(879): dispatchTouchEvent ACTION_DOWN
E/MyButton(879): onTouch ACTION_DOWN
E/MyButton(879): onTouchEvent ACTION_DOWN
E/MyButton(879): dispatchTouchEvent ACTION_MOVE
E/MyButton(879): onTouch ACTION_MOVE
E/MyButton(879): onTouchEvent ACTION_MOVE
E/MyButton(879): dispatchTouchEvent ACTION_UP
E/MyButton(879): onTouch ACTION_UP
E/MyButton(879): onTouchEvent ACTION_UP`

按照上面的簡單實例我們可以簡單得出一個結論:View的事件分發(fā)無論DOWN、MOVE、UP都會經過dispatchTouchEventonTouch(如果設置的話)、onTouchEvent


源碼解讀:

Step1 View

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    boolean result = false;
    ...
    
    //重點判斷onTouch
    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
    }
    
    //重點判斷onTouchEvent
    if (!result && onTouchEvent(event)) {
        result = true;
    }
    ...
    return result;
}

先判斷mOnTouchListener是否為空?改View是否為Enable?onTouch是否返回true?若三個同時成立,返回true,且onTouchEvent不會執(zhí)行。

mOnTouchListener從哪里來呢?

 public void setOnTouchListener(OnTouchListener l) {  
     mOnTouchListener = l;  
 }  

可以看到這就是栗子中Activity.java里mButton.setOnTouchListener設置的。

如果我們設置了onTouchListener,且設置返回為true,那么View的onTouchEvent就不會執(zhí)行!

Step2 View

public boolean onTouchEvent(MotionEvent event) {  
    final int viewFlags = mViewFlags;  
    
    //情況1:如果view為disenable且可點擊,返回true
    //此情況還是會消費此觸摸事件,只是不做反應罷了
    if ((viewFlags & ENABLED_MASK) == DISABLED) { 
        return (((viewFlags & CLICKABLE) == CLICKABLE ||  
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));  
    }  
    
    ...
    
    //情況2:如果View為enable且可點擊,返回ture
    //大部分的觸摸操縱都在這里面
    if (((viewFlags & CLICKABLE) == CLICKABLE ||  
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { 
       
        switch (event.getAction()) {  
            case MotionEvent.ACTION_UP:  
                //這是Part02 
                break;  
            case MotionEvent.ACTION_DOWN:  
                //這是Part01 
                break;  
            case MotionEvent.ACTION_CANCEL:  
                //這是Part04
                break;  
            case MotionEvent.ACTION_MOVE:  
                //這是Part03                       
                break;  
        }  
        return true;  
    }  
    
    //情況3:如果View為enable但不能點擊,直接返回false
    return false;  
}

在onTouchEvent工有3個主要情況:

  • 情況1:如果view為disEnable且clickable,返回true。此情況還是會消費此觸摸事件,只是不做反應
  • 情況2:如果View為enable且clickable,返回ture。大部分的觸摸操縱都在這里面
  • 情況3:如果View為enable但unClickable,直接返回false。其實就是View為unClickable,基本就是返回false了。

view的enable和clickable都可以在java和xml設置。

以上代碼可以看出,onToucheEvent里的重點操作都在switch里了,這里我們分幾個步驟進行分析

Part01 ACTION_DOWN
case MotionEvent.ACTION_DOWN:  
    if (mPendingCheckForTap == null) {  
        mPendingCheckForTap = new CheckForTap();  
    }  
    mPrivateFlags |= PREPRESSED;  
    mHasPerformedLongPress = false;  
    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
break;  
  1. 初始化CheckForTap,此類為Runnable
  2. 給mPrivateFlags設置一個PREPRESSED的標識
  3. 設置mHasPerformedLongPress=false;表示長按事件還未觸發(fā);
  4. 發(fā)送一個延遲為ViewConfiguration.getTapTimeout()=115的延遲消息,到達延時時間后會執(zhí)行CheckForTap()里面的run方法:
CheckForTap
private final class CheckForTap implements Runnable {  
  public void run() {  
      mPrivateFlags &= ~PREPRESSED;  
      mPrivateFlags |= PRESSED;  
      refreshDrawableState();  
      //檢測長按
      if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {  
          postCheckForLongClick(ViewConfiguration.getTapTimeout());  
      }  
  }  
}
  1. 取消mPrivateFlags的PREPRESSED
  2. 設置PRESSED標識
  3. 刷新背景
  4. 如果View支持長按事件,則再發(fā)一個延時消息,檢測長按;

具體如何檢測長按呢?

private void postCheckForLongClick(int delayOffset) {  
       mHasPerformedLongPress = false;  
  
       if (mPendingCheckForLongPress == null) {  
           mPendingCheckForLongPress = new CheckForLongPress();  
       }  
       mPendingCheckForLongPress.rememberWindowAttachCount();  
       postDelayed(mPendingCheckForLongPress,  
               ViewConfiguration.getLongPressTimeout() - delayOffset);  
   } 
  1. 初始化CheckForLongPress,此類為Runnable
  2. 發(fā)送一個延遲為ViewConfiguration.getLongPressTimeout() - delayOffset=(500-115=385)的延遲消息,到達延時時間后會執(zhí)行CheckForLongPress()里面的run方法:
CheckForLongPress
class CheckForLongPress implements Runnable {  
  
    private int mOriginalWindowAttachCount;  
  
    public void run() {  
        if (isPressed() && (mParent != null)  
                && mOriginalWindowAttachCount == mWindowAttachCount) {  
            if (performLongClick()) {  
                mHasPerformedLongPress = true;  
            }  
        }  
    }

經過一系列判斷,最終調用performLongClick()即長按的接口調用。

這里我們可以得出一個小結論:

當用戶點擊視圖時,超過500ms后且設置了長按監(jiān)聽的話,會觸發(fā)長按監(jiān)聽接口!

Wonder疑問

  1. 那當用戶在500ms內將手抬起會是什么情況呢?
  2. LongClick已經有了,那我們平時使用的的Click呢?
Part02 ACTION_UP
case MotionEvent.ACTION_UP: 
 
    //判斷mPrivateFlags是否包含PREPRESSED
    boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  
    
   //如果包含PRESSED或者PREPRESSED則進入執(zhí)行體,在115ms的前后抬起都會進入執(zhí)行體。
    if ((mPrivateFlags & PRESSED) != 0 || prepressed) {  
    
        //如果該視圖還未獲取焦點,則獲之
        boolean focusTaken = false;  
        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {  
            focusTaken = requestFocus();  
        }  
        
        //判斷是否為長按狀態(tài),不是的話,進入執(zhí)行體
        if (!mHasPerformedLongPress) {  
        
            //這是一個輕點擊操作,所以要移除長按檢測操作
            removeLongPressCallback();  
  
            //只有在按下狀態(tài)時才執(zhí)行點擊動作
            if (!focusTaken) { 
             
                //使用Runnable進行發(fā)送消息,而不是直接執(zhí)行performClick。
                //這樣視圖可以在點擊操作前更新其可視化狀態(tài) 
                
                if (mPerformClick == null) {  
                    mPerformClick = new PerformClick();//*重點01*
                }
                
                //重點 * 調用平時用的click
                if (!post(mPerformClick)) {  
                    performClick(); 
                }  
            }  
        }  
  
        if (mUnsetPressedState == null) {  
            mUnsetPressedState = new UnsetPressedState();//*重點02*
        }  
        
        //根據(jù)視圖的mPrivateFlags的狀態(tài)進行操作
        if (prepressed) {  
            mPrivateFlags |= PRESSED;  
            refreshDrawableState();  
            postDelayed(mUnsetPressedState,  
                    ViewConfiguration.getPressedStateDuration());  
        } else if (!post(mUnsetPressedState)) {  
            // If the post failed, unpress right now  
            mUnsetPressedState.run();  
        }  
        //移除點擊事件的檢測操作
        removeTapCallback();  
    }  
    break;  

mPrivateFlags的狀態(tài):125ms前為prepressed(點擊前),125ms后位pressed(點擊后)。以上代碼已經做了注釋。這些操作就是500ms內的點擊操作處理。

以上有兩個比較重要的點,這里分析一下:

PerformClick
private final class PerformClick implements Runnable {
    @Override
    public void run() {
        performClick();
    }
}

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);//<----------這里
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}
    
//設置點擊事件回調
public void setOnClickListener(OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

以上代碼可以很清楚的看到onClick的調用!

UnsetPressedState
private final class UnsetPressedState implements Runnable {
    public void run() {
        setPressed(false);
    }
}
public void setPressed(boolean pressed) {
    if (pressed) {
        mPrivateFlags |= PRESSED;
    } else {
        mPrivateFlags &= ~PRESSED;
    }
    refreshDrawableState();
    dispatchSetPressed(pressed);
}

我們可以看到無論如何這個Runnable都會執(zhí)行,只是對不同的狀態(tài)(prePressed,pressed)進行處理。修改mPrivateFlags的狀態(tài),刷新背景,分發(fā)SetPress等。

這里我們可以得出一個小結論:

當用戶點擊視圖時,低于500ms設置onClick的接口,就會觸發(fā)onClick的接口。且這個過程是在OnTouchEvent的ACTION_UP完成。

Part03 ACTION_MOVE
case MotionEvent.ACTION_MOVE:  
    final int x = (int) event.getX();  
    final int y = (int) event.getY();  
  
    //判斷該觸摸事件是否已經移出控件外
    int slop = mTouchSlop;  
    if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
            (y < 0 - slop) || (y >= getHeight() + slop)) {  
             
        //當觸摸移出當前視圖
        //移除點擊回調
        removeTapCallback();  
        if ((mPrivateFlags & PRESSED) != 0) {  
            //移除長按檢測 
            removeLongPressCallback();  
            
            //mPrivateFlags去除PRESSED標志
            mPrivateFlags &= ~PRESSED;  
            
            //刷新背景
            refreshDrawableState();  
        }  
    }  
    break; 

ACTION_MOVE的工作相對簡單一點:不斷的記錄x,y。判斷當前觸摸事件是否已經移除當前控件之外?如果移除了,移除相對應的檢測回調,以及刷新相對應的變量和背景。

Part04 ACTION_CANCLE
case MotionEvent.ACTION_CANCEL:  
    mPrivateFlags &= ~PRESSED;  
    refreshDrawableState();  
    removeTapCallback();  
    break;

ACTION_CANCEL的工作主要是:刷新相對應的變量和背景,移除響度應的檢測回調。一般遇到的比較少。有一種情況是:當用戶保持按下操作,并從你的控件轉移到外層控件時,會觸發(fā)ACTION_CANCEL。

總結 Summary

  1. View的事件轉發(fā)流程為:View.dispatchEvent->View.setOnTouchListener->View.onTouchEvent
  2. 在dispatchTouchEvent中會進行OnTouchListener的判斷,如果OnTouchListener不為null且返回true,則表示事件被消費,onTouchEvent不會被執(zhí)行;否則執(zhí)行onTouchEvent。
  3. 長按點擊的回調是在ACTION_DOWN調用的。
  4. 輕按點擊的回調是在ACTION_UP調用的。
  5. 判斷觸摸事件是否移除了當前控件是在ACTION_MOVE監(jiān)聽的。

額外 Extra

我們在來舉一個栗子:

public class MainActivity extends Activity  
{  
    private Button mButton ;  
    @Override  
    protected void onCreate(Bundle savedInstanceState)  
    {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
          
        mButton = (Button) findViewById(R.id.id_btn);  
        mButton.setOnClickListener(new OnClickListener()  
        {  
            @Override  
            public void onClick(View v)  
            {  
                Log.e("e", "輕觸點擊");  
            }  
        });  
          
        mButton.setOnLongClickListener(new OnLongClickListener()  
        {  
            @Override  
            public boolean onLongClick(View v)  
            {  
                Log.e("e", "長按點擊");   
                return false;  
            }  
        });  
    }    
} 

如果onLongClick返回的是ture(表示消費了),則onClick無法觸發(fā)。如果返回false,就可以。大家可以結合下上面的代碼解析看看為什么會這樣~

Android觸摸事件分發(fā)機制(2)之ViewGroup


PS:本文整理自以下文章,若有發(fā)現(xiàn)問題請致郵 caoyanglee92@gmail.com
工匠若水 Android觸摸屏事件派發(fā)機制詳解與源碼分析一(View篇)
Hohohong Android View 事件分發(fā)機制 源碼解析 (上)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容