美文网首页Android 进阶之旅
Android 进阶学习(四) Android 事件传递View

Android 进阶学习(四) Android 事件传递View

作者: Tsm_2020 | 来源:发表于2020-11-09 17:05 被阅读0次

    Android 事件传递的过程在Activity ViewGroup 和View 中有不同的体现,事件的传递主要分为三个阶段

    1.分发 Dispatch

    和事件分发有关的方法为dispatchTouchEvent,所有触摸事件都是从Activity 的 dispatchTouchEvent 的方法开始下发的, 方法的原型为public boolean dispatchTouchEvent(MotionEvent ev), 返回一个boolean 类型值,如果为true 则表示该事件在此方法中被消费,如果返回false 事件则会分发下去

    2. 拦截 Intercept

    和事件拦截的有关的方法是onInterceptTouchEvent,这个方法只有在ViewGroup中才有,事件的原型为public boolean onInterceptTouchEvent(MotionEvent ev) ,同样他也返回一个boolean 类型值用来是否拦截这个事件,true 表示拦截该事件,此事件会被消费,false 表示不作拦截

    3.消费 Consume

    和事件消费相关的方法为 onTouchEvent ,这个方法的原型为 public boolean onTouchEvent(MotionEvent event) ,同样他也返回一个boolean 类型值,返回true 则代表着这个事件被消费了,如果返回false 这个事件就会被传递给父控件的onTouchEvent 做处理

    那么在Activity ViewGroup View 在事件的消费过程中,都参与了哪些方法呢,

    1.Activity

    Activity 中包含 dispatchTouchEvent 和 onTouchEvent 两个方法

    2.ViewGroup

    Activity 中包含 dispatchTouchEvent ,onInterceptTouchEvent 和 onTouchEvent 三个方法

    3.View (虽然ViewGroup是View 的子类,但是这里所说的View,是除去ViewGroup的其他View)

    View 中包含 dispatchTouchEvent 和 onTouchEvent 两个方法

    下面我们通过一个简单的例子,来看一下View 事件分发的具体流程

    public class TsmTextView extends androidx.appcompat.widget.AppCompatTextView {
       public TsmTextView(Context context) {
           super(context);
       }
    
       public TsmTextView(Context context, @Nullable AttributeSet attrs) {
           super(context, attrs);
       }
    
       public TsmTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
           super(context, attrs, defStyleAttr);
       }
    
       @Override
       public boolean dispatchTouchEvent(MotionEvent event) {
           log    ("TsmTextView            dispatchTouchEvent"+"      " + "action"+event.getAction());
           return super.dispatchTouchEvent(event);
       }
    
       @Override
       public boolean onTouchEvent(MotionEvent event) {
           log    ("TsmTextView            onTouchEvent"+"      " + "action"+event.getAction());
           return super.onTouchEvent(event);
       }
    
       public void log(String tag){
           LogUtils.i(tag);
       }
    }
    

    Activity 中的代码

    public class MainActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener {
    
       @Override
       protected void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           setContentView(R.layout.activity_main);
    
           TsmTextView tv_view= findViewById(R.id.tv_view);
    
    //        tv_view.setOnClickListener(this);
    //        tv_view.setOnTouchListener(this);
       }
    
    
       @Override
       public boolean dispatchTouchEvent(MotionEvent ev) {
           LogUtils.i("MainActivity   dispatchTouchEvent    action:"+ev.getAction());
           return super.dispatchTouchEvent(ev);
       }
    
    
       @Override
       public boolean onTouchEvent(MotionEvent event) {
           LogUtils.i("MainActivity   onTouchEvent    action:"+event.getAction());
           return super.onTouchEvent(event);
       }
    
       @Override
       public void onClick(View v) {
           LogUtils.i("onClick");
       }
    
       @Override
       public boolean onTouch(View v, MotionEvent event) {
           switch (v.getId()){
               case R.id.tv_view:
                   switch (event.getAction()) {
                       case MotionEvent.ACTION_DOWN:
                           LogUtils.i("MainActivity       onTouch"+"        action:"+ event.getAction());
                           return  false;
    //                    case MotionEvent.ACTION_UP:
    //                        LogUtils.i(TsmUtils.getClassName(this) +"onTouch"+"        "+ "ACTION_UP");
    //                        return  false;
    //                    case MotionEvent.ACTION_MOVE:
    //                        LogUtils.i(TsmUtils.getClassName(this) +"onTouch"+"        "+ "ACTION_HOVER_MOVE");
    //                        return  false;
                       default:
                           return  false;
                   }
           }
           return false;
       }
    }
    

    打印的结果

    msg:==>MainActivity   dispatchTouchEvent    action:0
    msg:==>TsmTextView            dispatchTouchEvent      action0
    msg:==>TsmTextView            onTouchEvent      action0
    msg:==>MainActivity   onTouchEvent    action:0
    msg:==>MainActivity   dispatchTouchEvent    action:2
    msg:==>MainActivity   onTouchEvent    action:2
    msg:==>MainActivity   dispatchTouchEvent    action:2
    msg:==>MainActivity   onTouchEvent    action:2
    msg:==>MainActivity   dispatchTouchEvent    action:2
    msg:==>MainActivity   onTouchEvent    action:2
    msg:==>MainActivity   dispatchTouchEvent    action:2
    msg:==>MainActivity   onTouchEvent    action:2
    msg:==>MainActivity   dispatchTouchEvent    action:1
    msg:==>MainActivity   onTouchEvent    action:1
    

    事件分发是从Activity dispatchTouchEvent -->TsmTextView dispatchTouchEvent -->TsmTextView onTouchEvent -->MainActivity onTouchEvent

    如果我们给TsmTextView 设置了OnTouchListener,那么整个流程又变为Activity dispatchTouchEvent -->TsmTextView dispatchTouchEvent -->TsmTextView onTouch --> TsmTextView onTouchEvent -->MainActivity onTouchEvent

    如果我们继续给TsmTextView 设置一个OnClickListener,表示 TsmTextView 会将这个事件消费掉,那么流程又变为Activity dispatchTouchEvent -->TsmTextView dispatchTouchEvent -->TsmTextView onTouch --> TsmTextView onTouchEvent -->TsmTextView onClick
    TsmTextView 由于执行了onClick 表示该事件已经被消费掉了,所以Activity 的onTouchEvent 不会收到View 的回传信息

    在为TsmTextView 设置onClickListeners 后,我们将Activity 的dispatchTouchEvent 修改一下,禁止他传递ACTION_DOWN 事件

       @Override
       public boolean dispatchTouchEvent(MotionEvent ev) {
           LogUtils.i("MainActivity   dispatchTouchEvent    action:"+ev.getAction());
           if(ev.getAction()==MotionEvent.ACTION_DOWN){
               return false;
           }
           return super.dispatchTouchEvent(ev);
       }
    

    打印的日志变为

    msg:==>MainActivity   dispatchTouchEvent    action:0
    msg:==>MainActivity   dispatchTouchEvent    action:2
    msg:==>MainActivity   onTouchEvent    action:2
    msg:==>MainActivity   dispatchTouchEvent    action:2
    msg:==>MainActivity   onTouchEvent    action:2
    msg:==>MainActivity   dispatchTouchEvent    action:2
    msg:==>MainActivity   onTouchEvent    action:2
    msg:==>MainActivity   dispatchTouchEvent    action:2
    msg:==>MainActivity   onTouchEvent    action:2
    msg:==>MainActivity   dispatchTouchEvent    action:1
    msg:==>MainActivity   onTouchEvent    action:1
    

    发现如果拦截了Activity 的dispatchTouchEvent 的ACTION_DOWN 后,TsmTextView 后续关于ACTION_DOWN 也没有执行,并且它的onClick也没有执行,同时这里我发现不管是否返回true 或者false,TsmTextView 后续的动作都没有执行,猜想应该是没有执行super.dispatchTouchEvent(ev)的原因,但是找了半天也没发现在父类Activity 中有关于dispatchTouchEvent 的逻辑,这里我们先放下,继续去看View 的方法,同时还原Activity dispatchTouchEvent 的方法,因为View 的dispatchTouchEvent 源码很简单就看到了

    我们接下来看一下修改View 的dispatchTouchEvent 后的表现

       @Override
       public boolean dispatchTouchEvent(MotionEvent event) {
           log    ("TsmTextView            dispatchTouchEvent"+"      " + "action"+event.getAction());
           if(event.getAction()==MotionEvent.ACTION_DOWN){
               return false;
           }
           return super.dispatchTouchEvent(event);
       }
    

    如果将TsmTextView 的dispatchTouchEvent 返回false 他的表现为

    msg:==>MainActivity   dispatchTouchEvent    action:0
    msg:==>TsmTextView            dispatchTouchEvent      action0
    msg:==>MainActivity   onTouchEvent    action:0
    msg:==>MainActivity   dispatchTouchEvent    action:2
    msg:==>MainActivity   onTouchEvent    action:2
    msg:==>MainActivity   dispatchTouchEvent    action:2
    msg:==>MainActivity   onTouchEvent    action:2
    

    直接返回false,不调用super.dispatchTouchEvent(event); 表示 View 对这件事情不关心,后续的 onTouch 和onTouchEvent 都不会执行,而是将后续事件交给了Activity 的onTouchEvent
    如果直接返回true ,不调用super.dispatchTouchEvent(event);表示View 消费了这个事件,那么后续的 onTouch 和onTouchEvent 都不会执行,而且Activity 的也不会收到后续的事件
    以上两种情况TsmTextView 的onClick都不会执行,也就说想要执行onClick ,必须执行super.dispatchTouchEvent(event); 或者自己手动处理,

    我们去看一下View 的 dispatchTouchEvent 的源码

    public boolean dispatchTouchEvent(MotionEvent event) {
           if (event.isTargetAccessibilityFocus()) {///判断焦点
               if (!isAccessibilityFocusedViewOrHost()) {
                   return false;
               }
               event.setTargetAccessibilityFocus(false);
           }
           boolean result = false;
           if (mInputEventConsistencyVerifier != null) {
               mInputEventConsistencyVerifier.onTouchEvent(event, 0);
           }
           final int actionMasked = event.getActionMasked();
           if (actionMasked == MotionEvent.ACTION_DOWN) {
               stopNestedScroll();
           }
           if (onFilterTouchEventForSecurity(event)) {
               if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                   result = true;
               }
               ListenerInfo li = mListenerInfo;
               if (li != null && li.mOnTouchListener != null
                       && (mViewFlags & ENABLED_MASK) == ENABLED
                       && li.mOnTouchListener.onTouch(this, event)) {
                   result = true;
               }
               if (!result && onTouchEvent(event)) {
                   result = true;
               }
           }
           if (!result && mInputEventConsistencyVerifier != null) {
               mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
           }
           if (actionMasked == MotionEvent.ACTION_UP ||
                   actionMasked == MotionEvent.ACTION_CANCEL ||
                   (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
               stopNestedScroll();
           }
    
           return result;
       }
    

    代码看起来很多,但是我们只关心onFilterTouchEventForSecurity 内的逻辑逻辑就好了,其他都是一些关于focus 的判断,
    也就是

               ListenerInfo li = mListenerInfo;
               if (li != null && li.mOnTouchListener != null
                       && (mViewFlags & ENABLED_MASK) == ENABLED
                       && li.mOnTouchListener.onTouch(this, event)) {
                   result = true;
               }
               if (!result && onTouchEvent(event)) {
                   result = true;
               }
    

    关于mListenerInfo 就是View 响应事件的封装,

     public void setOnClickListener(@Nullable OnClickListener l) {
           if (!isClickable()) {
               setClickable(true);
           }
           getListenerInfo().mOnClickListener = l;
       }
       ListenerInfo getListenerInfo() {
           if (mListenerInfo != null) {
               return mListenerInfo;
           }
           mListenerInfo = new ListenerInfo();
           return mListenerInfo;
       }
    

    我们就清楚了一些信息,如果View 有 onTouch 并且返回true ,且是enable 的状态时,那么dispatchTouchEvent就返回true ,否则 dispatchTouchEvent返回的结果和onTouchEvent 保持一致,
    那么View 的onTouchEvent的执行逻辑是什么呢,我们继续看

    public boolean onTouchEvent(MotionEvent event) {
           final float x = event.getX();
           final float y = event.getY();
           final int viewFlags = mViewFlags;
           final int action = event.getAction();
           
           ///可以点击  或者长按
           final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                   || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                   || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
          ////如果是disable 的状态,则返回clickable  
           if ((viewFlags & ENABLED_MASK) == DISABLED) {
               if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                   setPressed(false);
               }
               mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
               return clickable;
           }
           ///
           if (mTouchDelegate != null) {
               if (mTouchDelegate.onTouchEvent(event)) {
                   return true;
               }
           }
           if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
               switch (action) {
                   case MotionEvent.ACTION_UP:
                       mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                       if ((viewFlags & TOOLTIP) == TOOLTIP) {
                           handleTooltipUp();
                       }
                       if (!clickable) {
                           removeTapCallback();
                           removeLongPressCallback();
                           mInContextButtonPress = false;
                           mHasPerformedLongPress = false;
                           mIgnoreNextUpEvent = false;
                           break;
                       }
                       boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                       if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                           boolean focusTaken = false;
                           if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                               focusTaken = requestFocus();
                           }
                           if (prepressed) {
                               setPressed(true, x, y);
                           }
                           if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                               removeLongPressCallback();
                               if (!focusTaken) {
                                   if (mPerformClick == null) {
                                       mPerformClick = new PerformClick();
                                   }
                                   if (!post(mPerformClick)) {
                                       performClickInternal();
                                   }
                               }
                           }
                           if (mUnsetPressedState == null) {
                               mUnsetPressedState = new UnsetPressedState();
                           }
                           if (prepressed) {
                               postDelayed(mUnsetPressedState,
                                       ViewConfiguration.getPressedStateDuration());
                           } else if (!post(mUnsetPressedState)) {
                               mUnsetPressedState.run();
                           }
                           removeTapCallback();
                       }
                       mIgnoreNextUpEvent = false;
                       break;
                   case MotionEvent.ACTION_DOWN:
                       if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                           mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                       }
                       mHasPerformedLongPress = false;
                       if (!clickable) {
                           checkForLongClick(ViewConfiguration.getLongPressTimeout(),x,y,TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                           break;
                       }
                       if (performButtonActionOnTouchDown(event)) {
                           break;
                       }
                       boolean isInScrollingContainer = isInScrollingContainer();
                       if (isInScrollingContainer) {
                           mPrivateFlags |= PFLAG_PREPRESSED;
                           if (mPendingCheckForTap == null) {
                               mPendingCheckForTap = new CheckForTap();
                           }
                           mPendingCheckForTap.x = event.getX();
                           mPendingCheckForTap.y = event.getY();
                           postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                       } else {
                           setPressed(true, x, y);
                    checkForLongClick(ViewConfiguration.getLongPressTimeout(),x,y,TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                       }
                       break;
                   case MotionEvent.ACTION_CANCEL:
                       if (clickable) {
                           setPressed(false);
                       }
                       removeTapCallback();
                       removeLongPressCallback();
                       mInContextButtonPress = false;
                       mHasPerformedLongPress = false;
                       mIgnoreNextUpEvent = false;
                       mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                       break;
                   case MotionEvent.ACTION_MOVE:
                       if (clickable) {
                           drawableHotspotChanged(x, y);
                       }
                       final int motionClassification = event.getClassification();
                       final boolean ambiguousGesture =
                               motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
                       int touchSlop = mTouchSlop;
                       if (ambiguousGesture && hasPendingLongPressCallback()) {
                           final float ambiguousMultiplier =
                                   ViewConfiguration.getAmbiguousGestureMultiplier();
                           if (!pointInView(x, y, touchSlop)) {
                               removeLongPressCallback();
                               long delay = (long) (ViewConfiguration.getLongPressTimeout()   * ambiguousMultiplier);
                               delay -= event.getEventTime() - event.getDownTime();
                               checkForLongClick(   delay,  x,   y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                           }
                           touchSlop *= ambiguousMultiplier;
                       }
                       if (!pointInView(x, y, touchSlop)) {
                           removeTapCallback();
                           removeLongPressCallback();
                           if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                               setPressed(false);
                           }
                           mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                       }
                       final boolean deepPress =
                               motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
                       if (deepPress && hasPendingLongPressCallback()) {
                           removeLongPressCallback();
                           checkForLongClick( 0, x, y,TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
                       }
                       break;
               }
               return true;
           }
    
           return false;
       }
    

    正眼一看又是很多看不懂的代码,我们还是好好分析一下,首先我们看到前面先判断了clickable ,如果控件是disable 的状态,那么返回clickable,
    接下来我们看到就走到了一个根据action 做判断的switch方法,但是最后都返回了true ,我们先看一下ACTION_DOWN,如果不可点击的话直接postDelay一个runnable延时,如果这个延时执行的话那么就代表了是长按,否则的话是先setPressed(true) 设置是按下的状态,在postDelay 那个长按的runnable, 接下来我们看ACTION_UP 的代码,这里面的代码逻辑上就比较简单了,很多标识符逻辑虽然有用但是不必太多执着,我们可以看到!mHasPerformedLongPress && !mIgnoreNextUpEvent 这个判断代表了没有执行长按方法,就post 了一个PerformClick 的runnable,最后post了一个runnable 将view 的Pressed 状态改为未点击,
    这里面有一个疑问,那就是clickable 这个属性是在哪里设置的,

       public void setOnClickListener(@Nullable OnClickListener l) {
           if (!isClickable()) {
               setClickable(true);
           }
           getListenerInfo().mOnClickListener = l;
       }
       public void setOnLongClickListener(@Nullable OnLongClickListener l) {
           if (!isLongClickable()) {
               setLongClickable(true);
           }
           getListenerInfo().mOnLongClickListener = l;
       }
    
    

    非常神奇吧,竟然是在设置点击监听的时候,整体上也就连贯起来了,

    总结一下上面的流程,

    1.事件是从Actvity 的dispatchTouchEvent 开始的,如果不进行干预,也就是默认返回父类的同名函数,那么事件会按照嵌套的层级从外层向内层传递,到了最内层的View 的时候,如果onTouchEvent 对事件进行了处理,也就是返回true,那么这个事就不会重新向外传递,如果返回false 那么事件就会按照嵌套的层级由内向外执行 onTouchEvent ,直到Activity 的onTouchEvent ,

    2.如果在向内层传递的过程中如果干预了,那么嵌套在他内部的view 则会不收到这个事件,

    3.View 的触摸事件是从onTouch 开始的,如果onTouch 返回true,那么这个事件则会被拦截,不会执行View 的onTouchEvent ,也不会执行View 的onClick 方法,

    4.如果view 设置了点击事件,在View 的onTouchEvent 方法中就会根据clickable 走上面所说的switch(action) 的方法, 让onTouchEvent 直接返回true,让Activity和父控件的onTouchEvent 收不到View回传回来的事件,

    相关文章

      网友评论

        本文标题:Android 进阶学习(四) Android 事件传递View

        本文链接:https://www.haomeiwen.com/subject/qjvlbktx.html