「Android 事件分發(fā)機(jī)制」

「Android 事件分發(fā)機(jī)制」

一、事件分發(fā)機(jī)制

Android體系中,事件分發(fā)機(jī)制占有重要的一份,了解事件的分發(fā)機(jī)制,對(duì)于滑動(dòng)等沖突才有更深刻的理解。自定義View中能更好的擴(kuò)展,遇到相關(guān)問題能從整個(gè)流程上思考,尋找最優(yōu)解決辦法。

  • 一個(gè)簡單的點(diǎn)擊事件是怎樣一步步被消費(fèi)處理的呢?誰該處理,誰不該處理又是由什么因素決定的,這是在實(shí)際開發(fā)中繞不開的問題,尤其是在自定義View的應(yīng)用場景下。
  • 先上圖,從整體上大致了解事件是怎樣被傳遞與消費(fèi)的:


    view事件分發(fā).png
二、從Activity開始

分析一個(gè)最簡單的初始頁面,Activity布局中僅僅包含一個(gè)ViewGroup,首先需要了解View的層級(jí)結(jié)構(gòu)。如果此時(shí)點(diǎn)擊ViewGroup,來看看事件是如何傳遞的。先來搞清楚Activity的層級(jí)結(jié)構(gòu),基于最新的AppCompatActivity的加載流程,看一下代碼實(shí)現(xiàn):

  • 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是個(gè)啥?自從切換到AppCompatActivity以后,加載setContentView()跟之前的流程有差異。

2.先看一段關(guān)于抽象類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其實(shí)委托類,而這個(gè)類是為了兼容Activity而增加的。幾乎支持了所有Activity的操作,且方法同名。

3.AppCompatDelegate作為抽象類,那么具體的實(shí)現(xiàn)細(xì)節(jié)得找到它的實(shí)現(xiàn)類,也就是-AppCompatDelegateImpl,那么在setContentView(),它到底做了哪些操作呢?而整個(gè)調(diào)用流程從#1-#3,加上我們自己定義的CustomActivity應(yīng)該是:CoustomActivity#setContentView->AppCompatActivity#setContentView->AppCompatActivity#getDelegate->AppCompatDelegate#setContentView

  • AppCompatDelegate的實(shí)現(xiàn)類AppCompatDelegateImpl

對(duì)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的啟動(dòng)流程的話,應(yīng)該對(duì)Decor并不陌生,似乎有點(diǎn)是DecorView的意思,那到底是不是呢?ensureSubDecor()創(chuàng)建出來的是什么?

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.通過對(duì)createSubDecor創(chuàng)建過程分析,發(fā)現(xiàn)它并不是Window中的DecorView,而是在創(chuàng)建DecorView之后創(chuàng)建的一個(gè)subDecorView,包括是否是包含actionBar、floating等,也即是相當(dāng)于之前的DecorViewtitleBar。

2.等到subDecorView創(chuàng)建流程走完,此時(shí)view的層級(jí)已經(jīng)是Activity->PhoneWindow->DecorView->subDecorView了。

activity層級(jí).png

3.當(dāng)ensureSubDecor()執(zhí)行完畢:

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

subDecor通過findViewById其實(shí)就是一個(gè)父親容器,而這個(gè)父親容器的id已經(jīng)是確定的了-R.id.content

通過動(dòng)態(tài)加載的方式將我們自己的布局(對(duì)應(yīng)resId)添加到了subDecorView之上。此時(shí)的層級(jí)Activity->PhoneWindow->DecorView->subDecorView->cutomView.

2.層級(jí)關(guān)系
  • 通過上圖,大致了解到Activity的層級(jí)關(guān)系比較清晰了,在Activity的初始創(chuàng)建,通過addView,將View一層層貼附到容器之中(當(dāng)然沒有分析具體的流程),View Tree直觀上,最上層的view則是最后被添加上的?;谶@個(gè)特點(diǎn),當(dāng)事件傳遞時(shí)源碼中對(duì)子View采用了倒序遍歷,增大命中機(jī)率。
  • 無論是點(diǎn)擊事件,滑動(dòng)事件,或者是觸摸事件,總會(huì)包含幾個(gè)狀態(tài)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默認(rèn)實(shí)現(xiàn)是false,注釋里解釋的也很清楚,事件到此結(jié)束。但是有個(gè)前提的是getWindow().superDispatchTouchEvent(ev) = false,而getWindow返回的是window,window作為接口,它的唯一實(shí)現(xiàn)PhoneWindow,superDispatchTouchEvent(ev)調(diào)用了父類的方法也即ViewGroup.dispatchTouchEvent
PhoneWindow
@Override
   public boolean superDispatchTouchEvent(MotionEvent event) {
       return mDecor.superDispatchTouchEvent(event);
   }

1.window的作用更像是一個(gè)工人,起到了連接的作用,這里的mDecor = DecorView,DecorView繼承自FrameLayout,F(xiàn)rameLayout繼承自Viewgroup
mDecor.superDispatchTouchEvent(event),最終調(diào)用的是Viewgroup中的dispatchTouchEvent方法。

  • 總結(jié)一下,當(dāng)事件被activity接收,并可以向下傳遞,則傳遞的順序?yàn)?strong>activity.dispatchTouchEvent->PhoneWindow.superDispatchTouchEvent(ev)->DecorView.superDispatchTouchEvent(event)->ViewGroup.dispatchTouchEvent,事件由此傳遞到ViewGroup,重點(diǎn)分析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.當(dāng)事件傳遞到ViewGroupdispatchTouchEvent方法時(shí),之前提到的一個(gè)完成的事件序列總是以ACTION_DOWN為開端的,首先就對(duì)ACTION_DOWN作了判斷。

2.第二步,判斷ViewGroup是否需要攔截此次事件,當(dāng)然默認(rèn)返回的是falseonInterceptTouchEvent,即默認(rèn)是不攔截的。

//viewgroup默認(rèn)是不攔截事件 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.同方法中對(duì)子View的遍歷操作,注意這里采用的是倒序的形式,判斷View是否可見、是否正在執(zhí)行動(dòng)畫、點(diǎn)擊范圍是否在其之上、從而來決定View是否消費(fèi)此次事件:

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
     //包含了,長按,點(diǎn)擊,ontouch等監(jiān)聽。
     ListenerInfo li = mListenerInfo;
     if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) {
                 result = true;
      }
    //mOnTouchListener的優(yōu)先級(jí)最高
      if (!result && onTouchEvent(event)) {
          result = true;
         }
     }
}

1.在View中是沒有攔截事件的方法的,默認(rèn)就是處理事件,可以認(rèn)為dispatchTouchEvent是將事件分發(fā)給自己處理。

2.ListenerInfo中包含了長按、點(diǎn)擊、onTouch等監(jiān)聽,這里有一個(gè)細(xì)節(jié),如果View設(shè)置了mOnTouchListener監(jiān)聽,它的優(yōu)先級(jí)是很高的,在ontouchevent之前??纯?strong>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在處理事件時(shí),首先就是對(duì)長按做出了判斷checkForLongClick,需要注意的是DEFAULT_LONG_PRESS_TIMEOUT這個(gè)默認(rèn)為500的超時(shí)時(shí)間。分析對(duì)長按是如何判斷的:

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,默認(rèn)的500ms,通過handler發(fā)送了一條延遲為500ms的Runnable到消息隊(duì)列當(dāng)中。如果500ms內(nèi)事件得以消費(fèi),返回true則長按事件會(huì)被處理,否則將會(huì)在ACTION_UP中將事件移除-removeLongPressCallback

  • View的點(diǎn)擊事件的處理
//在onTouchEvent方法的 ACTION_UP分支之中
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
  removeLongPressCallback();
  if (!focusTaken) {
    if (mPerformClick == null) {
         mPerformClick = new PerformClick();
    }
    if (!post(mPerformClick)) {
         performClickInternal();
    }
  }
}

1.點(diǎn)擊事件同樣也不是直接調(diào)用,同樣也是通過Runnable的方式post出去的,這樣做的好處是點(diǎn)擊開始前view的狀態(tài)更新是不受到影響的。

2.對(duì)于不可能點(diǎn)擊的狀態(tài)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;
}

可以發(fā)現(xiàn),即使是不可能點(diǎn)擊的view,依然是會(huì)調(diào)用到onTouchEvent方法的,只是事件默認(rèn)沒有被處理了。

3.簡單總結(jié)一下整個(gè)流程
  • 對(duì)于一個(gè)ViewGroup,事件產(chǎn)生以后會(huì)首先傳遞到dispatchTouchEvent,如果此時(shí)onInterceptTouchEvent返回是true表示要攔截此次事件,重要的是接下來事件會(huì)交給這個(gè)ViewGroup處理,onTouchEvent就會(huì)被調(diào)用,如果onInterceptTouchEvent返回的是false,那么事件會(huì)繼續(xù)向下傳遞給子View,此時(shí)子元素的dispatchTouchEvent會(huì)被調(diào)用,依次類推,直到事件完全被處理完畢。
  • 當(dāng)View需要處理事件時(shí),如果設(shè)置了OnTouchListener(優(yōu)先級(jí)是最高的),那么OnTouchListeneronTouch方法會(huì)被調(diào)用,而OnClickListener的優(yōu)先級(jí)是處于事件傳遞的末端的。
  • 一個(gè)完整的事件序列的消費(fèi)的順序是Activity->PhoneWindow->View;如果某一個(gè)最末端的ViewonTouchEvent返回了false即不處理,此時(shí)事件上拋,父親容器的onTouchEvent會(huì)被調(diào)用,如果所有的View都處理該事件,最終事件被傳遞到Activity,則ActivityonTouchEvent會(huì)被調(diào)用。
  • 一般情況下一個(gè)事件序列只能被一個(gè)View攔截消費(fèi),同一個(gè)事件序列所有事件都會(huì)直接交給它處理,并且它的onInterceptTouchEvent不會(huì)再被調(diào)用。如果子view中調(diào)用requestDisallowInterceptTouchEvent,則會(huì)決定父view是否攔截事件(除action_down以外的事件,action_down會(huì)重置FLAG_DISALLOW_INTERCEPT的狀態(tài)值)
@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);
    }
}
  • 某個(gè)View一旦開始處理事件,如果它不消耗ACTION_DOWN(onTouchEvent返回了false),那么同一事件序列中其他事件都不會(huì)再交給它來處理,事件將重新交給他的父元素處理,即父元素的onTouchEvent會(huì)被調(diào)用。
  • 如果某個(gè)View不消耗除ACTION_DOWN以外的其他事件,那么這個(gè)點(diǎn)擊事件會(huì)消失,此時(shí)父元素的onTouchEvent并不會(huì)被調(diào)用,并且當(dāng)前View可以收到后續(xù)事件,最終這些消失的點(diǎn)擊事件會(huì)傳遞給Activity處理。
  • ViewGroup默認(rèn)不攔截任何事件,ViewGroup的onInterceptTouchEvent方法默認(rèn)返回false,View沒有onInterceptTouchEvent方法,一旦有事件傳遞給它,那么它的onTouchEvent方法就會(huì)被調(diào)用。
  • View的onTouchEvent方法默認(rèn)消耗事件(返回true),除非他是不可點(diǎn)擊的(clickable和longClickable同時(shí)為false)。View的longClickable屬性默認(rèn)都為false,clickable屬性分情況,Button默認(rèn)為true,TextView默認(rèn)為false。disable不會(huì)影響事件的消費(fèi),即時(shí)一個(gè)view是disable狀態(tài),依然會(huì)消費(fèi)事件,只是用戶無感知,即無反饋。
三、有什么用處?

開發(fā)中存在僅僅展示列表的情況,也即是不可點(diǎn)擊的列表,如果是這個(gè)需求該如何實(shí)現(xiàn)?當(dāng)然如果以RecyclerView為例可以在item禁止,那是否可以以事件的傳遞默認(rèn)不消費(fèi)點(diǎn)擊的事件呢?

上面提到的,如果某個(gè)View不消耗ACTION_DOWN事件也即是onTouchEvent返回false不就可以滿足需求了嘛?簡單使用:

1.自定義一個(gè)不可點(diǎn)擊的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,同時(shí)onInterceptTouchEvent返回true表示攔截下此次事件并且不消費(fèi)。

?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡信或評(píng)論聯(lián)系作者。

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

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