由Android禁止viewpager滑動,想到的安卓事件分發機制

前言

產品有個需求是兩個tab頁面可以左右切換,當時立馬想到我用viewPager+fragment,但是我們知道viewPager默認是可以左右滑動的,而我的需求是隻可點擊不可滑動,於是我就翻了一下viewpager的API發現並沒有可以設置是否可以滑動的相關方法。於是我就想是否可以通過事件的分發機制去攔截它左右滑動的touch。果不其然!

public class myViewpager extends ViewPager {
    private boolean canTouch = false;//設置默認不滑動

    public myViewpager(@NonNull Context context) {
        super(context);
    }

    public myViewpager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return canTouch;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return canTouch;
    }

    public void setCanTouch(boolean canTouch) {//提供設置是否可以點擊方法默認不可以
        this.canTouch = canTouch;
    }
}

可以看到這裏重寫了onTnterceptTouchEvent、onTouchEvent並且返回false事情就這樣實現了!

事件分發機制

但是它的事件是如何傳遞的呢?這就得從android的事件分發機制說起:
說到它必須排上三個方法:

dispatchTouchEvent()
onInterceptTouchEvent()
onTouchEvent()

字面意思其實就是事件的分發、攔截、觸摸的處理。
我們知道,安卓中的控件千千萬,也就是說每一個view都有自己的事件分發,那我怎麼能找到他們的規律呢?總不能以偏概全吧?是的,我們Java的三大特性是啥?封裝、繼承、多態我們看它的父類不難發現所有的自定義控件都是繼承view或者是viewgroup,還有一個特殊的就是我們的activity。activity是一個可視化窗口(我是這麼理解的)你把所有的控件擺在上面,當然它也能處理一些事件。因此我們事件的作用對象可以大致分爲:

View的事件分發機制
ViewGroup的事件分發機制
Activity的事件分發機制

產生一個事件

事件是如何產生並進行分發的?

當屏幕被觸摸時(view或者viewGroup),將會產生點擊事件touch,touch是事件的具體實現像:UP(擡起)、DOWN(按下)、MOVE(移動)、CANCEL(取消)。

聽着聽着是不是感覺這個不就是咱們的MotionEvent,還就是,我們的touch細節就被封裝在MotionEvent方法裏面而我們的MotionEvent方法在哪調用了?

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

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

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

是的就是我們前面排出的三大方法。想搞懂事件分發機制我們先看看這個方法。

MotionEvent

MotionEvent常用四種類型:

MotionEvent.ACTION_DOWN 按下View(所有事件的開始)
MotionEvent.ACTION_UP 擡起View(與DOWN對應)
MotionEvent.ACTION_MOVE 滑動View
MotionEvent.ACTION_CANCEL 結束事件

當然你打開MotionEvent會發現裏面有很多touch的類型方法如:ACTION_HOVER_MOVE、ACTION_POINTER_UP
總結一下:一般情況下,事件列都是以DOWN事件開始、UP事件結束,中間有無數的MOVE事

事件分發機制流程

上面咱們看了一個重要的方法MotionEvent(處理事件的類型爲事件分發做準備),到這我們看一下事件分發的流程。

事件分發的傳遞順序是:Activity -> ViewGroup -> View
即:點擊事件發生後,事件先傳到Activity、再傳到ViewGroup、最終再傳到 View

這裏說一下View和ViewGroup區別:大白話說,我們去自定義控件的時候可能這個控件不像Button一樣,是一個簡單的繪製的一個距形框上繪製字體,比如LineaLayout,我們自帶的可以包含子佈局的控件,ViewGroup相比較View多一個onLayout方法,也就是設置子佈局擺放位置的相關方法。但是viewGroup最終調用的還是View,所以說viewGroup也是view多一個子類,但是它是一個特殊的view因爲view可以分爲兩類:我本身只是個view,我不會再容下其他View。而我們的特殊View(ViewGroup)我這個view還可以放其他view,我可以約束你,讓你規範。(好難表達)?

事件分發過程

方法 作用 調用時機
dispatchTouchEvent() 分發點擊事件 當事件傳遞給view時(按下屏幕時機)
onTouchevent () 處理點擊事件 在dispatchTouchEvent內部事件沒有被父類消耗掉是調用
onInterceptTouchEvent() 判斷是否攔截某個點擊事件,View無改方法 在ViewGroup的dispatchTouchEvent內部調用

這裏可以看到對於view、viewGroup、Activity他們的事件分發機制是不同的,說到這最難的時候來了,空說無憑啊,他們源碼到底是如何實現的呢?

Activity的事件分發

在Activity中分別調用者三個方法(注意包名)
activity是:package android.app;
View、ViewGroup是:package android.view;

        this.dispatchTouchEvent();
        view.dispatchTouchEvent()
        viewGroup.dispatchTouchEvent()

點開this.dispatchTouchEvent();

  public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();//方法1
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;//方法2
        }
        return onTouchEvent(ev);//方法3
    }
    方法1:
    看到的是如下的空方法:大概意思是你需要自己去實現,處理通知欄相關的,比如你下滑、上推通知欄,也就是用戶實現交互的方式。
      public void onUserInteraction() {
    }
    方法2:
    getWindow().superDispatchTouchEvent(ev)
    mDecor.superDispatchTouchEvent(event)
   屬於頂層View(DecorView)
      a. DecorView類是PhoneWindow類的一個內部類
      b. DecorView繼承自FrameLayout,是所有界面的父類
      c. FrameLayout是ViewGroup的子類,故DecorView的間接父類 = ViewGroup
  */
    @return boolean Return true if this event was consumed
    這個是官方的註釋,若getWindow().superDispatchTouchEvent(ev)的返回true表示該事件停止往下傳遞,被消耗掉。
    方法3:
      public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }
    這個是對view的邊界處理,點擊事件在邊界外返回true,否則返回false,而這個mWindow.shouldCloseOnTouch(this, event)內部實現的是對邊界判斷。
    同時我們也發現Activity是沒有onInterceptTouchEvent方法的。這是因爲Android裏面只有可以作爲雙親的視圖纔會有onInterceptTouchEvent,因此也只有ViewGroup纔有這個方法,用來截取觸摸事件

這裏簡單總結一下:

判斷事件是否被消耗
false
true
開始
調用dispatchTouchEvent
getWindow.superDispatchTouchEvent
mDecor.superDispatchTouchEvent<實現了事件從Activity到viewGroup的傳遞>
Activity.dispatchTouchEvent將結果返回給onTouchEvent
Activity.OnTouchEvent到此事件結束同時判斷邊界問題
Activity.dispatchTouchEvnet 返回true
結束

這便是Activity的事件傳遞

viewGroup事件分發

嗯,乍一看完全看不懂,要比Activity的dispatchTouchEvent要複雜的多,但是,我們還是可以看懂一點的。我們知道viewgroup是有onInterceptTouchEvent這裏是對事件是否攔截做的處理,源碼裏也有體現

  // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
disallowIntercept = 是否禁用事件攔截的功能(默認是false),可通過調用requestDisallowInterceptTouchEvent()修改
 !onInterceptTouchEvent(ev) = 對onInterceptTouchEvent()返回值取反
                    // a. 若在onInterceptTouchEvent()中返回false(即不攔截事件),就會讓第二個值爲true,從而進入到條件判斷的內部
                    // b. 若在onInterceptTouchEvent()中返回true(即攔截事件),就會讓第二個值爲false,從而跳出了這個條件判斷


                    final int childrenCount = mChildrenCount;
                    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 (!canViewReceivePointerEvents(child)
                                    || !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();
                    }
這裏是對內部view循環判斷處理,找到當前點擊對view
  // 條件判斷的內部調用了該View的dispatchTouchEvent()
 // 即 實現了點擊事件從ViewGroup到子View的傳遞(具體請看下面的View事件分發機制)

所以有以下事件分發的流程圖
在這裏插入圖片描述#### View的事件分發機制
view和ViewGroup是在同一個類。

public boolean dispatchTouchEvent(MotionEvent event) {  

        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
                mOnTouchListener.onTouch(this, event)) {  
            return true;  
        } 
        return onTouchEvent(event);  
  }
  // 說明:只有以下3個條件都爲真,dispatchTouchEvent()才返回true;否則執行onTouchEvent()
  //     1. mOnTouchListener != null
  //     2. (mViewFlags & ENABLED_MASK) == ENABLED
  //     3. mOnTouchListener.onTouch(this, event)
  // 下面對這3個條件逐個分析


/**
  * 條件1:mOnTouchListener != null
  * 說明:mOnTouchListener變量在View.setOnTouchListener()方法裏賦值
  */
  public void setOnTouchListener(OnTouchListener l) { 

    mOnTouchListener = l;  
    // 即只要我們給控件註冊了Touch事件,mOnTouchListener就一定被賦值(不爲空)
        
} 

/**
  * 條件2:(mViewFlags & ENABLED_MASK) == ENABLED
  * 說明:
  *     a. 該條件是判斷當前點擊的控件是否enable
  *     b. 由於很多View默認enable,故該條件恆定爲true
  */

/**
  * 條件3:mOnTouchListener.onTouch(this, event)
  * 說明:即 回調控件註冊Touch事件時的onTouch();需手動複寫設置,具體如下(以按鈕Button爲例)
  */
    button.setOnTouchListener(new OnTouchListener() {  
        @Override  
        public boolean onTouch(View v, MotionEvent event) {  
     
            return false;  
        }  
    });
    // 若在onTouch()返回true,就會讓上述三個條件全部成立,從而使得View.dispatchTouchEvent()直接返回true,事件分發結束
    // 若在onTouch()返回false,就會使得上述三個條件不全部成立,從而使得View.dispatchTouchEvent()中跳出If,執行onTouchEvent(event)

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章