<<Android 開發(fā)藝術探索>> Chapter 3

View的事件體系

View的基礎

  1. view位置參數(shù)

    • View的位置主要由它的四個頂點來決定,分別對應于View的四個屬性:top芦缰、left垄提、rightbottom评矩,其中top是左上角縱坐標叶堆,left是左上角橫坐標,right是右下角橫坐標斥杜,bottom是右下角縱坐標, 這四個參數(shù)的坐標值都是View相對于父View的.
      View的寬高和坐標的關系:

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

      如何得到這四個參數(shù):

      Left = getLeft();
      Right = getRight();
      Top = getTop();
      Bottom = getBottom();
      
    • 從Android 3.0開始虱颗,view增加了xy蔗喂、translationX忘渔、translationY四個參數(shù),這幾個參數(shù)也是相對于父容器的坐標. x和y是左上角的坐標缰儿,而translationX和translationY是view左上角相對于父容器的偏移量畦粮,默認值都是0.

      x = left + translationX
      y = top + translationY
      

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

      移動前

      移動后

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

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

  2. MotionEvent和TouchSlop

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

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

      正常情況下乖阵,一次手指觸摸屏幕的行為會觸發(fā)一系列點擊事件宣赔,考慮如下幾種情況:

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

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

    • TouchSlop
      TouchSlope是系統(tǒng)所能識別出的可以被認為是滑動的最小距離钩蚊,獲取方式是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
      手勢檢測砰逻,用于輔助檢測用戶的點擊、滑動泛鸟、長按蝠咆、雙擊等行為.我們通過查看源碼,發(fā)現(xiàn)在GestureDetector類中封裝了兩個接口和一個內部類:


      GestureDetector

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

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

      OnGestureListenerOnDoubleTapListener接口具體如下:

      public interface OnGestureListener {
          boolean onDown(MotionEvent e);  //手指剛剛觸碰屏幕的一瞬間北滥, 由一個ACTION_DOWN觸發(fā)
          void onShowPress(MotionEvent e); //手指輕輕觸碰屏幕刚操, 尚未松開或拖動, 由一個ACTION_DOWN觸碑韵,它和onDown的區(qū)別是它強調的是沒有松開或者拖動的狀態(tài)
          boolean onSingleTapUp(MotionEvent e); //單擊行為赡茸, 伴隨著一個ACTION_UP而觸發(fā)
          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觸發(fā)
      }
      
      public interface OnDoubleTapListener {
          boolean onSingleTapConfirmed(MotionEvent e); //嚴格的單擊行為辙纬, 即這只可能是單擊而不可能是雙擊中的一次單擊
          boolean onDoubleTap(MotionEvent e); //雙擊行為,它不可能和onSingleTapConfirmed共存
          boolean onDoubleTapEvent(MotionEvent e); //表示發(fā)生了雙擊行為叭喜, 在雙擊期間ACTION_DOWN贺拣,ACTION_MOVE,ACTION_UP均會觸發(fā)此回調
      }
      

      在日常開發(fā)中捂蕴,比較常用的有: onSingleTapUp(單擊)譬涡、onFling(快速滑動)onScroll(拖動)啥辨、onLongPress(長按)onDoubleTap(雙擊).
      建議:如果只是監(jiān)聽滑動相關的事件在onTouchEvent中實現(xiàn)溉知;如果要監(jiān)聽雙擊這種行為的話陨瘩,那么就使用GestureDetector恒傻。

    3. Scroller
      彈性滑動對象,用于實現(xiàn)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()方法會觸發(fā)computeScroll()方法, 然后我們重寫了computeScroll()在里面調用scrollTo來讓View移動到Scroller計算過后的位置割择, 然后再次觸發(fā)invalidate()方法眷篇, 直到Scroller計算完成。


View的滑動

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

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

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

    • view動畫操作的是控件的影像而不是view的位置參數(shù)(它不會移動view的本身也不會移動view的內容)荔泳,盡管我們在視覺上看到了滑動的效果蕉饼,但實際上view的位置卻不曾發(fā)生改變。這點可以從如果我們不設置view的控件參數(shù)fillAftrer為true的時候玛歌,那么當動畫完成后昧港,View會瞬間恢復到動畫前的效果就可以看得出來。而且支子,即便我們設置了fillAfter參數(shù)為true创肥。也只是相當于把view投影到移動的位置,但當我們再要執(zhí)行點擊操作的時候值朋,卻是不能發(fā)生響應的叹侄。因為view的位置不會發(fā)生改變。它的真身仍在原始位置上昨登。
    • view的屬性動畫可以解決上面的問題, 但是它無法兼容3.0以下的版本.
  3. 通過改變布局參數(shù)
    通過改變布局參數(shù)的方式來實現(xiàn)滑動趾代,實際上改變的是LayoutParams參數(shù)绘搞,如果我們想要滑動某個控件番电,則直接通過修改LayoutParams參數(shù)來實現(xiàn),這個方法最為簡單暴力渔伯,但操作較為復雜,需要根據(jù)不同的情況去做不同的處理尿褪。使用方法如下(以移動一個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和實現(xiàn)復雜的動畫效果
    • 改變布局參數(shù): 操作稍微復雜杖玲,適用于有交互的View

View彈性滑動

  1. 使用Scroller
    上面已經(jīng)介紹過了Scroller的原理和使用方法

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

  3. 使用延時策略
    使用延時策略來實現(xiàn)彈性滑動臼闻,它的核心思想是通過發(fā)送一系列延時消息從而達到一種漸進式的效果,具體來說可以使用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的事件分發(fā)機制

  1. 點擊事件的傳遞規(guī)則
    所謂點擊事件的事件分發(fā),其實就是對MotionEvent的分發(fā)過程蕉毯。當一個MotionEvent產(chǎn)生之后乓搬,系統(tǒng)需要將其傳遞給某個具體的View,比如Button控件代虾,并且被這個View所消耗进肯。整個事件分發(fā)過程由三個方法完成,分別是:

    • dispatchTouchEvent(MotionEvent event)
      /**
       * 這個方法用來進行事件的分發(fā)棉磨,當MotionEvent事件傳遞到當前View時江掩,便會觸發(fā)當前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來說叔锐,當產(chǎn)生點擊事件后,首先會傳遞給它见秽,此時調用它的dispatchTouchEvent
    方法愉烙,如果dispatchTouchEvent方法中的onInterceptTouchEvent(event)返回true,則表示這個ViewGroup要消耗當前事件,于是調用ViewGroupOnTouchEvent(event)方法解取。而如果onInterceptTouchEvent(event)返回的是false步责,則將該event交給這個當前View的子元素的dispatchTouchEvent去處理。如此遞歸禀苦,直到事件被最終處理掉蔓肯。

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

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

    1. 同一個事件序列是指從手指接觸到屏幕的那一刻起陪毡,到手指離開屏幕的那一刻結束米母。期間以Down為開始,中間含有數(shù)量不等(可以為0)的MOVE,最終則以UP結束毡琉。
    2. 正常情況下铁瞒,一個事件序列只能被一個View攔截且進行消耗。
    3. 某個View一旦決定攔截事件序列桅滋,那么這一個事件序列只能由它來處理(只要在這個view進行攔截之前沒有其他view對這個事件序列進行攔截)慧耍,并且它的onInterceptTouchEvent方法也不會再被調用。
    4. 某個View一旦開始處理事件序列丐谋,如果它不消耗ACTION_DOWN事件(OnTouchEvent返回false)芍碧,那么同一個事件序列中的其他事件都不會由它來處理,而是直接將其交由父元素去處理号俐。并且當前view是無法再次接收到該事件的泌豆。
    5. 如果View不消耗除了ACTION_DOWN之外的其他事件,那么這個點擊事件就會消失吏饿,并且父元素的OnTouchEvent方法也不會被調用踪危,同時蔬浙,當前View可以持續(xù)收到后續(xù)的事件,最終這些消失的點擊事件會交由Activity進行處理贞远。
    6. ViewGroup不攔截任何事件畴博。Android源碼中ViewGrouponInterceptTouchEvent方法默認返回false
    7. Android源碼中蓝仲,View并沒有onInterceptTouchEvent方法绎晃,一旦有點擊事件傳遞給它。那么它的OnTouchEvent方法就會被調用杂曲。
    8. viewOnTouchEvent默認會消耗該事件(默認返回true),除非它是不可點擊的(clickablelongclickable同時為false)庶艾。
    9. viewenable屬性不影響onTouchEvent的默認放回值。即便該viewdisable狀態(tài)的擎勘,但只要它的clickablelongClickable有一個為true,那么它的返回值就為true咱揍。
    10. onclick會發(fā)生的前提是當前View是可點擊的,并且它接收到了ACTION_DOWNACTION_UP事件棚饵。
    11. 事件傳遞過程是由外向內的煤裙,及事件總是先傳遞給父元素。然后再有父元素去分發(fā)給子元素噪漾。但通過requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的分發(fā)過程硼砰,但ACTION_DOWN事件除外。
  2. 從源碼去看事件分發(fā)機制:

    • Activity分發(fā)
      從上面我們知道欣硼,每個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分發(fā)
      我們可以看到Activity其實是將點擊事件交給了Window進行下一步處理, 但是Window類其實是一個抽象類, 它里面的superDispatchTouchEvent()方法是一個抽象方法.
      所以我們需要去它的唯一實現(xiàn)類PhoneWindow中去查看superDispatchTouchEvent()是如何實現(xiàn)的.

      //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分發(fā)
      那么梯澜,現(xiàn)在事件已經(jīng)傳遞到頂級View(一個ViewGroup)了,接下來又該是怎樣的呢渴析?邏輯思路如下:

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

      根據(jù)這個晚伙,我們先來看一下ViewGroup對點擊事件的分發(fā)過程,其主要體現(xiàn)在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事件到來時做重置狀態(tài)的操作藻烤,而在 resetTouchState方法中會對FLAG _DISALLOW _INTERCEPT進行重置绷雏,因此子View調用requestDisallowInterceptTouchEvent方法時并不能影響ViewGroupACTION _DOWN的影響。
      接著我們再看當ViewGroup不攔截事件的時候怖亭。事件會向下分發(fā)涎显,交由它的子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沒有播放動畫&&點擊事件落在了它的區(qū)域內
              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);
          }
      }
      

      從源碼中箭跳,我們可以發(fā)現(xiàn)它的過程如下:首先遍歷ViewGroup的所有子元素,然后判定子元素是否能夠接收到點擊事件(子元素是否在播動畫或者點擊事件的坐標是否落在子元素的區(qū)域內)潭千。如果某個子元素滿足這兩個條件谱姓,那么事件就會交由它來處理∨偾纾可以看到屉来,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蝶桶,那么我們就不需考慮事件在子元素是怎么派發(fā)的慨绳,那么mFirstTouchTarget就會被賦值,同時跳出for循環(huán)莫瞬。從源碼中抽取相關部分見下:

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

      有人說儡蔓,這段代碼并沒有對mFirstTouchTarget的賦值,因為它實際上出現(xiàn)在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);
      }
      

      注意這一段源碼的第三個參數(shù)childnull蹬铺,從前面的分析就可以知道尝哆,它會調用super.dispatchTouchEvent(event),很顯然甜攀,這里就從ViewGroup轉到了ViewdispatchTouchEvent(event)秋泄。
      在隨后我們對ViewdispatchTouchEvent(event)分析中我們會發(fā)現(xiàn), 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, 那么此時我們執(zhí)行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()方法中我們會執(zhí)行B的BB()方法.

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

    • View分發(fā)
      接下來我們回過頭繼續(xù)看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方法的優(yōu)先級高于onTouchEvent歧胁。

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

      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));
          }
          // 省略...
      }
      

      很顯然喊巍,不可用狀態(tài)下的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狀態(tài)。
      而當MOTION_UP事件發(fā)生時之剧,則觸發(fā)performClick()方法郭卫,如果View設置了onClickListener,那么performClick()方法內部會調用它的onClick方法

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


view的滑動沖突

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

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

  • 場景二:外部滑動和內部滑動方向一致钾军,比如ScrollView嵌套ListView鳄袍,或者是ScrollView嵌套自己。表現(xiàn)在要么只能有一層能夠滑動吏恭,要么兩者滑動起來顯得十分卡頓拗小。
    處理規(guī)則:從業(yè)務上尋找突破點,比如業(yè)務上有規(guī)定:當處于某種狀態(tài)時需要外部View處理用戶的操作樱哼,而處理另一種狀態(tài)時則讓內部View處理用戶的操作哀九。

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

滑動沖突的解決方式:

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

  • 外部攔截法:
    所謂外部攔截法是指點擊事件都先經(jīng)過父容器的攔截處理阅束,如果父容器需要此事件就攔截,如果不需要此事件就不攔截茄唐,這樣就可以解決滑動沖突的問題息裸,這個方法需要重寫父容器的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;
        }
    }
    
    以上兩種方式挽荠,是針對場景一而得出的通用的解決方法。對于場景二和場景三而言平绩,只需改變相關的滑動規(guī)則的邏輯即可圈匆。
    注意:因為內部攔截法的操作較為復雜,因此推薦采用外部攔截法來處理常見的滑動沖突捏雌。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末跃赚,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子性湿,更是在濱河造成了極大的恐慌纬傲,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肤频,死亡現(xiàn)場離奇詭異叹括,居然都是意外死亡,警方通過查閱死者的電腦和手機宵荒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門汁雷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來净嘀,“玉大人,你說我怎么就攤上這事侠讯⊥诓兀” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵厢漩,是天一觀的道長膜眠。 經(jīng)常有香客問我,道長溜嗜,這世上最難降的妖魔是什么宵膨? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮粱胜,結果婚禮上柄驻,老公的妹妹穿的比我還像新娘狐树。我一直安慰自己焙压,他們只是感情好,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布抑钟。 她就那樣靜靜地躺著涯曲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪在塔。 梳的紋絲不亂的頭發(fā)上幻件,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音蛔溃,去河邊找鬼绰沥。 笑死,一個胖子當著我的面吹牛贺待,可吹牛的內容都是我干的徽曲。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼麸塞,長吁一口氣:“原來是場噩夢啊……” “哼秃臣!你這毒婦竟也來了?” 一聲冷哼從身側響起哪工,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤奥此,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后雁比,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體稚虎,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年偎捎,在試婚紗的時候發(fā)現(xiàn)自己被綠了蠢终。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片非洲。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蜕径,靈堂內的尸體忽然破棺而出两踏,到底是詐尸還是另有隱情,我是刑警寧澤兜喻,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布梦染,位于F島的核電站,受9級特大地震影響朴皆,放射性物質發(fā)生泄漏帕识。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一遂铡、第九天 我趴在偏房一處隱蔽的房頂上張望肮疗。 院中可真熱鬧,春花似錦扒接、人聲如沸伪货。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽碱呼。三九已至,卻和暖如春宗侦,著一層夾襖步出監(jiān)牢的瞬間愚臀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工矾利, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留姑裂,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓男旗,卻偏偏與公主長得像舶斧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子剑肯,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容