<<Android 開發藝術探索>> Chapter 3

View的事件體系

View的基礎

  1. view位置參數

    • View的位置主要由它的四個頂點來決定,分別對應于View的四個屬性:topleftrightbottom,其中top是左上角縱坐標,left是左上角橫坐標,right是右下角橫坐標,bottom是右下角縱坐標, 這四個參數的坐標值都是View相對于父View的.
      View的寬高和坐標的關系:

      width = right - left;
      height = bottom - top;
      

      如何得到這四個參數:

      Left = getLeft();
      Right = getRight();
      Top = getTop();
      Bottom = getBottom();
      
    • 從Android 3.0開始,view增加了xytranslationXtranslationY四個參數,這幾個參數也是相對于父容器的坐標. x和y是左上角的坐標,而translationX和translationY是view左上角相對于父容器的偏移量,默認值都是0.

      x = left + translationX
      y = top + translationY
      

      View在平移過程中改變的是x, y, translationX, translationY這四個參數, lefttop等是原始左上角的位置信息, 其值不會隨著平移改變.

      移動前

      移動后

      setX()內部也是調用的setTranslationX()
      setLeft()方法系統不建議我們人為調用, 因為left屬性在layout()時系統會隨時更改

    • View在滑動其內容時更改的是它的mScrollX mScrollY這兩個參數
      mScrollX的值總是等于View左邊緣和View內容左邊緣在水平方向的距離
      mScrollY的值總是等于View上邊緣和View內容上邊緣在垂直方向的距離
      scrollTo()scrollBy()內部其實就是更改這兩個參數.

  2. MotionEvent和TouchSlop

    • MotionEvent
      在手指觸摸屏幕后所產生的一系列事件中,典型的事件類型有:

      1. ACTION_DOWN ----- 手指剛接觸屏幕
      2. ACTION_MOVE ----- 手指在屏幕上移動
      3. ACTION_UP ----- 手機從屏幕上松開的一瞬間

      正常情況下,一次手指觸摸屏幕的行為會觸發一系列點擊事件,考慮如下幾種情況:

      1. 點擊屏幕后離開松開,事件序列為 DOWN -> UP
      2. 點擊屏幕滑動一會再松開,事件序列為DOWN->MOVE->...->UP

      通過MotionEvent對象我們可以得到點擊事件發生的x和y坐標,getX/getY返回的是相對于當前View左上角的x和y坐標,getRawX和getRawY是相對于手機屏幕左上角的x和y坐標。

    • TouchSlop
      TouchSlope是系統所能識別出的可以被認為是滑動的最小距離,獲取方式是ViewConfiguration.get(getContext()).getScaledTouchSlope()

  3. VelocityTracker、GestureDetector和Scroller

    1. VelocityTracker
      用于追蹤手指在滑動過程中的速度,包括水平和垂直方向上的速度.
      VelocityTracker的使用方式:

      //初始化
      VelocityTracker mVelocityTracker = VelocityTracker.obtain();
      //在onTouchEvent方法中
      mVelocityTracker.addMovement(event);
      //獲取速度
      mVelocityTracker.computeCurrentVelocity(1000);
      float xVelocity = mVelocityTracker.getXVelocity();//一般在MotionEvent.ACTION_UP的時候調用
      //重置和回收
      mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的時候調用
      mVelocityTracker.recycle(); //一般在onDetachedFromWindow中調用
      
    2. GestureDetector
      手勢檢測,用于輔助檢測用戶的點擊、滑動、長按、雙擊等行為.我們通過查看源碼,發現在GestureDetector類中封裝了兩個接口和一個內部類:


      GestureDetector

      分別為OnGestureListenerOnDoubleTapListener兩種listener.
      SimpleOnGestureListener實現了上述兩種listener, 但是內部的實現方法都為null, 使用時根據個人需要來實現對應的方法.
      GestureDetector使用方式:

      GestureDetector mGestureDetector = new GestureDetector(new SimpleOnGestureListener () {
          //實現需要用到的方法
      });
      mGestureDetector.setIsLongPressEnabled(false);//解決長按屏幕后無法拖動的現象.
      
      boolean consume = mGestureDetector.onTouchEvent(event);//一般在onTouchEvent中接管event
      return consume;
      

      OnGestureListenerOnDoubleTapListener接口具體如下:

      public interface OnGestureListener {
          boolean onDown(MotionEvent e);  //手指剛剛觸碰屏幕的一瞬間, 由一個ACTION_DOWN觸發
          void onShowPress(MotionEvent e); //手指輕輕觸碰屏幕, 尚未松開或拖動, 由一個ACTION_DOWN觸,它和onDown的區別是它強調的是沒有松開或者拖動的狀態
          boolean onSingleTapUp(MotionEvent e); //單擊行為, 伴隨著一個ACTION_UP而觸發
          boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY); //手指按下屏幕并拖動, 由一個ACTION_DOWN和多個ACTION_MOVE組成,這是拖動行為
          void onLongPress(MotionEvent e); //用戶長久的按著屏幕不放,即長按
          boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); //快速滑動行為,由一個ACTION_DOWN,多個ACTION_MOVE,一個ACTION_UP觸發
      }
      
      public interface OnDoubleTapListener {
          boolean onSingleTapConfirmed(MotionEvent e); //嚴格的單擊行為, 即這只可能是單擊而不可能是雙擊中的一次單擊
          boolean onDoubleTap(MotionEvent e); //雙擊行為,它不可能和onSingleTapConfirmed共存
          boolean onDoubleTapEvent(MotionEvent e); //表示發生了雙擊行為, 在雙擊期間ACTION_DOWN,ACTION_MOVE,ACTION_UP均會觸發此回調
      }
      

      在日常開發中,比較常用的有: onSingleTapUp(單擊)onFling(快速滑動)onScroll(拖動)onLongPress(長按)onDoubleTap(雙擊).
      建議:如果只是監聽滑動相關的事件在onTouchEvent中實現;如果要監聽雙擊這種行為的話,那么就使用GestureDetector

    3. Scroller
      彈性滑動對象,用于實現View的彈性滑動。Scroller本身無法讓View彈性滑動,它需要和View的computeScroll方法配合使用才能共同完成這個功能。
      Scroller使用方式

      Scroller scroller = new Scroller(mContext);
      
      // 緩慢滾動到指定位置
      private void smoothScrollTo(int destX, int destY) {
         int scrollX = getScrollX();
         int delta = destX - scrollX;
         //1000ms內滑動到destX的位置
         mScroller.startScroll(scrollX, 0, delta, 0, 1000);
         invalidate();
      }
      
      @Override
      public void computeScroll() {
         if(mScroller.computeScrollOffset()) {
             scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
             postInvalidate();
         }
      }
      

      原理:invalidate()方法會觸發computeScroll()方法, 然后我們重寫了computeScroll()在里面調用scrollTo來讓View移動到Scroller計算過后的位置, 然后再次觸發invalidate()方法, 直到Scroller計算完成。


View的滑動

  1. 使用scrollTo或scrollBy
    scrollTo()是基于所傳參數的絕對滑動, scrollBy()是基于目前所在位置的相對滑動.
    scrollTo()scrollBy()只能改變View內容的位置, 不能改變View在布局中的位置.

  2. 使用動畫
    android中動畫分為三種:View動畫 幀動畫 屬性動畫.
    我們通過View動畫屬性動畫都可以完成View的滑動, 使用動畫主要操作的是ViewtranslationXtranslationY這兩個屬性(因為setX()內部其實調用的時setTranslationX()).

    使用上我們需要注意以下兩點:

    • view動畫操作的是控件的影像而不是view的位置參數(它不會移動view的本身也不會移動view的內容),盡管我們在視覺上看到了滑動的效果,但實際上view的位置卻不曾發生改變。這點可以從如果我們不設置view的控件參數fillAftrer為true的時候,那么當動畫完成后,View會瞬間恢復到動畫前的效果就可以看得出來。而且,即便我們設置了fillAfter參數為true。也只是相當于把view投影到移動的位置,但當我們再要執行點擊操作的時候,卻是不能發生響應的。因為view的位置不會發生改變。它的真身仍在原始位置上。
    • view的屬性動畫可以解決上面的問題, 但是它無法兼容3.0以下的版本.
  3. 通過改變布局參數
    通過改變布局參數的方式來實現滑動,實際上改變的是LayoutParams參數,如果我們想要滑動某個控件,則直接通過修改LayoutParams參數來實現,這個方法最為簡單暴力,但操作較為復雜,需要根據不同的情況去做不同的處理。使用方法如下(以移動一個Button為例):

    Button button = (Button) findViewById(R.id.btn_changeparams);
    MarginLayoutParams params = (MarginLayoutParams) button.getLayoutParams();
    params.width += 100;
    params.leftMargin +=100;
    button.requestLayout();
    
  4. 三種滑動方式對比:

    • scrollTo/scrollBy: 操作簡單,適合對View內容的滑動
    • 動畫: 操作簡單,主要適用于沒有交互的View和實現復雜的動畫效果
    • 改變布局參數: 操作稍微復雜,適用于有交互的View

View彈性滑動

  1. 使用Scroller
    上面已經介紹過了Scroller的原理和使用方法

  2. 使用動畫
    采用這種方法除了能完成彈性滑動以外,還可以實現其他動畫效果,我們完全可以在onAnimationUpdate方法中加上我們想要的其他操作。

  3. 使用延時策略
    使用延時策略來實現彈性滑動,它的核心思想是通過發送一系列延時消息從而達到一種漸進式的效果,具體來說可以使用HandlersendEmptyMessageDelayed(xxx)viewpostDelayed()方法,也可以使用線程的sleep方法。

    private Handler = new Handler(){
        public void handleMwssage(Message msg){
            switch(msg.what){
                case  MOVE_VIEW:
                //move view step
                handle.sendEmptyMessageDelayed(MOVE_VIEW,1000);
                break;
            }
        }
    };
    

View的事件分發機制

  1. 點擊事件的傳遞規則
    所謂點擊事件的事件分發,其實就是對MotionEvent的分發過程。當一個MotionEvent產生之后,系統需要將其傳遞給某個具體的View,比如Button控件,并且被這個View所消耗。整個事件分發過程由三個方法完成,分別是:

    • dispatchTouchEvent(MotionEvent event)
      /**
       * 這個方法用來進行事件的分發,當MotionEvent事件傳遞到當前View時,便會觸發當前View的這個方法,
       * 返回的結果受當前View的onTouchEvent和下級的dispatchTouchEvent方法的影響,表示是否消耗該MotionEvent。
       * true表示被當前View所消耗,false則表示事件未被消耗。
       */
      public boolean dispatchTouchEvent(MotionEvent event);
      
    • onInterceptTouchEvent(MotionEvent event)
      /**
       * 這個方法在dispatchTouchEvent方法內部調用,用來判斷是否攔截某個事件,
       * 如果當前View攔截了某個事件,那么在同一個事件序列中,此方法不會再被調用,
       * 返回結果表示是否攔截當前事件。
       */
      public boolean onInterceptTouchEvent(MotionEvent event);
      
    • onTouchEvent(MotionEvent event)
      /**
       * 這個方法在dispatchTouchEvent方法內部調用,用來處理點擊事件,
       * 返回結果表示是否消耗當前事件,如果不消耗(ACTION_DOWN),則在同一事件序列中,當前View無法再次接收到該事件。
       */
       public boolean onTouchEvent(MotionEvent event);
      

    以上三者的關系可以用偽代碼進行表示:

    public boolean dispatchTouchEvent(MotionEvent event){
        boolean consume = false;
        if(onInterceptTouchEvent(event)){
            consume = onTouchEvent(event);
        }else{
            consume = childView.dispatchTouchEvent(event);
        }
        return consume;
    }
    

    對于一個根ViewGroup來說,當產生點擊事件后,首先會傳遞給它,此時調用它的dispatchTouchEvent
    方法,如果dispatchTouchEvent方法中的onInterceptTouchEvent(event)返回true,則表示這個ViewGroup要消耗當前事件,于是調用ViewGroupOnTouchEvent(event)方法。而如果onInterceptTouchEvent(event)返回的是false,則將該event交給這個當前View的子元素的dispatchTouchEvent去處理。如此遞歸,直到事件被最終處理掉。

    當一個點擊事件產生后,它的傳遞順序如下:Activity -> Window -> View
    Activity是怎么接收到點擊事件的請參考這篇文章
    當頂級View接收到該事件后,就會將其按照事件分發機制去分發該事件,也即從父容器到子容器間層層傳遞,直到在某一個階段事件被消耗完畢。但在這里存在另一個問題:如果最底層的子元素并沒有消耗點擊事件,怎么辦?為解決這個問題,系統做了以下的措施:如果一個View的onTouchEvent方法返回的是false,那么該view的父容器的onTouchEvent方法也會被調用,以此類推,若該點擊事件沒有任何元素去消耗,那么最終仍是會由Activity進行處理

    關于事件傳遞的機制,有以下結論:

    1. 同一個事件序列是指從手指接觸到屏幕的那一刻起,到手指離開屏幕的那一刻結束。期間以Down為開始,中間含有數量不等(可以為0)的MOVE,最終則以UP結束。
    2. 正常情況下,一個事件序列只能被一個View攔截且進行消耗。
    3. 某個View一旦決定攔截事件序列,那么這一個事件序列只能由它來處理(只要在這個view進行攔截之前沒有其他view對這個事件序列進行攔截),并且它的onInterceptTouchEvent方法也不會再被調用。
    4. 某個View一旦開始處理事件序列,如果它不消耗ACTION_DOWN事件(OnTouchEvent返回false),那么同一個事件序列中的其他事件都不會由它來處理,而是直接將其交由父元素去處理。并且當前view是無法再次接收到該事件的。
    5. 如果View不消耗除了ACTION_DOWN之外的其他事件,那么這個點擊事件就會消失,并且父元素的OnTouchEvent方法也不會被調用,同時,當前View可以持續收到后續的事件,最終這些消失的點擊事件會交由Activity進行處理。
    6. ViewGroup不攔截任何事件。Android源碼中ViewGrouponInterceptTouchEvent方法默認返回false
    7. Android源碼中,View并沒有onInterceptTouchEvent方法,一旦有點擊事件傳遞給它。那么它的OnTouchEvent方法就會被調用。
    8. viewOnTouchEvent默認會消耗該事件(默認返回true),除非它是不可點擊的(clickablelongclickable同時為false)。
    9. viewenable屬性不影響onTouchEvent的默認放回值。即便該viewdisable狀態的,但只要它的clickablelongClickable有一個為true,那么它的返回值就為true
    10. onclick會發生的前提是當前View是可點擊的,并且它接收到了ACTION_DOWNACTION_UP事件。
    11. 事件傳遞過程是由外向內的,及事件總是先傳遞給父元素。然后再有父元素去分發給子元素。但通過requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的分發過程,但ACTION_DOWN事件除外。
  2. 從源碼去看事件分發機制:

    • Activity分發
      從上面我們知道,每個MotionEvent都是最先交由Activity進行的,那么我們來看看Activity中的dispatchTouchEvent方法

       public boolean dispatchTouchEvent(MotionEvent ev) {
          if (ev.getAction() == MotionEvent.ACTION_DOWN) {
              onUserInteraction();
          }
          if (getWindow().superDispatchTouchEvent(ev)) {
              return true;
          }
          return onTouchEvent(ev);
      }
      
    • Window分發
      我們可以看到Activity其實是將點擊事件交給了Window進行下一步處理, 但是Window類其實是一個抽象類, 它里面的superDispatchTouchEvent()方法是一個抽象方法.
      所以我們需要去它的唯一實現類PhoneWindow中去查看superDispatchTouchEvent()是如何實現的.

      //PhoneWindow中的superDispatchTouchEvent()方法
      public boolean superDispatchTouchEvent(MotionEvent event){
          return mDecor.superDispatchTouchEvent(event);
      }
      

      這里的mDecor其實就是DecorView,那么DecorView是什么呢?我們來看

      private final class DecorView extends FrameLayout implements RootViewSurfaceTacker{
          private DecorView mDecor;
          @override
          public final View getDecorView(){
              if(mDecor == null){
                  installDecor();
              }
              return mDecor;
          }
      }
      

      我們知道,通過(ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content).getChildAt(0);這種方式可以獲取Activity的所預設置的View,而這個mDector顯然就是返回的對象。也就是說,這里的DecorView是頂級View(ViewGroup),內部有titlebarcontentParent兩個子元素,contentParentidcontent,而我們設置的main.xml布局則是contentParent里面的一個子元素。那么,當事件傳遞到DecorView這里的時候,因為DecorView繼承了FrameLayout且還是父View,所以最終的事件會傳送到我們在setContentView()所設置的頂級View中。

    • ViewGroup分發
      那么,現在事件已經傳遞到頂級View(一個ViewGroup)了,接下來又該是怎樣的呢?邏輯思路如下:

      頂級View調用dispatchTouchEvent方法
      if 頂級view需要攔截事件(onInterceptTouchEvent方法返回true)
        處理點擊事件
      else
        把事件傳遞給子元素進行處理
      

      根據這個,我們先來看一下ViewGroup對點擊事件的分發過程,其主要體現在dispatchTouchEvent方法中。因為這個方法比較長,分段說明,先看下面一段:

      public boolean dispatchTouchEvent(MotionEvent ev) {
          //....省略
          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;
            }
          //....省略
      }
      

      從上面的代碼可以看出,ViewGroup會在兩種情況下判斷是否攔截當前事件:一是事件類型為ACTION_DOWN,二則是mFirstTouchTarget != null。在這里,mFirstTouchTarget是什么意思呢? 可以這么理解:當事件由ViewGroup的子元素成功處理時,mFirstTouchTarget會被賦值并指向子元素。也就是說,當ViewGroup不攔截事件并且把事件交給子元素處理時,則mFirstTouchTarget != null。反之,如果ViewFroup攔截了這個事件,則mFirstTouchTarget != null就不成立, 所以當ACTION_MOVEACTION_UP事件到來時, actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != nullfalse, ViewGroup將會直接攔截事件而不會再次調用它自己的onInterceptTouchEvent(ev)方法,并且同一序列中的其他事件會交由它處理(前提是事件到達它之前沒有被攔截)。對上面第3條結論的驗證

      當然,事實無絕對,此處有一個特殊情況,就是FLAG _DISALLOW _INTERCEPT這個標志位,它是通過requestDisallowInterceptTouchEvent()方法來設置的,一般用于子View中。它一旦被設置,ViewGroup則將無法攔截除了ACTION _DOWN以外的其他點擊事件。為什么是除了ACTION_DOWN以外呢?

      public boolean dispatchTouchEvent(MotionEvent ev) {
          //省略...
          // Handle an initial down.
          if (actionMasked == MotionEvent.ACTION_DOWN) {
              // Throw away all previous state when starting a new touch gesture.
              // The framework may have dropped the up or cancel event for the previous gesture
              // due to an app switch, ANR, or some other state change.
              cancelAndClearTouchTargets(ev);
              resetTouchState();
          }   
          //省略...
      }
      

      在這段源碼中,ViewGroup會在ACTION_DOWN事件到來時做重置狀態的操作,而在 resetTouchState方法中會對FLAG _DISALLOW _INTERCEPT進行重置,因此子View調用requestDisallowInterceptTouchEvent方法時并不能影響ViewGroupACTION _DOWN的影響。
      接著我們再看當ViewGroup不攔截事件的時候。事件會向下分發,交由它的子View進行處理的過程:

      public boolean dispatchTouchEvent(MotionEvent ev) {
        // 省略...View的LONG_CLICKABLE屬性默認為false,而CLICKABLE的屬性則和具體的View有關。通過setClickable和setLongClickable方法可以修改這兩個值。此外,在setOnClickListener中也會自動將CLICKABLE屬性改為true,而setOnLongClickListener則將LONG _CLICKABLE設置為true。
        final View[] children = mChildren;
        for (int i = childrenCount - 1; i >= 0; i--) {
              final int childIndex = customOrder
                      ? getChildDrawingOrder(childrenCount, i) : i;
              final View child = (preorderedList == null)
                      ? children[childIndex] : preorderedList.get(childIndex);
              // 如果一個child沒有播放動畫&&點擊事件落在了它的區域內
              if (!canViewReceivePointerEvents(child)
                      || !isTransformedTouchPointInView(x, y, child, null)) {
                  continue;
              }
              // 省略...
              if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                  // 省略...
                  // 這個child消耗了這個點擊事件, 對mFirstTouchTarget賦值
                  newTouchTarget = addTouchTarget(child, idBitsToAssign);
                  alreadyDispatchedToNewTouchTarget = true;
                  break;
              }
          }
          // 省略...
          if (mFirstTouchTarget == null) {
              // 沒有子View消耗了點擊事件
              handled = dispatchTransformedTouchEvent(ev, canceled, null,
                      TouchTarget.ALL_POINTER_IDS);
          }
      }
      

      從源碼中,我們可以發現它的過程如下:首先遍歷ViewGroup的所有子元素,然后判定子元素是否能夠接收到點擊事件(子元素是否在播動畫或者點擊事件的坐標是否落在子元素的區域內)。如果某個子元素滿足這兩個條件,那么事件就會交由它來處理。可以看到,dispatchTransformedTouchEvent方法實際上調用的就是子元素的dispatchTouchEvent方法。怎么看的呢?在這個方法的內部,有這么一段:

      private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
          View child, int desiredPointerIdBits) {
          // 省略...
          if (child == null) {
              handled = super.dispatchTouchEvent(event);
          } else {
              handled = child.dispatchTouchEvent(event);
          }
          // 省略...
          return handled;
      }
      

      返回上一段源碼,如果子元素的dispatchTouchEvent(event)方法返回true,那么我們就不需考慮事件在子元素是怎么派發的,那么mFirstTouchTarget就會被賦值,同時跳出for循環。從源碼中抽取相關部分見下:

      newTouchTarget = addTouchTarget(child, idBitsToAssign);
      alreadyDispatchedToNewTouchTarget = true;
      break;
      

      有人說,這段代碼并沒有對mFirstTouchTarget的賦值,因為它實際上出現在addTouchTarget方法中,源碼如下:

      private TouchTarget addTouchTarget(View child, int pointerIdBits) {
          TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
          target.next = mFirstTouchTarget;
          mFirstTouchTarget = target;
          return target;
      }
      

      從這個方法的內部結構可以看出,mFirstTouchTarget是以一種單鏈表結構,它的賦值與否直接影響到了ViewGroup的攔截策略。

      接下來我們再次返回最初的源碼中, 如果遍歷所有的子元素事件后都沒有被合適地處理,這包含兩種情況:一是ViewGroup中沒有子元素,二則是子元素處理了點擊事件,但是在dispatchTouchEvent方法中返回了false在這兩種情況下,ViewGroup會調用它自己的onTouchEvent()處理點擊事件

      if (mFirstTouchTarget == null) {
          // 沒有子View消耗了點擊事件
          handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
      }
      

      注意這一段源碼的第三個參數childnull,從前面的分析就可以知道,它會調用super.dispatchTouchEvent(event),很顯然,這里就從ViewGroup轉到了ViewdispatchTouchEvent(event)
      在隨后我們對ViewdispatchTouchEvent(event)分析中我們會發現, ViewdispatchTouchEvent(event)會調用onTouchEvent()方法.

      注意:在這時View的dispatchTouchEvent()中其實調用的是ViewGroup中的onTouchEvent()方法.
      因此當一個ViewGroupACTION_DOWN事件沒有被子View消耗時, 這個ViewGroup本身的onTouchEvent()就會被調用來處理這個點擊事件(對上面第4條結論的驗證)

      這時你們可能會奇怪, 為什么我們在ViewdispatchTouchEvent()方法中調用ViewGroup中的onTouchEvent()方法.

      我們來看下面這段代碼:

      public class A {
          public void AA() {
              System.out.println("A.AA");
              BB();
          }
      
          public void BB() {
              System.out.println("A.BB");
          }
      }
      
      public class B extends A {
          @Override
          public void AA() {
              System.out.println("B.AA");
          }
      
          @Override
          public void BB() {
              System.out.println("B.BB");
          }
      
          public void CC() {
              super.AA();
          }
      }
      

      我們定義兩個類A和B, A和B中都有AABB方法, 并且輸出不同的Log, 那么此時我們執行new B().CC()會輸出什么結果呢?
      答案是:

      A.AA
      B.BB
      

      是不是猜錯了?
      為什么會是這樣的結果呢, 因為我們是在B中調用的super.AA(), 因此在A的AA()方法中我們調用this其實拿到的是一個B的引用, 如下圖

      Screenshot from 2018-03-08 17:19:40.png

      所以在A的AA()方法中我們會執行B的BB()方法.

      現在是不是就明白了, 為什么我們在View的dispatchTouchEvent()中調用的是ViewGroup中的onTouchEvent()方法了? 因為View的dispatchTouchEvent()是通過ViewGroup調起來的.

    • View分發
      接下來我們回過頭繼續看ViewdispatchTouchEvent()方法

      public boolean dispatchTouchEvent(MotionEvent event) {
          boolean result = false;
          // 省略...
          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;
          }
          // 省略...
          return result;
      }
      

      View對點擊事件的處理過程比較簡單,因為View是一個單獨的元素,因此無法向下傳遞事件。所以它只能自己處理事件。從上面的源碼可以看出View對點擊事件的處理過程:首先判斷有沒有設置onTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不會被調用,由此可見OnTouchListener方法的優先級高于onTouchEvent

      接下來,分析onTouchEvent的實現。先看當View處于不可用狀態下點擊事件的處理過程:

      public boolean onTouchEvent(MotionEvent event) {
          // 省略...
          if ((viewFlags & ENABLED_MASK) == DISABLED) {
              if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                 setPressed(false);
              }
              // A disabled view that is clickable still consumes the touch
              // events, it just doesn't respond to them.
              return (((viewFlags & CLICKABLE) == CLICKABLE ||
                      (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
          }
          // 省略...
      }
      

      很顯然,不可用狀態下的view照樣會消耗點擊事件,盡管它看起來不可用。

      接著,再來看一下onTouchEvent方法中對點擊事件的具體處理:

      public boolean onTouchEvent(MotionEvent event) {
          // 省略...
          if (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
              switch (event.getAction()) {
                  case MotionEvent.ACTION_UP:
                      // 省略...
                      if (mPerformClick == null) {
                          mPerformClick = new PerformClick();
                      }
                      if (!post(mPerformClick)) {
                          performClick();
                      }
                      // 省略...
                      break;
                  case MotionEvent.ACTION_DOWN:
                      // 省略...
                      break;
                  case MotionEvent.ACTION_CANCEL:
                      // 省略...
                      break;
                  case MotionEvent.ACTION_MOVE:
                      // 省略...
                      break;
              }
              return true;
          }
      }
      

      從源碼來看,只要ViewCLICKABLELONG_CLICKABLE有一個為true,那么它就將消耗這個事件,即onTouchEvent返回true, 不管它是不是DISABLE狀態。
      而當MOTION_UP事件發生時,則觸發performClick()方法,如果View設置了onClickListener,那么performClick()方法內部會調用它的onClick方法

      ViewLONG_CLICKABLE屬性默認為false,而CLICKABLE的屬性則和具體的View有關。通過setClickablesetLongClickable方法可以修改這兩個值。此外,在setOnClickListener中也會自動將CLICKABLE屬性改為true,而setOnLongClickListener則將LONG_CLICKABLE設置為true


view的滑動沖突

Android中的滑動沖突是比較常見的一個問題,只要在界面中內外兩層同時滑動的時候,就會產生滑動。意即有一個占主導地位的View搶著去執行滑動操作,從而帶來非常差的用戶體驗。常見的滑動沖突場景分為如下三種:

  • 場景一:外部滑動方向與內部滑動方向不一致,主要是將ViewPager和Fragment配合使用所形成的頁面滑動效果。在這個效果中,可以通過左右滑動來切換頁面,而每個頁面內部往往又是一個Listview。這種情況下本來是很容易發生滑動沖突的,但ViewPager內部處理了這種滑動沖突,所以如果使用ViewPager,則無需擔心這個問題。但如果使用的是Scroller,則必須手動處理滑動沖突了。否則后果就是內外兩層只能有一層能夠滑動。
    處理規則:當用戶左右滑動時,需要讓外部的View攔截點擊事件。當用戶上下滑動時,需要讓內部View攔截點擊事件。這個時候我們就可以根據它們的特征來解決滑動沖突。具體來說是:根據滑動的方向判斷到底由什么來攔截事件。

  • 場景二:外部滑動和內部滑動方向一致,比如ScrollView嵌套ListView,或者是ScrollView嵌套自己。表現在要么只能有一層能夠滑動,要么兩者滑動起來顯得十分卡頓。
    處理規則:從業務上尋找突破點,比如業務上有規定:當處于某種狀態時需要外部View處理用戶的操作,而處理另一種狀態時則讓內部View處理用戶的操作。

  • 場景三:上面兩種情況的嵌套。
    處理規則:同場景二

滑動沖突的解決方式:

針對場景一的滑動沖突,有兩種處理滑動的解決方式:

  • 外部攔截法:
    所謂外部攔截法是指點擊事件都先經過父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要此事件就不攔截,這樣就可以解決滑動沖突的問題,這個方法需要重寫父容器的onInterceptTouchEvent()方法。偽代碼如下所示:
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted=false;
        int x=(int)event.getX();
        int y=(int)event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                intercepted=false;
                break;
            case MotionEvent.ACTION_MOVE:
                if(父容器需要當前點擊事件){
                   intercepted=true;
                }else {
                    intercepted=false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted=false;
                break;
            default:
                break;
        }
        mLastXIntercept=x;
        mLastYIntercept=y;
        return intercepted;
    }
    
  • 內部攔截法:
    內部攔截法是指父容器不攔截任何事件,所有的事件傳遞給子元素,如果子元素需要此事件就直接消耗掉,如果不需要則交由父容器處理。需要配合requestDisallowInterceptTouchEvent()方法才能正常工作。偽代碼如下:
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x=(int)event.getX();
        int y=(int)event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX=x-mLastX;
                int deltaY=y-mLastY;
                if(父容器需要當前點擊事件){
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        mLastX=x;
        mLastY=y;
        return super.dispatchTouchEvent(event);
    }
    
    另外,為了使父容器不接收ACTION_DOWN事件,我們需要對父類進行一下修改:
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        int action=event.getAction();
        if (action==MotionEvent.ACTION_DOWN){
            return false;
        }else{
            return true;
        }
    }
    
    以上兩種方式,是針對場景一而得出的通用的解決方法。對于場景二和場景三而言,只需改變相關的滑動規則的邏輯即可。
    注意:因為內部攔截法的操作較為復雜,因此推薦采用外部攔截法來處理常見的滑動沖突。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容