Android開發藝術探索 - 第4章 View的工作原理

1.ViewRoot和DecorView

ViewRoot對應ViewRootImpl,實現了DecorView和WindowManager之間的交互。
View的繪製流程從ViewRoot#performTraversals開始,經過measure、layout、draw最終將一個View繪製出來:

例,measur過程:performMeasure->measure->onMeasure->子View的measure。
measure決定了View的寬高,measure完成後可以通過getMeasureWidth/getMeasureHeight獲取測量後的寬高;layout決定了View四個頂點的座標和實際View的寬高,可調用getLeft/getTop/getRight/getBottom/getWidth/getHeight獲取對應屬性;draw決定了View的顯示,draw完成後View的內容才顯示在屏幕上。
Activity中通過setContentView設置的view位於DecorView的content部分,可以通過android.R.id.content索引到該View的父容器,然後通過getChildAt(0)定位到該View:

2.MeasureSpec

  1. 用於parent向child傳遞layout要求。真正傳遞的實際上是一個32位int存儲,高2位代表mode,低30位代表size,MeasureSpec只是一個工具類,幫助拼裝和拆解這個int。
    mode:
    • UNSPECIFIED
      parent不對child強加任何限制。child想要多大就多大。
    • EXACTLY
      parent已經決定了child準確的大小,child要依據這個大小。
    • AT_MOST
      child可以想多大就多大,但是被指定了一個上限。
  2. 與LayoutParams的關係
    對於一個View,可以設置LayoutParams來指定寬高,系統會綜合該LayoutParams和parent施加的MeasureSpec,得出最後應用於該View的MeasureSpec;而對於DecorView,因爲其沒有parent,所以取而代之的是Window的size,結合自己的LayoutParams得出最後的MeasureSpec。MeasureSpec一旦確定,onMeasure中就可以確定View的寬高。
    • DecorView的MeasureSpec計算過程:
      在ViewRootImpl的measureHierarchy中,計算了DecorView的MeasureSpec。desiredWindow*爲window的size:
      childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
      childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
      performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
      
      getRootMeasureSpec中根據window size和DecorView的LayoutParams計算出MeasureSpec。規則很簡單,如果是MATCH_PARENT或者固定的值,則spec mode爲EXACTLY,同時size設置爲相應的值;如果是WRAP_CONTENT,則spec mode爲AT_MOST,size爲window size:
      private static int getRootMeasureSpec(int windowSize, int rootDimension) {
          int measureSpec;
          switch (rootDimension) {
              case ViewGroup.LayoutParams.MATCH_PARENT:
                  // Window can't resize. Force root view to be windowSize.
                  measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
                  break;
              case ViewGroup.LayoutParams.WRAP_CONTENT:
                  // Window can resize. Set max size for root view.
                  measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
                  break;
              default:
                  // Window wants to be an exact size. Force root view to be that size.
                  measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
                  break;
          }
          return measureSpec;
      }
      
    • 普通View的MeasureSpec計算過程:
      以ViewGroup的measureChildWithMargins爲例,在該方法中會計算child的MeasureSpec。計算完成後,會直接對該view進行measure。計算時也會考慮parent的padding,child的margin:
      protected void measureChildWithMargins(View child,
              int parentWidthMeasureSpec, int widthUsed,
              int parentHeightMeasureSpec, int heightUsed) {
          final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
      
          final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                  mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                          + widthUsed, lp.width);
          final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                  mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                          + heightUsed, lp.height);
      
          child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      }
      
      具體的計算過程在getChildMeasureSpec中進行:
      • child指定確定的size,則遵從child的這個size設置。
      • child指定match_parent,如果parent表示可以exactly,則其size爲parent size;如果parent表示atmost,即其size也不確定,則其atmost爲parent size。
      • child指定wrap_content,則此時size由child自己決定,所以只限制其atmost爲parent size。

3.View的渲染流程

measure確定測量寬高->layout確定最終寬高和四個頂點位置->draw繪製到屏幕上

  1. measure
    • View的measure
      View的measure過程由其measure方法執行,其中會調用onMeasure,具體的計算在onMeasure中進行。measure用final修飾,所以只有onMeasure可以而且必須被子類複寫。onMeasure的默認實現,是通過setMeasuredDimension(onMeasure中一定要調用該方法設置measure出的值)設置測量值爲view默認的size:
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                  getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
      }
      
      getSuggestedMinimumWidth中會根據android:minWidth和是否設置了background得出minwidth。getSuggestedMinimumHeight原理一樣:
      protected int getSuggestedMinimumHeight() {
          return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
      }
      protected int getSuggestedMinimumWidth() {
          return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
      }
      
      默認的size在getDefaultSize中計算。對於parent的spec mode爲UNSPECIFIED的情況,最終的size即爲minwidth;在AT_MOST和EXACTLY的情況下默認的size就是parent中指定的size:
      public static int getDefaultSize(int size, int measureSpec) {
          int result = size;
          int specMode = MeasureSpec.getMode(measureSpec);
          int specSize = MeasureSpec.getSize(measureSpec);
      
          switch (specMode) {
          case MeasureSpec.UNSPECIFIED:
              result = size;
              break;
          case MeasureSpec.AT_MOST:
          case MeasureSpec.EXACTLY:
              result = specSize;
              break;
          }
          return result;
      }
      
      所以,onMeasure的默認實現都會將measure size設置爲parent size。對於child使用wrap_content的情況,這通常不是符合預期的設置。所以在自定義View的時候,需要重寫onMeasure方法,將View的measure size設置爲預期的默認值,一般是該View的默認最小值。(這也是爲什麼一定要重寫onMeasure)
      具體的處理方式,在onMeasure中,針對AT_MOST的情況,將對應的size(width或者height)設置爲默認最小值。因爲在ViewGroup的getChildMeasureSpec方法中,針對child爲wrap_contentchild爲match_parent+parent爲wrap_content這兩種情況,最終的spec mode都會是AT_MOST,即針對無法由parent決定child的情況,最終都會是AT_MOST。
    • ViewGroup的measure
      ViewGroup除了完成自身的measure之外,還要遍歷子View去執行其measure方法。因爲ViewGroup不同的派生類具有不同佈局特性,所以測量方式也不同,故沒有提供默認的onMeasure方法。但是ViewGroup中提供了簡單的measure其child的方法,提供給其派生類使用(在其onMeasure中調用);複雜的情況下,其派生類一般是自己實現。
      protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
          final int size = mChildrenCount;
          final View[] children = mChildren;
          for (int i = 0; i < size; ++i) {
              final View child = children[i];
              if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                  measureChild(child, widthMeasureSpec, heightMeasureSpec);
              }
          }
      }
      
      protected void measureChild(View child, int parentWidthMeasureSpec,
              int parentHeightMeasureSpec) {
          final LayoutParams lp = child.getLayoutParams();
      
          final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                  mPaddingLeft + mPaddingRight, lp.width);
          final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                  mPaddingTop + mPaddingBottom, lp.height);
      
          child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      }
      
      protected void measureChildWithMargins(View child,
              int parentWidthMeasureSpec, int widthUsed,
              int parentHeightMeasureSpec, int heightUsed) {
          ...
      }
      
    • 獲取View的寬高的tips:
      • 因爲系統可能多次measure之後,才能確定最終的寬高,所以measure之後的measureWidth和measureHeight可能是不準確的,這個時候就要在onLayout之後去獲取寬高。
      • 在Activity的生命週期回調中無法直接獲取view的寬高,因爲View的渲染過程和其聲明週期回調不是同步執行的,可以通過如下方法:
        • Activity/View#onWindowFocusChanged
          該方法會在Activity的窗口獲得和失去焦點的時候被調用;伴隨着焦點的變化,該方法會被調用多次:
          @Override
          public void onWindowFocusChanged(boolean hasFocus) {
              super.onWindowFocusChanged(hasFocus);
              
              if (hasFocus) {
                  int width = view.getMeasuredWidth();
                  int height = view.getMeasuredHeight();
              }
          }
          
        • view.post(Runnable)
          當View初始化完畢之後,Looper就開始執行各個post進去的Runnable:
          @Override
          protected void onStart() {
              super.onStart();
              
              view.post(new Runnable() {
                  @Override
                  public void run() {
                      int width = view.getMeasuredWidth();
                      int height = view.getMeasuredHeight();
                  }
              });
          }
          
        • ViewTreeObserver
          如果一個View的view tree的layout狀態或者view的可見性發生了變化,onGlobalLayout就會被回調;伴隨着view tree的變化,該方法會被調用多次:
          ViewTreeObserver observer = view.getViewTreeObserver();
          observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
              @Override
              public void onGlobalLayout() {
                  view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
          
                  int width = view.getMeasuredWidth();
                  int height = view.getMeasuredHeight();
              }
          });
          
        • view.measure
          手動調用view.measure去測量,然後得到寬高。對於該View的LayoutParams爲match_parent的情況,無法使用該方法,因爲此時parent的MeasureSpec是不確定的:
          // dp/px
          int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
          int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
          view.measure(widthMeasureSpec, heightMeasureSpec);
          
          // wrap_content
          int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
          int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 < 30) - 1, MeasureSpec.AT_MOST);
          view.measure(widthMeasureSpec, heightMeasureSpec);
          
  2. layout
    該步驟用來確定View的位置,依據就是measure之後存儲的measure值。由View的layout方法處理:首先會通過setFrame設置自身的l/t/r/b(位置/寬高被確定)然後調用onLayout,在onLayout中需要遍歷其所有的子View,計算其layout數據,然後調用其layout方法,直至所有View都layout完成。所以,對於ViewGroup一定要實現onLayout方法。
        public void layout(int l, int t, int r, int b) {
            if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
                onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
    
            int oldL = mLeft;
            int oldT = mTop;
            int oldB = mBottom;
            int oldR = mRight;
    
            boolean changed = isLayoutModeOptical(mParent) ?
                    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                onLayout(changed, l, t, r, b);
    
    ViewGroup和View都沒有onLayout的默認實現,因爲其實現也與具體的佈局有關。
    以LinearLayout的layout過程爲例來看。首先LinearLayout的layout的起點也是其layout方法,被parent調用之後,設置了l/t/r/b;之後調用自己的onLayout向子View發起layout。根據佈局方式的不同,水平和垂直的layout也不同:
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }
    
    對於垂直的情況:childTop會隨着child一個個的layout逐漸增大,其表現就是後面child會被放置在更下面;拿到child的measureWidth和measureHeight之後,調用setChildFrame將layout工作傳遞給該child。
    void layoutVertical(int left, int top, int right, int bottom) {
        ...
    
        final int count = getVirtualChildCount();
    
        ...
    
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
    
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();
    
                ...                
    
                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }
    
                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
    
                i += getChildrenSkipCount(child, i);
            }
        }
    }
    
    setChildFrame則直接調用了child的layout方法。而這裏的layout方法傳遞的r和b參數,對應的是l+measureWidth和t+measureHeight計算出來的。所以通常情況下,調用View的getWidth方法(返回的是r-l)和getMeasureWidth方法,其返回值是一致的,即measureWidth的值。而在一些情況下,多次進行measure會導致layout階段與measure階段的width不同,但總的來說兩者基本上是相等的。height也是一樣的情況。
    private void setChildFrame(View child, int left, int top, int width, int height) {
        child.layout(left, top, left + width, top + height);
    }
    
  3. draw
    • 流程:
      1. 繪製背景:background.draw(canvas)
      2. 繪製自己:onDraw
      3. 繪製children:dispatchDraw->child.draw
      4. 繪製裝飾:onDrawScrollBars
      public void draw(Canvas canvas) {
          final int privateFlags = mPrivateFlags;
          final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                  (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
          mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
      
          /*
           * Draw traversal performs several drawing steps which must be executed
           * in the appropriate order:
           *
           *      1. Draw the background
           *      2. If necessary, save the canvas' layers to prepare for fading
           *      3. Draw view's content
           *      4. Draw children
           *      5. If necessary, draw the fading edges and restore layers
           *      6. Draw decorations (scrollbars for instance)
           */
      
          // Step 1, draw the background, if needed
          int saveCount;
      
          if (!dirtyOpaque) {
              drawBackground(canvas);
          }
      
          // skip step 2 & 5 if possible (common case)
          final int viewFlags = mViewFlags;
          boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
          boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
          if (!verticalEdges && !horizontalEdges) {
              // Step 3, draw the content
              if (!dirtyOpaque) onDraw(canvas);
      
              // Step 4, draw the children
              dispatchDraw(canvas);
      
              drawAutofilledHighlight(canvas);
      
              // Overlay is part of the content and draws beneath Foreground
              if (mOverlay != null && !mOverlay.isEmpty()) {
                  mOverlay.getOverlayView().dispatchDraw(canvas);
              }
      
              // Step 6, draw decorations (foreground, scrollbars)
              onDrawForeground(canvas);
      
              // Step 7, draw the default focus highlight
              drawDefaultFocusHighlight(canvas);
      
              if (debugDraw()) {
                  debugDrawFocus(canvas);
              }
      
              // we're done...
              return;
          }
      
    • tips
      setWillNotDraw方法:該方法用於設置表示其是否會進行draw,以便系統進行優化。ViewGroup會默認設置爲true,所以如果一個自定義VIewGroup有draw的需求,要將其設置爲false。

4.自定義View

  1. 分類
    • 繼承View
      實現一些特殊的效果,需要重寫onDraw。同時需要處理wrap_content和padding。
    • 繼承ViewGroup
      實現自定義佈局,實現組合的效果。需要處理自身的measure、layout,以及children的measure、layout。
    • 繼承特定View
      擴展某個已有View的功能。wrap_content和padding不需要自己處理。
    • 繼承特定ViewGroup
      實現組合的效果。不需要處理measure、layout。
  2. 自定義View須知
    • 處理wrap_content
      直接繼承View或ViewGroup,默認的onMeasure無法正確處理wrap_content。
    • 處理padding
      直接繼承View需要在draw方法中處理padding;直接繼承ViewGroup需要在onMeasure和onLayout中考慮padding和margin的影響。
    • 儘量不要在View中使用Handler
      View內部提供的post系列方法可以滿足需求。
    • View中的線程和動畫,需要及時停止
      當View不可見時,需要及時停止,否則可能造成內存泄漏。可以根據回調方法去處理。當View被remove或者VIew所在的Activity退出時,View#onDetachedFromWindow會被調用;當View的Activity啓動時,View#onAttactedToWindow會被調用。
    • VIew嵌套滾動時,處理好滾動衝突
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章