View的繪製和事件分發

語言: CN / TW / HK

本文已參與「新人創作禮」活動,一起開啟掘金創作之路。

View 及 ViewGroup 繪製過程概述

  1. measure

    對單個View控制元件來說即 measure 完成獲取 View 本身的寬高;對 ViewGroup 來說除了完成自身整體的 measure,還要遍歷完成所有子View的 measure。onMeasure方法中應完成:①得到各個子控制元件的寬/高測量值給②使用;②得到自定義根佈局的整體寬高,並根據當前佈局的測量模式將對應寬/高測量值通過 setMeasuredDimension(widthSize, heightSize); 交給系統。注意:對這個測量值有影響的因素包括

    • (子)View 本身的具體寬高

    • (子)View 本身的 android:layout_width="wrap_content | match_parent | 具體dp值" android:layout_height="wrap_content | match_parent | 具體dp值"

      及其父容器的

      android:layout_width="wrap_content | match_parent | 具體dp值" android:layout_height="wrap_content | match_parent | 具體dp值" 這些都封裝在 LayoutParams

      針對第二條舉個例子:假設父容器 LinearLayout 的 layout_width 是 match_parent,子 View(Button) 的 layout_width 也是 match_parent,那麼該 Button 的測量寬值就是 LinearLayout 的寬值;但如果子 Button 的 layout_width 是 wrap_content 或者具體 dp值時,那麼該 Button 的測量寬值就應該是其本身的具體寬值

    上述總結的兩個因素,系統幫我們將其一起封裝在了 MeasureSpec(一個32位 int 值)中,MeasureSpec 顧名思義即測量規格,系統將 View 本身的具體寬高 Size 封裝在 MeasureSpec 的後30位中,將 LayoutParams 作為測量模式封裝在 MeasureSpec 的前兩位中,下面總結 MeasureSpec 中的測量模式種類:

    |MeasureSpec 的前兩位標識|種類|含義|對應的 LayoutParams| |:--|:--|:--|:--| | 00|UNSPECIFIED|父容器對 View 沒有大小限制|一般用於系統內部| | 01|EXACTLY|父容器指定 View 所需大小,View 忽略自身大小|match_parent 或具體數值| | 10|AT_MOST|父容器指定可用大小,View 的大小不能超過該值,具體值看 View 本身大小|wrap_content|

  2. layout

    layout 是 ViewGroup 用來確定子元素的位置,當 ViewGroup 位置確定,onLayout方法中會遍歷所有子元素並呼叫其 layout方法,而在 layout方法中 onLayout方法又會被呼叫。layout方法確定 View本身的位置,onLayout方法確定所有子元素的位置

    • 示例:簡單自定義的ViewGroup中確定所有子元素位置

      ``` @Override protected void onLayout(boolean changed, int l, int t, int r, int b) {

      int left = 0;
      ......
      
       for (int i = 0; i < getChildCount(); i++) {
          ......
          // 第一個子元素位置(0,0)(子元素寬,第一個子元素高)
          // 第二個子元素位置(第一個子元素寬,0)(第一、二子元素寬的和,第二個子元素高)
          // 後面依次類推可確定每個子元素的位置
          childView.layout(left, 0, left + childWidth, childHeight);
      
          left += childWidth;
          ......
      }
      

      } ```

    • getMeasureWidth()&getWidth() -- getMeasureHeight()&getHeight()

      • getMeasureWidth() 和 getMeasureHeight() 是View的 onMeasure() 過後得到的測量寬高

      • getWidth() 和 getHeight() 是View的 layout() 後得到的最終寬高

  3. draw

    使用 @Override protected void onDraw(Canvas canvas) 中的 canvas 和 Paint物件等繪製圖形即可

View 的事件體系一

  1. MotionEvent

    • 手指接觸屏幕後會產生一系列事件,主要的事件型別如下

      • ACTION_DOWN:手指剛接觸螢幕

      • ACTION_MOVE:手指在螢幕上移動

      • ACTION_UP:手指離開螢幕的瞬間

    • 通過 MotionEvent 物件可得到點選事件發生時的 x/y 座標

      • getX/getY:相對當前 View 左上角的 x/y 座標

      • getRawX/getRawY:相對手機螢幕左上角的 x/y 座標

  2. TouchSlop

    即系統所能識別的最小滑動距離,若滑動距離小於該常量,系統不認為此時進行的是滑動操作

  3. VelocityTracker

    onTouchEvent方法 中追蹤並獲取滑動的速度,包括水平和豎直方向的速度

    ``` // 獲取VelocityTracker例項 VelocityTracker mVelocityTracker = VelocityTracker.obtain();

    @Override public boolean onTouchEvent(MotionEvent event) { // 追蹤事件 mVelocityTracker.addMovement(event);

    // 計算並獲取當前滑動的水平/豎直速度
    mVelocityTracker.computeCurrentVelocity(1000); // 每1000ms滑動的畫素值
    float xVelocity = mVelocityTracker.getXVelocity();
    float yVelocity = mVelocityTracker.getYVelocity();
    ......
    

    } ```


    當不再需要使用時,應重置並回收記憶體 mVelocityTracker.clear(); mVelocityTracker.recycle();

  4. GestureDetector

    輔助檢測使用者的單擊、滑動、長按、雙擊等行為

    • 完整示例:左右滑動切換 Activity

      ``` public abstract class BaseSetupActivity extends Activity {

      private GestureDetector mGestureDetector;
      
      @Override protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
      
          // 例項化手勢識別器,並新增滑動監聽
          mGestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
      
              /** 快速滑動。e1: 起點座標 e2: 終點座標 velocityX: 水平滑動速度 velocityY:豎直滑動速度 */
              @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
      
                  if (Math.abs(e2.getRawY() - e1.getRawY()) > 100) {
                      Log.d("catfaceooo", "豎直方向滑動範圍太大");
                      return true;
                  }
      
                  if (Math.abs(velocityX) < 100) {
                      Log.d("catfaceooo", "水平滑動速度太慢");
                      return true;
                  }
      
                  // 判斷向左劃還是向右劃
                  if (e2.getRawX() - e1.getRawX() > 200) { // 向右劃,上一頁
                      showPrevious();
                      return true;
                  }
      
                  if (e1.getRawX() - e2.getRawX() > 200) { // 向左劃,下一頁
                      showNext();
                      return true;
                  }
      
                  return super.onFling(e1, e2, velocityX, velocityY);
              }
          });
      }
      
      /** 按鈕點選上一頁 */
      public void previous(View view) {
          showPrevious();
      }
      
      /** 按鈕點選下一頁 */
      public void next(View view) {
          showNext();
      }
      
      // 暴露給需要滑動切換的Activity
      public abstract void showPrevious();
      public abstract void showNext();
      
      /** 當前介面被觸控時,走此方法 */
      @Override
      public boolean onTouchEvent(MotionEvent event) {
          mGestureDetector.onTouchEvent(event);// 將事件委託給手勢識別器處理
          return super.onTouchEvent(event);
      }
      

      } ```

    • GestureDetector 的監聽介面及方法的部分總結如下

      1. OnGestureListener

        • onDown:手指觸屏瞬間

        • onShowPress:手指觸屏尚未鬆開或滑動

        • onSingleTapUp:單擊

        • onScroll:滑動

        • onLongPress:長按

        • onFling:手指觸屏,快速滑動後鬆開

      2. OnDoubleTapListener

        • onDoubleTap:雙擊,與 onSingleTapConfirmed 不可共存

        • onSingleTapConfirmed:嚴格單擊,不是雙擊中的一次單擊

        • onDoubleTapEvent:雙擊行為

    • 建議監聽滑動在 onTouchEvent方法中實現,監聽雙擊則使用 GestureDetector

View 的事件體系二 - - 滑動

  1. 通過View本身的scrollTo/scrollBy方法實現

    • scrollBy(基於當前位置的滑動)也是呼叫scrollTo(基於所傳遞引數的絕對滑動)方法

    • 滑動過程中View內部的屬性mScrollX和mScrollY可通過getScrollX和getScrollY方法獲取

    • 本方式只能移動View的內容,不能移動View本身

  2. 通過動畫實現

    • 主要操作View的translationX和translationY兩個屬性來對View進行平移

    • 明白補間動畫(不能真正改變View的位置引數,僅對View的影像做操作)和屬性動畫(√)的區別

  3. 通過改變View的LayoutParams使View重新佈局實現

    MarginLayoutParams params = (MarginLayoutParams) bt_test.getLayoutParams(); // 下面兩行的結果就是:向右平移100畫素 params.width += 100; params.leftMargin += 50; bt_test.setLayoutParams(params);

    上述三個方法的使用場景

    | 方法 | 使用場景 | | :--| :-- | | 1.scrollTo/scrollBy|對View內容的滑動| | 2.動畫|無互動的View和實現複雜的動畫效果| | 3.LayoutParams|有互動的View|

  4. 彈性滑動

    1. Scroller的工作機制

      • Scroller本身不能實現View的滑動,需要配合View的 computeScroll方法(自行實現)才能完成彈性滑動的效果,不斷的讓View重繪,而每次重繪的距離起始時間會有一個時間間隔,通過這個時間間隔Scrollerr就可以得到View的滑動位置,然後通過scrollTo方法來完成View的滑動。即View的每次重繪都會導致View進行小幅滑動,多次小幅滑動就組成了彈性滑動

      • 當View重繪後會在draw方法中呼叫computeScroll,而computeScroll又會向Scroller獲取當前的scrollX和scrollY;然後通過scrollTo方法實現滑動;接著又呼叫postInvalidate方法進行第二次重繪,同樣會呼叫computeScroll方法;然後繼續向Scroller獲取當前的scrollX和scrollY,然後通過scrollTo方法滑動到新的位置,如此反覆至滑動過程結束

    2. 動畫onAnimationUpdate方法

      在該方法中呼叫getAnimationFraction()方法獲取動畫幀片段,在每一小段時間中呼叫View的scrollTo方法一小段一小段的移動View

    3. 延時策略

      切割若干個時間片段,在Handler中呼叫View的scrollTo方法一小段一小段的移動View

事件分發 - 分析物件為 MotionEvent

  1. @Override public boolean dispatchTouchEvent(MotionEvent ev)

    進行事件的分發。若事件能夠傳遞給當前 View,則此方法一定會被掉用,返回結果受當前 View 的 onTouchEvent 方法和下級 View 的 dispatchTouchEvent 方法的影響,表示是否消耗當前事件

  2. @Override public boolean onInterceptTouchEvent(MotionEvent event)

    在上述方法內部掉用,用來判斷是否攔截某個事件,若當前 View 攔截了某個事件,則在同一事件序列(即從手指觸屏瞬間至手指離開螢幕瞬間,期間產生的一系列事件,即down-move-up過程)中,此方法不會被再次呼叫,返回結果表示是否攔截當前事件 預設返回 false 即不攔截事件,View 沒有該方法,一旦事件傳遞給它,則它的 onTouchEvent方法就會被呼叫

  3. @Override public boolean onTouchEvent(MotionEvent event)

    dispatchTouchEvent 方法中呼叫,用來處理點選事件,返回結果表示是否消耗當前事件,如不消耗,則在同一事件序列中,當前 View 無法再次接收到事件 可點選的 View 的 onTouchEvent方法預設返回 true 即消耗事件,且 View 的 enable/disable 屬性不影響該方法的預設返回值

  4. 上述三個方法的關係如下偽程式碼表示

    public boolean dispatchTouchEvent(MotionEvent event) { boolean consume = false; if(onInterceptTouchEvent(event)) { consume = onTouchEvent(event); } else { consume = child.dispathchTouchEvent(event); } return consume; }  根據虛擬碼簡要說明事件的傳遞規則:對於一個根 ViewGroup,事件產生後,首先會傳遞給它,此時它的 dispatchTouchEvent方法就會被呼叫,若它的 onInterceptTouchEvent方法返回 true 攔截當前事件,該事件就會在它的 onTouchEvent方法中處理;若它的 onInterceptTouchEvent方法返回 false不攔截當前事件,該事件就會繼續傳遞給它的子元素,接著子元素的 dispatchTouchEvent方法就會被呼叫,如此反覆直到事件被最終處理  補:當一個 View 需要處理事件時,若其設定了 OnTouchListener,則 OnTouchListener 中的 onnTouch方法會被呼叫,若返回 false,則當前 View 的 onTouchEvent方法才會被呼叫。在 onTouchEvent方法中,若設定 OnClickListener,則其 onClick方法會被呼叫,即 OnClickListener 優先順序處於事件傳遞的最末端

  5. requestDisallowInterceptTouchEvent

    事件傳遞過程時由外向內的,即由父元素分發給子元素。通過 requestDisallowInterceptTouchEvent方法可在子元素中干預父元素的事件分發過程,但 ACTION_DOWN事件除外

事件傳遞機制的總結

  1. 同一事件序列是指從手指觸屏瞬間至離開螢幕瞬間,過程中產生的一系列事件,即從down事件開始,up事件結束,中間包含若干move事件

  2. 正常情況下,一個事件序列只能被一個View攔截且消耗。因為一旦一個元素攔截了某個事件,那麼同一事件序列內的所有事件都會直接交給其處理,因此同一事件序列中的事件不能分別由兩個View同時處理。通過特殊手段,如一個View可將本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理

  3. 某個View一旦攔截,那麼同一事件序列都只能由它來處理,且其onInterceptTouchEvent方法不會再被呼叫

  4. 某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件,那麼同一事件序列中的其他事件都不會再交給它來處理,且事件將重新交個它的父元素處理,即父元素的onTouchEvent方法會被呼叫

  5. 若View不消耗ACTION_DOWN以外的其他事件,那麼這個點選事件會消失,此時父元素的onTouchEvent並不會被呼叫,且當前View可持續接收到後續的事件,最終這些消失的點選事件會傳遞給Activity處理

  6. ViewGroup預設不攔截任何事件,即ViewGroup的onInterceptTouchEvent方法預設返回false

  7. View沒有onInterceptTouchEvent方法,若有事件傳遞給它,則其onTouchEvent方法會被呼叫

  8. View的onTouchEvent預設會消耗事件,除非它是不可點選的(clickable,longClickable == false)。View的longClickable預設都為false

  9. View的enable屬性不影響onTouchEvent的預設返回值,即使View是disable狀態,只要其clickable或longClickable有一個為true,則其onTouchEvent就返回true

  10. onClick會發生的前提是當前View是可點選的,且其收到了down和up的事件

  11. 事件傳遞過程是由外向內的,即事件總是先傳遞給父元素,再由父元素分發給子元素。但通過requestDisallowInterceptTouchEvent方法可在子元素中干預父元素的事件分發過程,但ACTION_DOWN事件除外