Android開發藝術探索——第三章View事件體系讀書筆記

android開發藝術探索——第三章View的事件體系

3.1

View的基礎知識點

(一) View 和 ViewGroup

  Veiw 是Android中所有控件的基類。View是一種界面層的控件的抽象。
   ViewGroup也是繼承之View,翻譯爲控件組。ViewGroup內部包含了許多控件。所以View本身可以是單個控件也可以是由多個控件組成的一個控件。
   例:
      LinearLayout不但是一個View還是一個ViewGroup,而ViewGroup內部可以有子View的,這個子View同樣還可以是ViewGroup。

(二)View的位置參數

   View的位置由四個頂點來決定,分別對應於四個屬性:top,left,right,bottom。這四個參數都是相對於Veiw的父容器來說的,是相對座標。

(三)MotionEvent 和 TouchSlop

1.MotionEvent
    · ACTION_DOWN 手指剛接觸屏幕
    · ACTION_MOVE 手指在屏幕上移動
    · ACTION_UP 手指從屏幕上鬆開的一瞬間
    通過MotionEvent對象可以得到點擊事件發生的x 和 y座標。系統提供了兩組方法:
        getX/getY 和 getRawX/getRawY
    區別:
        getX/getY返回的是相對於當前View的左上角的x和y座標;
        getRawX/getRawY返回的是相對於屏幕的左上角的x和y的座標
2.TouchSolp
    系統所能識別出的被認爲是滑動的最小距離。也就是說,當手指在屏幕上滑動時,如果兩次滑動之間的距離小於這個常量,
    系統會認爲這不是一次滑動。需要注意的是,不同的
    設備這個常量值可能是不相同的。可通過如下的方式獲取這個常量值:
    ViewConfiguration.get(getContext()).getScaledTouchSlop()

(四)VelocityTracker、GestureDetector和Scroller

1.VelocityTracker
     速度跟蹤,用於追蹤手指在滑動過程中的速度,包括水平和垂直方向的速度。
     首先,在View的onTouchEvent方法中追蹤當前的點擊事件的速度:
       > 
        VelocityTracker velocityTracker = VelocityTracker.obtain();
        velocityTracler.addMovement(event);
     接着,
       >
        velocityTracker.computeCurrentVelocity(1000);
        int xVelocity= (int)velocityTracker.getXVelocity();
        int yVelocity= (int)velocityTracker.getYVelocity();
     這裏需要注意兩點:
         第一: 獲取速度之前必須先計算速度,即必須先調用computeCurrentVelocity(1000)之後是getXVelocity(),getYVelocity()
         第二: 這裏的速度是在指定時間內劃過的像素數
                例:將時間間隔設爲1s,在1s內,手指在水平方向從左向右滑動100像素,水平速度就是100;
                速度也可以爲負數。當手指從右向左滑過100像素,水平方向的速度就是負值。
                公式表示:
                         速度 = (終點位置-起始位置)/ 時間段
     最後,當不需要的時候,需要調用clear方法重置並回收內存。
       >
       velocityTracker.clear();
       velocityTracker.recycle();
2.GestureDetector
      手勢檢測,用於輔助檢測用戶單擊、滑動、長按和雙擊等行爲。(如果只是監聽滑動相關,建議在onTouchEvent;
      如果是雙擊,需要使用GestureDetector)
      首先,需要創建一個GestureDetector對象實現onGestureListener接口,
       這裏根據需要還可以實現OnDoubleTapListener 實現雙擊監聽
       >
         GestureDetector mGestureDetector = new GestureDetector(this);
         //解決長按屏幕後無法拖動的現象
         mGestureDetector.setIsLongpressEnabled(false);
      接着,接管目標的View的OnTouchEvent方法,在待監聽的View的OnTouchEvent中添加:
         >
            boolean consume = mGestureDetector.onTouchEvent(event);
            return consume;
      然後,可以有選擇地實現OnGestureListener 和 OnDoubleTapListener中的方法
3.Scroller
            Socroller本身無法讓View彈性滑動,需要和View的computeScroll配合使用
               >
                  Scroller scroller = new Scroller(this);

                  //緩慢滑到指定位置
                  private void smoothScrollTo(int destX, int destY){
                       int scrollX = getScrollX();
                       int delta= destX - getScrollX();
                       //1000ms內滑向destX,效果就是慢慢滑動
                       mScroller.startScroll(scrollX,0 , delta,0 , 1000);

                       invalidata();
                  }
                  @Override
                  public void computeScroll(){
                      if(mScroller.computeScrollOffset()){
                          scrollTo(mScroller.getCurrX(),mScroller.getCurrY());

                          postiInvalidate();
                      }
                 }

3.2

View的滑動

 一般View的滑動通過三種方式實現:
     第一種通過View本身的提供的scrollTo/scrollBy方法實現
     第二種通過動畫給View施加平移效果實現滑動
     第三種通過改變View的LayoutParams使得View重新佈局從而實現滑動

(一)使用scrollTo/scrollBy

    public void scrollTo(int x,int y){
           if( mScrollX != x && mScrollY != null){
                int oldX = mScrollX;
                int oldY = mScrollY;

                mScrollX = x;
                mScrollY = y;

                invalidateParentCaches();
                onScrollChanged(mScrollX,,mScrollY,oldX, oldY);
                if( !awakenScrollBars()){
                      postInvalidateOnAnimation();
                }
           }
    }
    /*************************************/
    public void scrollBy(int x , int y){
           scrollTo(mScrollX + x, mScrollY + y);
    }
scrollBy()調用scrollTo()方法。scrollTBy()基於當前位置的相對滑動,scrollTo()實現了基於
所傳遞參數的絕對滑動。

* 注意:*
使用scrollTo()和scrollBy()來實現View的滑動,只能將View上的內容進行移動,
並不能將View本身進行移動。
無論怎麼滑動都不可能將當前的View滑動到附近的View所在區域。

(二)使用動畫

 使用動畫主要是操作View的transklationX和transklationY屬性
 動畫代碼,此動畫在100ms內將一個View從原始位置向右下角移動100個像素。
 <set xmlns:android="http://schemas.android.com/apk/res/android"
      android:fillAfter="true"
      android:zAdjustment="normal">
      <translate
         adnroid:duration = "100"
         android:fromXDelta = "0"
         android:fromYDelta = "0"
         android:interpolator = "@android:anim/linear_interpolator"
         android:toXDelta = "100"
         android:toYDelta = "100" />
 </set>
 屬性動畫代碼
 ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
 View的動畫是對View的影像做操作,並不能真正改變View的位置參數,包括寬/高,
 如果希望保留動畫後的狀態,將
 fillAfter設置爲true;

(三)改變參數佈局

    MarginLayoutParams params = (MarginLayoutParams)mButtion1.getLayoutParams();
    params.width += 100;
    params.leftMargin += 100;0
    mButtion1.requestLayout();
    //或者mButton1.setLayoutParams(params)

(四)對比

-scrollTo/scrollBy : 操作簡單,適合對View內容的滑動
-動畫 : 操作簡單,適合用於沒有交互的View和實現複雜的動畫效果
-改變佈局參數 : 操作稍微複雜了點,適用於有交互的View

3.3彈性滑動

(一)使用Scroller

    Scroller源碼:
    Scroller scroller = new Scroller(mContext);

    //緩慢滾動到指定位置
    private void smoothScrollTo(int destX,int destY){
      int scrollX = getScrollX();
      int deltaX = destX - srollX;
      //1000ms內滑向destX,效果就是慢慢滑動
      mScroller.startScroll(scrollX,0,deltaX,1,1000);
      invalidate();
    }

    @Override
    public void computeScroll(){
      if(mScroller.computeScrollOffset()){
          scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
          postInvalidate();
      }
    }

3.4

View的事件分發

(一)點擊事件的傳遞規則

* public boolean dispatchTouchEvent(MotionEvent ev) *
如果事件能夠傳遞給當前View,此方法一定會被調用,返回結果受當前View的oTouchEvent 和
下級View的dispatchTouchEvent 方法的影響,表示是否消耗當前事件

* public boolean onInterceptTouchEvent(MotionEvent event) *
在上述內部調用,用來判斷是否攔截某個事件,如果當前View攔截某個事件,
那麼同一事件序列當中,此方法不會再調用,返回結果表示是否攔截當前事件

* public boolean onTouchEvent(MotionEvent event) *
在dispatchTouchEvent 方法中調用,用來處理點擊事件,返回結果表示是否消耗當前事件,
如果不消耗,則在同一個事件序列中,當前View無法再次接收到事件

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

傳遞規則:
對於一個根ViewGroup來說,點擊事件產生後,首先會傳遞給它,這時它的dispatchTouchEvent就會被調用,如果這個
ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當前事件,接着事件就會交給這個ViewGroup處理,即
它的onTouchEvent方法就會被調用;如果這個ViewGroup的onInterceptTouchEvent方法返回false就表示不攔截當前事件,
這時當前事件就會繼續傳遞給它的子元素,接着子元素的dispatchTouchEvent方法就會調用,如此反覆,直到事件被處理。

當一個View需要處理事件時,如果它設置了onTouchListener,onTouchListener中的onTouch方法會被回調。這件事如何處理還要
看onTouch的返回值,如果返回false。則當前View的onTouch方法會被調用;如果返回true,那麼onTouchEvent方法將不會被調用。
由此可見,給View設置onTouchListener,其優先級比onTouchEvent要高。在onTouch方法中,如果設置的有onClickListener,那麼
onClick方法會被調用。
點擊事件的傳遞順序: activity --> window --> View

事件分佈的源碼分析

(一)Activity對點擊事件的分佈 過程

  點擊事件用MotionEvent來表示,當一個點擊操作發生時,事件最先傳遞給當前的Activicity,由Activity的disaptchTouchEvent
來進行事件的派發,具體的工作是由Activcity內部的Window來完成的。Window會將事件傳遞給decor view, decor view一般就是當前
界面的底層容器,通過Activity.getWindow.getDecorView()獲得

源碼:Activity#dispatchTouchEvent   
public boolean dispatchTouchEvent(MotionEvent ev){
  if(ev.getAction() == MotionEvent.ACTION_DOWN){
        onUserInteraction();
  }
  if(getWindow().superDispatchTouchEvent (ev)){
     return true;
  }
  return onTouchEvent(ev);
}

  事件開始交給Acitivity所屬的Winow進行分佈,如果返回true,整個事件循環就結束,返回false
意味着事件沒人處理,所有View的onTouchEvent都返回了false,Activity的onTouchEvent會調用。Window
將事件傳遞給ViewGroup的。
  Window是個抽象類,Window的dispatchTouchEvent是個抽象方法,實現類是PhoneWindow。

 源碼:PhoneWindow#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent ev){
     return mDecor.superDispatchTouchEvent(ev);
}

  PhoneWindow將事件直接傳遞給了DecorView。
  通過((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)
獲得Activity所設置的View,這個mDecor就是getWindow().getDecorView()返回的View,通過setContentView
設置的View就是mDecor的子View。目前事件傳遞到了這裏。

(二)頂級View 對點擊事件的分發過程

View事件分發回顧:
    點擊事件達到頂級View(一般是一個ViewGroup)以後,會調用ViewGroup的dispatchTouchEvent方法;
此時,
      · 如果頂級ViewGroup攔截事件onInterceptTouchEvent返回 true,則事件由ViewGroup本身處理。
        這時如果ViewGroup的mOnTouchListener被設置,則OnTouch被調用,否則OnTouchEvent會被調
        用。也就是說如果能提供的話,onTouch會屏蔽掉onToucheEvent。在onTouchEvent中,如果設
        置了mOnClickListener,則onClick會被調用。

      · 如果頂級ViewGroup不攔截事件,則事件會傳遞到它所在的點擊事件鏈上的子View,這個時候子
        View的dispatchTouchEvent會被調用。

(三)View對點擊事件的處理過程

* 分發過程 和 處理過程第一遍沒看懂*
  View的setClickable 和 setOnLongClickListener會自動將View的 CLICKABLE 和 LONG_CLICKABLE屬性
設置爲true。

3.5

View的滑動衝突

(一)常見的滑動衝突場景

常見的滑動衝突場景,簡單分爲三種:

  • 場景1——外部滑動方向和內部滑動方向不一致
  • 場景2——外部滑動方向和內部滑動方向一致
  • 場景3——上面兩種情況的嵌套

(二)滑動衝突的解決方式

* 針對場景1的解決方式 *

1.外部攔截

  所謂外部攔截是指事情都經過父容器的攔截處理,如果父容器需要此事件就攔截;
如果不需要就不攔截。外部攔截法需要重寫父容器的onInterceptTouchEvent方法,在
內部做相應的攔截處理。

 僞碼如下:
public boolean onInterceptTouchEvent(MotionEvent event){
       boolean intercepted = false;
       int x = (int) event.getX();
       int y = (int) event.getY();
       switch(event.getAction()){
             case MotionEvent.ACTION_MOVE:
                  intercepted = true;
             break;
             case MotionEvent.Move:
                  if(父容器需要當前點擊事件){
                     intercepted = true;
                  }else{
                     intercepted = false; 
                  }
             break;
             case MotionEvent.ACTION_UP:
                  intercepted = false;
             break;
      }
      mLastXIntercept = x;
      mLastYIntercept = y;
      return intercepted;
}

  在onInterceptTouchEvent方法中,首先是在ACTION_DOWN,父容器必須返回false,即不攔截
ACTION_DOWN,ACTION_MOVE和ACTION_UP事件都會直接交由父容器處理,這個時候事件沒法再傳遞
子元素了;其次是ACTION_MOVE事件,這個事件可以根據需求決定是否需要攔截,如果父容器需要
攔截就返回true,否則返回false;最後是ACTION_UP事件,必須要返回false,因爲ACTION_UP時事
件本身沒有啥意義。

2.內部攔截

  內部攔截法指父容器不攔截任何事件,所有的事件都傳給子元素,如果子元素需要此事件就
直接消耗掉,若不需要,就交由父容器進行處理,這種方法和Andorid中的事件分發機制不一致,
需要配合requestDisallowInterceptTouchEvent方法才能正常工作m,使用起來外部攔截稍顯複雜。

  僞代碼如下:
public boolean dispatchTouchEvent(MotionEvent event){
       int x = (int) event.getX();
       int y = (int) event.getY();
       switch(event.getAction()){
          case MotionEvent.ACTION_DOWN:
               parent.requestDisallowInterceptTouchEvent(true);
          break;
          case MotionEvent.ACTION_MOVE:
               int deltaX = x - mLastX;
               int deltaY = y - mLastY;
               if(父容器需要此類點擊事件){
                   parent.requestDisallowInterceptTopuchEvent(false);
               }
          break;
          case MotionEvent.ACTION_UP:
               break;
          break;
       }
       mLastX = x ;
       mLastY = y ;
       return super.dispatchTouchEvent(event);
}

  當面對不同的滑動策略時只需要修改裏面的條件即可,其他不需要做改動也不能動。除了
子元素需要做處理以外,父元素也要默認攔截了 ACTION_DOWN 以外的其他事件,這樣當子元素調用parent.requestDisalowInterceptTouchEvent(false)方法時,父元素才能攔截所需的
事件。
  爲什麼父容器不能攔截ACTION_DOWN 事件呢?那是因爲ACTION_DOWN 事件並不受FLAG_DISALLOW_INTERCEPT這個標記的控制,所以一旦父容器攔截ACTION_DOWN事件,那麼所有的事件都無法傳遞到子元素中去,這樣內部攔截就起不了作用,所以父元素做下面的改動。

public boolean onInterceptTouchEvent(MotionEvent event){
       int action = event.getAction();
       if(action == MotionEvent.ACTION_DOWN){
          return false;
       }else{
           return true;
       }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章