「Android 事件分发机制」

「Android 事件分发机制」
一、事件分发机制

Android体系中,事件分发机制占有重要的一份,了解事件的分发机制,对于滑动等冲突才有更深刻的理解。自定义View中能更好的扩展,遇到相关问题能从整个流程上思考,寻找最优解决办法。

  • 一个简单的点击事件是怎样一步步被消费处理的呢?谁该处理,谁不该处理又是由什么因素决定的,这是在实际开发中绕不开的问题,尤其是在自定义View的应用场景下。
  • 先上图,从整体上大致了解事件是怎样被传递与消费的:
    在这里插入图片描述
二、从Activity开始

分析一个最简单的初始页面,Activity布局中仅仅包含一个ViewGroup,首先需要了解View的层级结构。如果此时点击ViewGroup,来看看事件是如何传递的。先来搞清楚Activity的层级结构,基于最新的AppCompatActivity的加载流程,看一下代码实现:

  • CustomActivitysetContentView()
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView();
}
  • AppCompatActivity
//#1
@Override
public void setContentView(@LayoutRes int layoutResID) {
  getDelegate().setContentView(layoutResID);
}
//#2
@NonNull 
public AppCompatDelegate getDelegate() {
  if (mDelegate == null) {
    mDelegate = AppCompatDelegate.create(this, this);
  }
  return mDelegate;
}
//#3 AppCompatDelegateImpl
@Override
public void setContentView(int resId) {
  ensureSubDecor();
  ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
  contentParent.removeAllViews();
  LayoutInflater.from(mContext).inflate(resId, contentParent);
  mAppCompatWindowCallback.getWrapped().onContentChanged();
}

1.AppCompatDelegate是个啥?自从切换到AppCompatActivity以后,加载setContentView() 跟之前的流程有差异。

2.先看一段关于抽象类AppCompatDelegate注释:

This class represents a delegate which you can use to extend AppCompat's support to any Activity.When using an AppCompatDelegate, you should call the following methods instead of the Activity method of the same name... 

了解到,AppCompatDelegate其实委托类,而这个类是为了兼容Activity而增加的。几乎支持了所有Activity的操作,且方法同名。

3.AppCompatDelegate作为抽象类,那么具体的实现细节得找到它的实现类,也就是-AppCompatDelegateImpl,那么在setContentView(),它到底做了哪些操作呢?而整个调用流程从 #1-#3,加上我们自己定义的CustomActivity应该是:CoustomActivity#setContentView->AppCompatActivity#setContentView->AppCompatActivity#getDelegate->AppCompatDelegate#setContentView

  • AppCompatDelegate的实现类AppCompatDelegateImpl

setContentView简单分析,看看具体做了哪些操作:

@Override
public void setContentView(int resId) {
  ensureSubDecor();
  ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
  contentParent.removeAllViews();
  LayoutInflater.from(mContext).inflate(resId, contentParent);
  mAppCompatWindowCallback.getWrapped().onContentChanged();
}
1.ensureSubDecor()

如果熟悉Activity的启动流程的话,应该对Decor并不陌生,似乎有点是DecorView的意思,那到底是不是呢?ensureSubDecor() 创建出来的是什么?

private void ensureSubDecor() {
  if (!mSubDecorInstalled) {
    mSubDecor = createSubDecor();
  }
  //.....
}

private ViewGroup createSubDecor() {
  TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
  //.....
  ensureWindow();
  mWindow.getDecorView();
  final LayoutInflater inflater = LayoutInflater.from(mContext);
  ViewGroup subDecor = null;
  if (!mWindowNoTitle) {
    if (!mWindowNoTitle) {
      // If we're floating, inflate the dialog title decor
      subDecor = (ViewGroup) inflater.inflate(
      R.layout.abc_dialog_title_material, null);
      // Floating windows can never have an action bar, reset the flags
      mHasActionBar = mOverlayActionBar = false;
    } else if (mHasActionBar) {
      
    }
  }
  mWindow.setContentView(subDecor);
  //....
  return subDecor;
}

1.通过对createSubDecor创建过程分析,发现它并不是Window中的DecorView,而是在创建DecorView之后创建的一个subDecorView,包括是否是包含actionBarfloating等,也即是相当于之前的DecorViewtitleBar

2.等到subDecorView创建流程走完,此时view的层级已经是Activity->PhoneWindow->DecorView->subDecorView了。

在这里插入图片描述

3.当ensureSubDecor() 执行完毕:

ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();

subDecor通过findViewById其实就是一个父亲容器,而这个父亲容器的id已经是确定的了-R.id.content

通过动态加载的方式将我们自己的布局(对应resId)添加到了subDecorView之上。此时的层级Activity->PhoneWindow->DecorView->subDecorView->cutomView.

2.层级关系
  • 通过上图,大致了解到Activity的层级关系比较清晰了,在Activity的初始创建,通过addView,将View一层层贴附到容器之中(当然没有分析具体的流程),View Tree直观上,最上层的view则是最后被添加上的。基于这个特点,当事件传递时源码中对子View采用了倒序遍历,增大命中机率。
  • 无论是点击事件,滑动事件,或者是触摸事件,总会包含几个状态ACTION_DOWN–ACTION_UP、ACTION_DOWN–MOVE–MOVE…–ACTION_UP.既然事件首先作用到Activity之上,那么从Activity入手。
Activity中的dispatchTouchEvent();
/**
   * Called to process touch screen events.  You can override this to
   * intercept all touch screen events before they are dispatched to the
   * window.  Be sure to call this implementation for touch screen events
   * that should be handled normally.
   * @param ev The touch screen event.
   * @return boolean Return true if this event was consumed.
   */
  public boolean dispatchTouchEvent(MotionEvent ev) {
      if (ev.getAction() == MotionEvent.ACTION_DOWN) {
          onUserInteraction();
      }
      if (getWindow().superDispatchTouchEvent(ev)) {
          return true;
      }
      return onTouchEvent(ev);
  }

  public void onUserInteraction() {

  }
/**
 * Called when a touch screen event was not handled by any of the views
 * under it.  This is most useful to process touch events that happen
 * outside of your window bounds, where there is no view to receive it.
 * @return Return true if you have consumed the event, false if you haven't.
 * The default implementation always returns false.
 */
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }
    return false;
}
  • 可以看到的是onTouchEvent默认实现是false,注释里解释的也很清楚,事件到此结束。但是有个前提的是getWindow().superDispatchTouchEvent(ev) = false,而getWindow返回的是window,window作为接口,它的唯一实现PhoneWindowsuperDispatchTouchEvent(ev)调用了父类的方法也即ViewGroup.dispatchTouchEvent
PhoneWindow
@Override
   public boolean superDispatchTouchEvent(MotionEvent event) {
       return mDecor.superDispatchTouchEvent(event);
   }

1.window的作用更像是一个工人,起到了连接的作用,这里的mDecor = DecorView,DecorView继承自FrameLayout,FrameLayout继承自Viewgroup
mDecor.superDispatchTouchEvent(event),最终调用的是Viewgroup中的dispatchTouchEvent方法。

  • 总结一下,当事件被activity接收,并可以向下传递,则传递的顺序为activity.dispatchTouchEvent->PhoneWindow.superDispatchTouchEvent(ev)->DecorView.superDispatchTouchEvent(event)->ViewGroup.dispatchTouchEvent,事件由此传递到ViewGroup,重点分析dispatchTouchEvent
1.VIewGroup#dispatchTouchEvent()
//...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
  if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
      //判断viewgroup是否需要拦截此次事件
      intercepted = onInterceptTouchEvent(ev);
      ev.setAction(action); // restore action in case it was changed
    } else {
      intercepted = false;
    }
  }
}
//.....

1.当事件传递到ViewGroupdispatchTouchEvent方法时,之前提到的一个完成的事件序列总是以ACTION_DOWN为开端的,首先就对ACTION_DOWN作了判断。

2.第二步,判断ViewGroup是否需要拦截此次事件,当然默认返回的是falseonInterceptTouchEvent,即默认是不拦截的。

//viewgroup默认是不拦截事件 return false
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}

3.同方法中对子View的遍历操作,注意这里采用的是倒序的形式,判断View是否可见、是否正在执行动画、点击范围是否在其之上、从而来决定View是否消费此次事件:

if (newTouchTarget == null && childrenCount != 0) {
    final float x = ev.getX(actionIndex);
    final float y = ev.getY(actionIndex);
    // Find a child that can receive the event.
    // Scan children from front to back.
    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
    final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {
    final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
    // If there is a view that has accessibility focus we want it
    // to get the event first and if not handled we will perform a
    // normal dispatch. We may do a double iteration but this is
    // safer given the timeframe.
    if (childWithAccessibilityFocus != null) {
       if (childWithAccessibilityFocus != child) {
           continue;
       }
       childWithAccessibilityFocus = null;
       i = childrenCount - 1;
    }
    if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
    ev.setTargetAccessibilityFocus(false);
    continue;
}
     newTouchTarget = getTouchTarget(child);
     if (newTouchTarget != null) {
     // Child is already receiving touch within its bounds.
     // Give it the new pointer in addition to the ones it is handling.
     newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
     }
     resetCancelNextUpFlag(child);
     if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
         // Child wants to receive touch within its bounds.
         mLastTouchDownTime = ev.getDownTime();
         if (preorderedList != null) {
         // childIndex points into presorted list, find original index
            for (int j = 0; j < childrenCount; j++) {
                 if (children[childIndex] == mChildren[j]) {
                     mLastTouchDownIndex = j;
                     break;
                  }
                }
              } else {
                mLastTouchDownIndex = childIndex;
               }
               mLastTouchDownX = ev.getX();
               mLastTouchDownY = ev.getY();
               newTouchTarget = addTouchTarget(child, idBitsToAssign);
               alreadyDispatchedToNewTouchTarget = true;
               break;
           }
           // The accessibility focus didn't handle the event, so clear
           // the flag and do a normal dispatch to all children.
            ev.setTargetAccessibilityFocus(false);
           }
           if (preorderedList != null) preorderedList.clear();
         }         
2.VIew#dispatchTouchEvent()
//view中的dispatchtouchevent方法
public boolean dispatchTouchEvent(MotionEvent event) {
  if (onFilterTouchEventForSecurity(event)) {
     if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
         result = true;
     }
     //noinspection SimplifiableIfStatement
     //包含了,长按,点击,ontouch等监听。
     ListenerInfo li = mListenerInfo;
     if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) {
                 result = true;
      }
    //mOnTouchListener的优先级最高
      if (!result && onTouchEvent(event)) {
          result = true;
         }
     }
}

1.在View中是没有拦截事件的方法的,默认就是处理事件,可以认为dispatchTouchEvent是将事件分发给自己处理。

2.ListenerInfo中包含了长按、点击、onTouch等监听,这里有一个细节,如果View设置了mOnTouchListener监听,它的优先级是很高的,在ontouchevent之前。看看ontouchevent中做了哪些操作。

  • View的onTouchEvent()
public boolean onTouchEvent(MotionEvent event) {
  case MotionEvent.ACTION_UP:
       mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
       if ((viewFlags & TOOLTIP) == TOOLTIP) {
           handleTooltipUp();
        }
        if (!clickable) {
           removeTapCallback();
           removeLongPressCallback();
           mInContextButtonPress = false;
           mHasPerformedLongPress = false;
           mIgnoreNextUpEvent = false;
           break;
         }
  case MotionEvent.ACTION_DOWN:
  if (!clickable) {
     checkForLongClick(ViewConfiguration.getLongPressTimeout(), x, y,TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
      break;
   }
}
/**
 * Defines the default duration in milliseconds before a press turns into
 * a long press
 */
private static final int DEFAULT_LONG_PRESS_TIMEOUT = 500; 

1.View在处理事件时,首先就是对长按做出了判断checkForLongClick,需要注意的是DEFAULT_LONG_PRESS_TIMEOUT这个默认为500的超时时间。分析对长按是如何判断的:

private void checkForLongClick(long delay, float x, float y, int classification) {
  if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
     mHasPerformedLongPress = false;
     if (mPendingCheckForLongPress == null) {
         mPendingCheckForLongPress = new CheckForLongPress();
      }
      mPendingCheckForLongPress.setAnchor(x, y);
      mPendingCheckForLongPress.rememberWindowAttachCount();
      mPendingCheckForLongPress.rememberPressedState();
      mPendingCheckForLongPress.setClassification(classification);
      postDelayed(mPendingCheckForLongPress, delay);
  }
}

public boolean postDelayed(Runnable action, long delayMillis) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.postDelayed(action, delayMillis);
     }
    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().postDelayed(action, delayMillis);
    return true;
}

private final class CheckForLongPress implements Runnable {
  private int mOriginalWindowAttachCount;
  private float mX;
  private float mY;
  private boolean mOriginalPressedState;
  private int mClassification;
  
  @Override
  public void run() {
     if ((mOriginalPressedState == isPressed()) && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) {
        recordGestureClassification(mClassification);
        if (performLongClick(mX, mY)) {
            mHasPerformedLongPress = true;
         }
       }
    }
}

public boolean performLongClick(float x, float y) {
   mLongClickX = x;
   mLongClickY = y;
   final boolean handled = performLongClick();
   mLongClickX = Float.NaN;
   mLongClickY = Float.NaN;
   return handled;
}

2.这里的delay的值就是DEFAULT_LONG_PRESS_TIMEOUT,默认的500ms,通过handler发送了一条延迟为500ms的Runnable到消息队列当中。如果500ms内事件得以消费,返回true则长按事件会被处理,否则将会在ACTION_UP中将事件移除-removeLongPressCallback

  • View的点击事件的处理
//在onTouchEvent方法的 ACTION_UP分支之中
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
  removeLongPressCallback();
  if (!focusTaken) {
    if (mPerformClick == null) {
         mPerformClick = new PerformClick();
    }
    if (!post(mPerformClick)) {
         performClickInternal();
    }
  }
}

1.点击事件同样也不是直接调用,同样也是通过Runnable的方式post出去的,这样做的好处是点击开始前view的状态更新是不受到影响的。

2.对于不可能点击的状态clickable,事件是不是就不处理了呢?答案是否定的:

if ((viewFlags & ENABLED_MASK) == DISABLED) {
   if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
    setPressed(false);
   }
   mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
   // A disabled view that is clickable still consumes the touch
   // events, it just doesn't respond to them.
   return clickable;
}

可以发现,即使是不可能点击的view,依然是会调用到onTouchEvent方法的,只是事件默认没有被处理了。

3.简单总结一下整个流程
  • 对于一个ViewGroup,事件产生以后会首先传递到dispatchTouchEvent,如果此时onInterceptTouchEvent返回是true表示要拦截此次事件,重要的是接下来事件会交给这个ViewGroup处理,onTouchEvent就会被调用,如果onInterceptTouchEvent返回的是false,那么事件会继续向下传递给子View,此时子元素的dispatchTouchEvent会被调用,依次类推,直到事件完全被处理完毕。
  • View需要处理事件时,如果设置了OnTouchListener(优先级是最高的),那么OnTouchListeneronTouch方法会被调用,而OnClickListener的优先级是处于事件传递的末端的。
  • 一个完整的事件序列的消费的顺序是Activity->PhoneWindow->View;如果某一个最末端的ViewonTouchEvent返回了false即不处理,此时事件上抛,父亲容器的onTouchEvent会被调用,如果所有的View都处理该事件,最终事件被传递到Activity,则ActivityonTouchEvent会被调用。
  • 一般情况下一个事件序列只能被一个View拦截消费,同一个事件序列所有事件都会直接交给它处理,并且它的onInterceptTouchEvent不会再被调用。如果子view中调用requestDisallowInterceptTouchEvent,则会决定父view是否拦截事件**(除action_down以外的事件,action_down会重置FLAG_DISALLOW_INTERCEPT的状态值)**
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        // We're already in this state, assume our ancestors are too
        return;
    }
    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
    // Pass it up to our parent
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}
  • 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN(onTouchEvent返回了false),那么同一事件序列中其他事件都不会再交给它来处理,事件将重新交给他的父元素处理,即父元素的onTouchEvent会被调用
  • 如果某个View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以收到后续事件,最终这些消失的点击事件会传递给Activity处理
  • ViewGroup默认不拦截任何事件,ViewGroup的onInterceptTouchEvent方法默认返回false,View没有onInterceptTouchEvent方法,一旦有事件传递给它,那么它的onTouchEvent方法就会被调用。
  • View的onTouchEvent方法默认消耗事件(返回true),除非他是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性分情况,Button默认为true,TextView默认为false。disable不会影响事件的消费,即时一个view是disable状态,依然会消费事件,只是用户无感知,即无反馈。
三、有什么用处?

开发中存在仅仅展示列表的情况,也即是不可点击的列表,如果是这个需求该如何实现?当然如果以RecyclerView为例可以在item禁止,那是否可以以事件的传递默认不消费点击的事件呢?

上面提到的,如果某个View不消耗ACTION_DOWN事件也即是onTouchEvent返回false不就可以满足需求了嘛?简单使用:

1.自定义一个不可点击的RecyclerView
/**
 * Created by Sai
 * on 2022/01/28 16:35.
 */
public class UnClickableRecyclerView extends RecyclerView {
    public UnClickableRecyclerView(@NonNull @NotNull Context context) {
        super(context);
    }

    public UnClickableRecyclerView(@NonNull @NotNull Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public UnClickableRecyclerView(@NonNull @NotNull Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        return true;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev);
    }
}

1.重写onTouchEvent返回为false,同时onInterceptTouchEvent返回true表示拦截下此次事件并且不消费。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐二狗呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值