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

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

例子

我們來舉一個(gè)栗子吧~

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;  
        }  
    });  
    }  
}  

我們?cè)贛yButton設(shè)置了OnTouchListener監(jiān)聽,

我們看一下Log的打印

標(biāo)志 消息
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`

按照上面的簡(jiǎn)單實(shí)例我們可以簡(jiǎn)單得出一個(gè)結(jié)論:View的事件分發(fā)無論DOWN、MOVE、UP都會(huì)經(jīng)過dispatchTouchEvent、onTouch(如果設(shè)置的話)、onTouchEvent


源碼解讀:

Step1 View

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

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

mOnTouchListener從哪里來呢?

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

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

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

Step2 View

public boolean onTouchEvent(MotionEvent event) {  
    final int viewFlags = mViewFlags;  
    
    //情況1:如果view為disenable且可點(diǎn)擊,返回true
    //此情況還是會(huì)消費(fèi)此觸摸事件,只是不做反應(yīng)罷了
    if ((viewFlags & ENABLED_MASK) == DISABLED) { 
        return (((viewFlags & CLICKABLE) == CLICKABLE ||  
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));  
    }  
    
    ...
    
    //情況2:如果View為enable且可點(diǎn)擊,返回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但不能點(diǎn)擊,直接返回false
    return false;  
}

在onTouchEvent工有3個(gè)主要情況:

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

view的enable和clickable都可以在java和xml設(shè)置。

以上代碼可以看出,onToucheEvent里的重點(diǎn)操作都在switch里了,這里我們分幾個(gè)步驟進(jìn)行分析

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設(shè)置一個(gè)PREPRESSED的標(biāo)識(shí)
  3. 設(shè)置mHasPerformedLongPress=false;表示長(zhǎng)按事件還未觸發(fā);
  4. 發(fā)送一個(gè)延遲為ViewConfiguration.getTapTimeout()=115的延遲消息,到達(dá)延時(shí)時(shí)間后會(huì)執(zhí)行CheckForTap()里面的run方法:
CheckForTap
private final class CheckForTap implements Runnable {  
  public void run() {  
      mPrivateFlags &= ~PREPRESSED;  
      mPrivateFlags |= PRESSED;  
      refreshDrawableState();  
      //檢測(cè)長(zhǎng)按
      if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {  
          postCheckForLongClick(ViewConfiguration.getTapTimeout());  
      }  
  }  
}
  1. 取消mPrivateFlags的PREPRESSED
  2. 設(shè)置PRESSED標(biāo)識(shí)
  3. 刷新背景
  4. 如果View支持長(zhǎng)按事件,則再發(fā)一個(gè)延時(shí)消息,檢測(cè)長(zhǎng)按;

具體如何檢測(cè)長(zhǎng)按呢?

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ā)送一個(gè)延遲為ViewConfiguration.getLongPressTimeout() - delayOffset=(500-115=385)的延遲消息,到達(dá)延時(shí)時(shí)間后會(huì)執(zhí)行CheckForLongPress()里面的run方法:
CheckForLongPress
class CheckForLongPress implements Runnable {  
  
    private int mOriginalWindowAttachCount;  
  
    public void run() {  
        if (isPressed() && (mParent != null)  
                && mOriginalWindowAttachCount == mWindowAttachCount) {  
            if (performLongClick()) {  
                mHasPerformedLongPress = true;  
            }  
        }  
    }

經(jīng)過一系列判斷,最終調(diào)用performLongClick()即長(zhǎng)按的接口調(diào)用。

這里我們可以得出一個(gè)小結(jié)論:

當(dāng)用戶點(diǎn)擊視圖時(shí),超過500ms后且設(shè)置了長(zhǎng)按監(jiān)聽的話,會(huì)觸發(fā)長(zhǎng)按監(jiān)聽接口!

Wonder疑問

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

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

以上有兩個(gè)比較重要的點(diǎn),這里分析一下:

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;
}
    
//設(shè)置點(diǎn)擊事件回調(diào)
public void setOnClickListener(OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

以上代碼可以很清楚的看到onClick的調(diào)用!

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);
}

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

這里我們可以得出一個(gè)小結(jié)論:

當(dāng)用戶點(diǎn)擊視圖時(shí),低于500ms設(shè)置onClick的接口,就會(huì)觸發(fā)onClick的接口。且這個(gè)過程是在OnTouchEvent的ACTION_UP完成。

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

ACTION_MOVE的工作相對(duì)簡(jiǎn)單一點(diǎn):不斷的記錄x,y。判斷當(dāng)前觸摸事件是否已經(jīng)移除當(dāng)前控件之外?如果移除了,移除相對(duì)應(yīng)的檢測(cè)回調(diào),以及刷新相對(duì)應(yīng)的變量和背景。

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

ACTION_CANCEL的工作主要是:刷新相對(duì)應(yīng)的變量和背景,移除響度應(yīng)的檢測(cè)回調(diào)。一般遇到的比較少。有一種情況是:當(dāng)用戶保持按下操作,并從你的控件轉(zhuǎn)移到外層控件時(shí),會(huì)觸發(fā)ACTION_CANCEL。

總結(jié) Summary

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

額外 Extra

我們?cè)趤砼e一個(gè)栗子:

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", "輕觸點(diǎn)擊");  
            }  
        });  
          
        mButton.setOnLongClickListener(new OnLongClickListener()  
        {  
            @Override  
            public boolean onLongClick(View v)  
            {  
                Log.e("e", "長(zhǎng)按點(diǎn)擊");   
                return false;  
            }  
        });  
    }    
} 

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

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


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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容