Android触摸事件分发机制(1)之View

记得大家刚开始接触安卓的时候,一个setOnClickListener就能实现一个View的点击,当时是如此的激动~。这大概就是大家对Android触摸事件最初的接触吧。今天我们来聊下Android重要的触摸事件分发机制。

例子

我们来举一个栗子吧~

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监听,

我们看一下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的事件分发无论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不会执行。

mOnTouchListener从哪里来呢?

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

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

如果我们设置了onTouchListener,且设置返回为true,那么View的onTouchEvent就不会执行!

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;表示长按事件还未触发;
  4. 发送一个延迟为ViewConfiguration.getTapTimeout()=115的延迟消息,到达延时时间后会执行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支持长按事件,则再发一个延时消息,检测长按;

具体如何检测长按呢?

private void postCheckForLongClick(int delayOffset) {  
       mHasPerformedLongPress = false;  
  
       if (mPendingCheckForLongPress == null) {  
           mPendingCheckForLongPress = new CheckForLongPress();  
       }  
       mPendingCheckForLongPress.rememberWindowAttachCount();  
       postDelayed(mPendingCheckForLongPress,  
               ViewConfiguration.getLongPressTimeout() - delayOffset);  
   } 
  1. 初始化CheckForLongPress,此类为Runnable
  2. 发送一个延迟为ViewConfiguration.getLongPressTimeout() - delayOffset=(500-115=385)的延迟消息,到达延时时间后会执行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后且设置了长按监听的话,会触发长按监听接口!

Wonder疑问

  1. 那当用户在500ms内将手抬起会是什么情况呢?
  2. LongClick已经有了,那我们平时使用的的Click呢?
Part02 ACTION_UP
case MotionEvent.ACTION_UP: 
 
    //判断mPrivateFlags是否包含PREPRESSED
    boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  
    
   //如果包含PRESSED或者PREPRESSED则进入执行体,在115ms的前后抬起都会进入执行体。
    if ((mPrivateFlags & PRESSED) != 0 || prepressed) {  
    
        //如果该视图还未获取焦点,则获之
        boolean focusTaken = false;  
        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {  
            focusTaken = requestFocus();  
        }  
        
        //判断是否为长按状态,不是的话,进入执行体
        if (!mHasPerformedLongPress) {  
        
            //这是一个轻点击操作,所以要移除长按检测操作
            removeLongPressCallback();  
  
            //只有在按下状态时才执行点击动作
            if (!focusTaken) { 
             
                //使用Runnable进行发送消息,而不是直接执行performClick。
                //这样视图可以在点击操作前更新其可视化状态 
                
                if (mPerformClick == null) {  
                    mPerformClick = new PerformClick();//*重点01*
                }
                
                //重点 * 调用平时用的click
                if (!post(mPerformClick)) {  
                    performClick(); 
                }  
            }  
        }  
  
        if (mUnsetPressedState == null) {  
            mUnsetPressedState = new UnsetPressedState();//*重点02*
        }  
        
        //根据视图的mPrivateFlags的状态进行操作
        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的状态: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都会执行,只是对不同的状态(prePressed,pressed)进行处理。修改mPrivateFlags的状态,刷新背景,分发SetPress等。

这里我们可以得出一个小结论:

当用户点击视图时,低于500ms设置onClick的接口,就会触发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的工作主要是:刷新相对应的变量和背景,移除响度应的检测回调。一般遇到的比较少。有一种情况是:当用户保持按下操作,并从你的控件转移到外层控件时,会触发ACTION_CANCEL。

总结 Summary

  1. View的事件转发流程为:View.dispatchEvent->View.setOnTouchListener->View.onTouchEvent
  2. 在dispatchTouchEvent中会进行OnTouchListener的判断,如果OnTouchListener不为null且返回true,则表示事件被消费,onTouchEvent不会被执行;否则执行onTouchEvent。
  3. 长按点击的回调是在ACTION_DOWN调用的。
  4. 轻按点击的回调是在ACTION_UP调用的。
  5. 判断触摸事件是否移除了当前控件是在ACTION_MOVE监听的。

额外 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无法触发。如果返回false,就可以。大家可以结合下上面的代码解析看看为什么会这样~

Android触摸事件分发机制(2)之ViewGroup


PS:本文整理自以下文章,若有发现问题请致邮 caoyanglee92@gmail.com
工匠若水 Android触摸屏事件派发机制详解与源码分析一(View篇)
Hohohong Android View 事件分发机制 源码解析 (上)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容