记得大家刚开始接触安卓的时候,一个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
              
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:本文整理自以下文章,若有发现问题请致邮 [email protected]
工匠若水 Android触摸屏事件派发机制详解与源码分析一(View篇)
Hohohong Android View 事件分发机制 源码解析 (上)

更多相关文章

  1. Android手势操作示例(上/下/左/右的判断)
  2. Android蓝牙操作
  3. Android事件分发机制研究
  4. Android 4.0 事件输入(Event Input)系统
  5. Ubuntu 14.04 64位机上配置Android Studio操作步骤
  6. android中的事件总线
  7. view对touch事件的处理
  8. Android Notes(06) - Touch事件分发响应机制

随机推荐

  1. android kernel 初始化 1
  2. Android(安卓)透明度对应16进制值
  3. android之【RelativeLayout布局】
  4. Android(安卓)Theme 样式 展示
  5. [转] Android电源管理
  6. Android(安卓)允许权限
  7. 面向UDP的Android——PC双向通信(三):在Andr
  8. Android(安卓)api,Android(安卓)SDK
  9. Uyghur Android
  10. android kernel 初始化 1