【轉】深刻理解Android之View的繪製流程

概述

本篇文章會從源碼(基於Android 6.0)角度分析Android中View的繪製流程,側重於對總體流程的分析,對一些難以理解的點加以重點闡述,目的是把View繪製的整個流程把握好,而對於特定實現細節則能夠往後再對相應源碼進行研讀。
在進行實際的分析以前,咱們先來看下面這張圖:html


 

咱們來對上圖作出簡單解釋:DecorView是一個應用窗口的根容器,它本質上是一個FrameLayout。DecorView有惟一一個子View,它是一個垂直LinearLayout,包含兩個子元素,一個是TitleView(ActionBar的容器),另外一個是ContentView(窗口內容的容器)。關於ContentView,它是一個FrameLayout(android.R.id.content),咱們日常用的setContentView就是設置它的子View。上圖還表達了每一個Activity都與一個Window(具體來講是PhoneWindow)相關聯,用戶界面則由Window所承載。node

Window

Window 即窗口,這個概念在 Android Framework 中的實現爲 android.view.Window 這個抽象類,這個抽象類是對 Android 系統中的窗口的抽象。在介紹這個類以前,咱們先來看看究竟什麼是窗口呢?android

實際上,窗口是一個宏觀的思想,它是屏幕上用於繪製各類UI元素及響應用戶輸入事件的一個矩形區域。一般具有如下兩個特色:canvas

  • 獨立繪製,不與其它界面相互影響;
  • 不會觸發其它界面的輸入事件;

在Android系統中,窗口是獨佔一個Surface實例的顯示區域,每一個窗口的Surface由WindowManagerService分配。咱們能夠把Surface看做一塊畫布,應用能夠經過Canvas或OpenGL在其上面做畫。畫好以後,經過SurfaceFlinger將多塊Surface按照特定的順序(即Z-order)進行混合,然後輸出到FrameBuffer中,這樣用戶界面就得以顯示。緩存

android.view.Window這個抽象類能夠看作Android中對窗口這一宏觀概念所作的約定,而PhoneWindow這個類是Framework爲咱們提供的Android窗口概念的具體實現。接下來咱們先來介紹一下android.view.Window這個抽象類。app

這個抽象類包含了三個核心組件:ide

  • WindowManager.LayoutParams: 窗口的佈局參數;
  • Callback: 窗口的回調接口,一般由Activity實現;
  • ViewTree: 窗口所承載的控件樹。

下面咱們來看一下Android中Window的具體實現(也是惟一實現)——PhoneWindow。佈局

PhoneWindow

前面咱們提到了,PhoneWindow 這個類是Framework爲咱們提供的Android窗口的具體實現。咱們平時調用setContentView()方法設置Activity的用戶界面時,實際上就完成了對所關聯的PhoneWindow的 ViewTree 的設置。咱們還能夠經過Activity類的 requestWindowFeature() 方法來定製Activity關聯 PhoneWindow 的外觀,這個方法實際上作的是把咱們所請求的窗口外觀特性存儲到了 PhoneWindow 的 mFeatures 成員中,在窗口繪製階段生成外觀模板時,會根據 mFeatures 的值繪製特定外觀。this

從setContentView()說開去

在分析setContentView()方法前,咱們須要明確:這個方法只是完成了Activity的ContentView的建立,而並無執行View的繪製流程。
當咱們自定義Activity繼承自android.app.Activity時候,調用的setContentView()方法是Activity類的,源碼以下:spa

public void setContentView(@LayoutRes int layoutResID) {    
  getWindow().setContentView(layoutResID);    
  . . .
}

getWindow()方法會返回Activity所關聯的PhoneWindow,也就是說,實際上調用到了PhoneWindow的setContentView()方法,源碼以下:

@Override
public void setContentView(int layoutResID) {
  if (mContentParent == null) {
    // mContentParent即爲上面提到的ContentView的父容器,若爲空則調用installDecor()生成
    installDecor();
  } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
    // 具備FEATURE_CONTENT_TRANSITIONS特性表示開啓了Transition
    // mContentParent不爲null,則移除decorView的全部子View
    mContentParent.removeAllViews();
  }
  if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
    // 開啓了Transition,作相應的處理,咱們不討論這種狀況
    // 感興趣的同窗能夠參考源碼
    . . .
  } else {
    // 通常狀況會來到這裏,調用mLayoutInflater.inflate()方法來填充佈局
    // 填充佈局也就是把咱們設置的ContentView加入到mContentParent中
    mLayoutInflater.inflate(layoutResID, mContentParent);
  }
  . . .
  // cb即爲該Window所關聯的Activity
  final Callback cb = getCallback();
  if (cb != null && !isDestroyed()) {
    // 調用onContentChanged()回調方法通知Activity窗口內容發生了改變
    cb.onContentChanged();
  }

  . . .
}

LayoutInflater.inflate()

在上面咱們看到了,PhoneWindow的setContentView()方法中調用了LayoutInflater的inflate()方法來填充佈局,這個方法的源碼以下:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
  return inflate(resource, root, root != null);
}

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
  final Resources res = getContext().getResources();
  . . .
  final XmlResourceParser parser = res.getLayout(resource);
  try {
    return inflate(parser, root, attachToRoot);
  } finally {
    parser.close();
  }
}

在PhoneWindow的setContentView()方法中傳入了decorView做爲LayoutInflater.inflate()的root參數,咱們能夠看到,經過層層調用,最終調用的是inflate(XmlPullParser, ViewGroup, boolean)方法來填充佈局。這個方法的源碼以下:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
  synchronized (mConstructorArgs) {
    . . .
    final Context inflaterContext = mContext;
    final AttributeSet attrs = Xml.asAttributeSet(parser);
    Context lastContext = (Context) mConstructorArgs[0];
    mConstructorArgs[0] = inflaterContext;

    View result = root;

    try {
      // Look for the root node.
      int type;
      // 一直讀取xml文件,直到遇到開始標記
      while ((type = parser.next()) != XmlPullParser.START_TAG &&
          type != XmlPullParser.END_DOCUMENT) {
        // Empty
       }
      // 最早遇到的不是開始標記,報錯
      if (type != XmlPullParser.START_TAG) {
        throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
      }

      final String name = parser.getName();
      . . .
      // 單獨處理<merge>標籤,不熟悉的同窗請參考官方文檔的說明
      if (TAG_MERGE.equals(name)) {
        // 若包含<merge>標籤,父容器(即root參數)不可爲空且attachRoot須爲true,不然報錯
        if (root == null || !attachToRoot) {
          throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
        }
        
        // 遞歸地填充佈局
        rInflate(parser, root, inflaterContext, attrs, false);
     } else {
        // temp爲xml佈局文件的根View
        final View temp = createViewFromTag(root, name, inflaterContext, attrs); 
        ViewGroup.LayoutParams params = null;
        if (root != null) {
          . . .
          // 獲取父容器的佈局參數(LayoutParams)
          params = root.generateLayoutParams(attrs);
          if (!attachToRoot) {
            // 若attachToRoot參數爲false,則咱們只會將父容器的佈局參數設置給根View
            temp.setLayoutParams(params);
          }

        }

        // 遞歸加載根View的全部子View
        rInflateChildren(parser, temp, attrs, true);
        . . .

        if (root != null && attachToRoot) {
          // 若父容器不爲空且attachToRoot爲true,則將父容器做爲根View的父View包裹上來
          root.addView(temp, params);
        }
      
        // 若root爲空或是attachToRoot爲false,則以根View做爲返回值
        if (root == null || !attachToRoot) {
           result = temp;
        }
      }

    } catch (XmlPullParserException e) {
      . . . 
    } catch (Exception e) {
      . . . 
    } finally {

      . . .
    }
    return result;
  }
}

在上面的源碼中,首先對於佈局文件中的 <merge> 標籤進行單獨處理,調用 rInflate() 方法來遞歸填充佈局。這個方法的源碼以下:

void rInflate(XmlPullParser parser, View parent, Context context,
    AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    // 獲取當前標記的深度,根標記的深度爲0
    final int depth = parser.getDepth();
    int type;
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
        parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
      // 不是開始標記則繼續下一次迭代
      if (type != XmlPullParser.START_TAG) {
        continue;
      }
      final String name = parser.getName();
      // 對一些特殊標記作單獨處理
      if (TAG_REQUEST_FOCUS.equals(name)) {
        parseRequestFocus(parser, parent);
      } else if (TAG_TAG.equals(name)) {
        parseViewTag(parser, parent, attrs);
      } else if (TAG_INCLUDE.equals(name)) {
        if (parser.getDepth() == 0) {
          throw new InflateException("<include /> cannot be the root element");
        }
        // 對<include>作處理
        parseInclude(parser, context, parent, attrs);
      } else if (TAG_MERGE.equals(name)) {
        throw new InflateException("<merge /> must be the root element");
      } else {
        // 對通常標記的處理
        final View view = createViewFromTag(parent, name, context, attrs);
        final ViewGroup viewGroup = (ViewGroup) parent;
        final ViewGroup.LayoutParams params=viewGroup.generateLayoutParams(attrs);
        // 遞歸地加載子View
        rInflateChildren(parser, view, attrs, true);
        viewGroup.addView(view, params);
      }
    }

    if (finishInflate) {
        parent.onFinishInflate();
    }
} 

咱們能夠看到,上面的inflate()和rInflate()方法中都調用了rInflateChildren()方法,這個方法的源碼以下:

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

 

從源碼中咱們能夠知道,rInflateChildren()方法實際上調用了rInflate()方法。

到這裏,setContentView()的總體執行流程咱們就分析完了,至此咱們已經完成了Activity的ContentView的建立與設置工做。接下來,咱們開始進入正題,分析View的繪製流程。

ViewRoot

在介紹View的繪製前,首先咱們須要知道是誰負責執行View繪製的整個流程。實際上,View的繪製是由ViewRoot來負責的。每一個應用程序窗口的decorView都有一個與之關聯的ViewRoot對象,這種關聯關係是由WindowManager來維護的。

那麼decorView與ViewRoot的關聯關係是在何時創建的呢?答案是Activity啓動時,ActivityThread.handleResumeActivity()方法中創建了它們二者的關聯關係。這裏咱們不具體分析它們創建關聯的時機與方式,感興趣的同窗能夠參考相關源碼。下面咱們直入主題,分析一下ViewRoot是如何完成View的繪製的。

View繪製的起點

當創建好了decorView與ViewRoot的關聯後,ViewRoot類的requestLayout()方法會被調用,以完成應用程序用戶界面的初次佈局。實際被調用的是ViewRootImpl類的requestLayout()方法,這個方法的源碼以下:

@Override
public void requestLayout() {
  if (!mHandlingLayoutInLayoutRequest) {
    // 檢查發起佈局請求的線程是否爲主線程  
    checkThread();
    mLayoutRequested = true;
    scheduleTraversals();
  }
}

 

上面的方法中調用了scheduleTraversals()方法來調度一次完成的繪製流程,該方法會向主線程發送一個「遍歷」消息,最終會致使ViewRootImpl的performTraversals()方法被調用。下面,咱們以performTraversals()爲起點,來分析View的整個繪製流程。

三個階段

View的整個繪製流程能夠分爲如下三個階段:

  • measure: 判斷是否須要從新計算View的大小,須要的話則計算;

  • layout: 判斷是否須要從新計算View的位置,須要的話則計算;

  • draw: 判斷是否須要從新繪製View,須要的話則重繪製。

    這三個子階段能夠用下圖來描述:

 
 

measure階段

此階段的目的是計算出控件樹中的各個控件要顯示其內容的話,須要多大尺寸。起點是ViewRootImpl的measureHierarchy()方法,這個方法的源碼以下:

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res, 
    final int desiredWindowWidth, final int desiredWindowHeight) {
  // 傳入的desiredWindowXxx爲窗口尺寸
  int childWidthMeasureSpec;
  int childHeightMeasureSpec;
  boolean windowSizeMayChange = false;
  . . .
  boolean goodMeasure = false;

  if (!goodMeasure) {
    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

    if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
      windowSizeMayChange = true;
    }
  }
  return windowSizeMayChange;
}

 

上面的代碼中調用getRootMeasureSpec()方法來獲取根MeasureSpec,這個根MeasureSpec表明了對decorView的寬高的約束信息。繼續分析以前,咱們先來簡單地介紹下MeasureSpec的概念。
MeasureSpec是一個32位整數,由SpecMode和SpecSize兩部分組成,其中,高2位爲SpecMode,低30位爲SpecSize。SpecMode爲測量模式,SpecSize爲相應測量模式下的測量尺寸。View(包括普通View和ViewGroup)的SpecMode由本View的LayoutParams結合父View的MeasureSpec生成。
SpecMode的取值可爲如下三種:

  • EXACTLY: 對子View提出了一個確切的建議尺寸(SpecSize);

  • AT_MOST: 子View的大小不得超過SpecSize;

  • UNSPECIFIED: 對子View的尺寸不做限制,一般用於系統內部。

傳入performMeasure()方法的MeasureSpec的SpecMode爲EXACTLY,SpecSize爲窗口尺寸。
performMeasure()方法的源碼以下:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
  . . .
  try { 
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  } finally {
    . . .
  }
}

 

上面代碼中的mView即爲decorView,也就是說會轉向對View.measure()方法的調用,這個方法的源碼以下:

/**
 * 調用這個方法來算出一個View應該爲多大。參數爲父View對其寬高的約束信息。
 * 實際的測量工做在onMeasure()方法中進行
 */
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
  . . . 
  // 判斷是否須要從新佈局

  // 若mPrivateFlags中包含PFLAG_FORCE_LAYOUT標記,則強制從新佈局
  // 好比調用View.requestLayout()會在mPrivateFlags中加入此標記
  final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
  final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
      || heightMeasureSpec != mOldHeightMeasureSpec;
  final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
      && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
  final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
      && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
  final boolean needsLayout = specChanged
      && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

  // 須要從新佈局  
  if (forceLayout || needsLayout) {
    . . .
    // 先嚐試從緩從中獲取,若forceLayout爲true或是緩存中不存在或是
    // 忽略緩存,則調用onMeasure()從新進行測量工做
    int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
    if (cacheIndex < 0 || sIgnoreMeasureCache) {
      // measure ourselves, this should set the measured dimension flag back
      onMeasure(widthMeasureSpec, heightMeasureSpec);
      . . .
    } else {
      // 緩存命中,直接從緩存中取值便可,沒必要再測量
      long value = mMeasureCache.valueAt(cacheIndex);
      // Casting a long to int drops the high 32 bits, no mask needed
      setMeasuredDimensionRaw((int) (value >> 32), (int) value);
      . . .
    }
    . . .
  }
  mOldWidthMeasureSpec = widthMeasureSpec;
  mOldHeightMeasureSpec = heightMeasureSpec;
  mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
      (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

 

從measure()方法的源碼中咱們能夠知道,只有如下兩種狀況之一,纔會進行實際的測量工做:

  • forceLayout爲true:這表示強制從新佈局,能夠經過View.requestLayout()來實現;

  • needsLayout爲true,這須要specChanged爲true(表示本次傳入的MeasureSpec與上次傳入的不一樣),而且如下三個條件之一成立:

  • sAlwaysRemeasureExactly爲true: 該變量默認爲false;

  • isSpecExactly爲false: 若父View對子View提出了精確的寬高約束,則該變量爲true,不然爲false

  • matchesSpecSize爲false: 表示父 View 的寬高尺寸要求與上次測量的結果不一樣

對於decorView來講,實際執行測量工做的是FrameLayout的onMeasure()方法,該方法的源碼以下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  int count = getChildCount();
  . . .
  int maxHeight = 0;
  int maxWidth = 0;

  int childState = 0;
  for (int i = 0; i < count; i++) {
    final View child = getChildAt(i);
    if (mMeasureAllChildren || child.getVisibility() != GONE) {
      measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
      final LayoutParams lp = (LayoutParams) child.getLayoutParams();
      maxWidth = Math.max(maxWidth,
          child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
      maxHeight = Math.max(maxHeight,
          child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
      childState = combineMeasuredStates(childState, child.getMeasuredState());

      . . .
    }
  }

  // Account for padding too
  maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
  maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

  // Check against our minimum height and width
  maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
  maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

  // Check against our foreground's minimum height and width
  final Drawable drawable = getForeground();
  if (drawable != null) {
    maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
    maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
  }

  setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
        resolveSizeAndState(maxHeight, heightMeasureSpec,
        childState << MEASURED_HEIGHT_STATE_SHIFT));
  . . . 
}

 

FrameLayout是ViewGroup的子類,後者有一個View[]類型的成員變量mChildren,表明了其子View集合。經過getChildAt(i)能獲取指定索引處的子View,經過getChildCount()能夠得到子View的總數。

在上面的源碼中,首先調用measureChildWithMargins()方法對全部子View進行了一遍測量,並計算出全部子View的最大寬度和最大高度。然後將獲得的最大高度和寬度加上padding,這裏的padding包括了父View的padding和前景區域的padding。而後會檢查是否設置了最小寬高,並與其比較,將二者中較大的設爲最終的最大寬高。最後,若設置了前景圖像,咱們還要檢查前景圖像的最小寬高。

通過了以上一系列步驟後,咱們就獲得了maxHeight和maxWidth的最終值,表示當前容器View用這個尺寸就可以正常顯示其全部子View(同時考慮了padding和margin)。然後咱們須要調用resolveSizeAndState()方法來結合傳來的MeasureSpec來獲取最終的測量寬高,並保存到mMeasuredWidth與mMeasuredHeight成員變量中。

從以上代碼的執行流程中,咱們能夠看到,容器View經過measureChildWithMargins()方法對全部子View進行測量後,才能獲得自身的測量結果。也就是說,對於ViewGroup及其子類來講,要先完成子View的測量,再進行自身的測量(考慮進padding等)。
接下來咱們來看下ViewGroup的measureChildWithMargins()方法的實現:

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);

}

 

由以上代碼咱們能夠知道,對於ViewGroup來講,它會調用child.measure()來完成子View的測量。傳入ViewGroup的MeasureSpec是它的父View用於約束其測量的,那麼ViewGroup自己也須要生成一個childMeasureSpec來限制它的子View的測量工做。這個childMeasureSpec就由getChildMeasureSpec()方法生成。接下來咱們來分析這個方法:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
  // spec爲父View的MeasureSpec
  // padding爲父View在相應方向的已用尺寸加上父View的padding和子View的margin
  // childDimension爲子View的LayoutParams的值
  int specMode = MeasureSpec.getMode(spec);
  int specSize = MeasureSpec.getSize(spec);

  // 如今size的值爲父View相應方向上的可用大小
  int size = Math.max(0, specSize - padding);

  int resultSize = 0;
  int resultMode = 0;

  switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
      if (childDimension >= 0) { // 從這裏也能夠看出來爲啥 match_parent 和 wrap_content 爲啥爲負數 // 表示子View的LayoutParams指定了具體大小值(xx dp)
        resultSize = childDimension;
        resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.MATCH_PARENT) {
        // 子View想和父View同樣大
        resultSize = size;
        resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.WRAP_CONTENT) {
        // 子View想本身決定其尺寸,但不能比父View大 
        resultSize = size;
        resultMode = MeasureSpec.AT_MOST;
      }
      break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
      if (childDimension >= 0) {
        // 子View指定了具體大小
        resultSize = childDimension;
        resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.MATCH_PARENT) {
        // 子View想跟父View同樣大,可是父View的大小未固定下來
        // 因此指定約束子View不能比父View大
        resultSize = size;
        resultMode = MeasureSpec.AT_MOST;
      } else if (childDimension == LayoutParams.WRAP_CONTENT) {
        // 子View想要本身決定尺寸,但不能比父View大
        resultSize = size;
        resultMode = MeasureSpec.AT_MOST;
      }
      break;

      . . .
  }

  //noinspection ResourceType
  return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

上面的方法展示了根據父 View 的 MeasureSpec 和子 View 的 LayoutParams 生成子 View 的 MeasureSpec 的過程,子 View 的 LayoutParams 表示了子 View 的期待大小 。這個產生的MeasureSpec用於指導子View自身的測量結果的肯定。
在上面的代碼中,咱們能夠看到當ParentMeasureSpec的SpecMode爲EXACTLY時,表示父View對子View指定了確切的寬高限制。此時根據子View的LayoutParams的不一樣,分如下三種狀況:

  • 具體大小(childDimension):這種狀況下令子View的SpecSize爲childDimension,即子View在LayoutParams指定的具體大小值;令子View的SpecMode爲EXACTLY,即這種狀況下若該子View爲容器View,它也有能力給其子View指定確切的寬高限制(子View只能在這個寬高範圍內),若爲普通View,它的最終測量大小就爲childDimension。

  • match_parent:此時表示子View想和父View同樣大。這種狀況下獲得的子View的SpecMode與上種狀況相同,只不過SpecSize爲size,即父View的剩餘可用大小。

  • wrap_content: 這表示了子View想本身決定本身的尺寸(根據其內容的大小動態決定)。這種狀況下子View的確切測量大小隻能在其自己的onMeasure()方法中計算得出,父View此時無從知曉。因此暫時將子View的SpecSize設爲size(父View的剩餘大小);令子View的SpecMode爲AT_MOST,表示了若子View爲ViewGroup,它沒有能力給其子View指定確切的寬高限制,畢竟它自己的測量寬高還懸而未定。

當 ParentMeasureSpec 的 SpecMode 爲 AT_MOST 時,咱們也能夠根據子 View 的 LayoutParams 的不一樣來分三種狀況討論:

  • 具體大小:這時令子 View 的 SpecSize 爲 childDimension,SpecMode 爲 EXACTLY。

  • match_parent:表示子 View 想和父View同樣大,故令子 View 的 SpecSize 爲 size,可是因爲父 View 自己的測量寬高還無從肯定,因此只是暫時令子View的測量結果爲父View目前的可用大小。這時令子 View 的 SpecMode 爲 AT_MOST。

  • wrap_content:表示子View想本身決定大小(根據其內容動態肯定)。然而這時父View還沒法肯定其自身的測量寬高,因此暫時令子View的SpecSize爲size,SpecMode爲AT_MOST。

    從上面的分析咱們能夠獲得一個通用的結論,當子View的測量結果可以肯定時,子View的SpecMode就爲EXACTLY;當子View的測量結果還不能肯定(只是暫時設爲某個值)時,子View的SpecMode爲AT_MOST。

在measureChildWithMargins()方法中,獲取了知道子View測量的MeasureSpec後,接下來就要調用child.measure()方法,並把獲取到的childMeasureSpec傳入。這時便又會調用onMeasure()方法,若此時的子View爲ViewGroup的子類,便會調用相應容器類的onMeasure()方法,其餘容器View的onMeasure()方法與FrameLayout的onMeasure()方法執行過程類似。

下面會咱們回到 FrameLayout的 onMeasure() 方法,當遞歸地執行完全部子 View 的測量工做後,會調用 resolveSizeAndState() 方法來根據以前的測量結果肯定最終對FrameLayout的測量結果並存儲起來。View類的resolveSizeAndState()方法的源碼以下:

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
  final int specMode = MeasureSpec.getMode(measureSpec);
  final int specSize = MeasureSpec.getSize(measureSpec);
  final int result;
  switch (specMode) {
    case MeasureSpec.AT_MOST:
      if (specSize < size) {
        // 父View給定的最大尺寸小於徹底顯示內容所需尺寸
        // 則在測量結果上加上MEASURED_STATE_TOO_SMALL
        result = specSize | MEASURED_STATE_TOO_SMALL;
      } else {
       result = size;
      }
      break;

    case MeasureSpec.EXACTLY:
      // 若specMode爲EXACTLY,則不考慮size,result直接賦值爲specSize
      result = specSize;
      break;

    case MeasureSpec.UNSPECIFIED:
    default:
      result = size;
  }

  return result | (childMeasuredState & MEASURED_STATE_MASK);

}

 

對於普通 View,會調用 View 類的 onMeasure() 方法來進行實際的測量工做,該方法的源碼以下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

 

對於普通View(非ViewgGroup)來講,只需完成自身的測量工做便可。以上代碼中經過 setMeasuredDimension() 方法設置測量的結果,具體來講是以 getDefaultSize() 方法的返回值來做爲測量結果。getDefaultSize() 方法的源碼以下:

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;
}

由以上代碼咱們能夠看到,View的getDefaultSize()方法對於AT_MOST和EXACTLY這兩種狀況都返回了SpecSize做爲result。因此若咱們的自定義View直接繼承了View類,咱們就要本身對wrap_content (對應了AT_MOST)這種狀況進行處理,不然對自定義View指定wrap_content就和match_parent效果同樣了。

layout階段

layout階段的基本思想也是由根View開始,遞歸地完成整個控件樹的佈局(layout)工做。

View.layout()

咱們把對decorView的layout()方法的調用做爲佈局整個控件樹的起點,實際上調用的是View類的layout()方法,源碼以下:

public void layout(int l, int t, int r, int b) {
    // l爲本View左邊緣與父View左邊緣的距離
    // t爲本View上邊緣與父View上邊緣的距離
    // r爲本View右邊緣與父View左邊緣的距離
    // b爲本View下邊緣與父View上邊緣的距離
    . . .
    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);
        . . .
            
    }
    . . .
}

 

這個方法會調用setFrame()方法來設置View的mLeft、mTop、mRight和mBottom四個參數,這四個參數描述了View相對其父View的位置(分別賦值爲l, t, r, b),在setFrame()方法中會判斷View的位置是否發生了改變,若發生了改變,則須要對子View進行從新佈局,對子View的局部是經過onLayout()方法實現了。因爲普通View( 非ViewGroup)不含子View,因此View類的onLayout()方法爲空。所以接下來,咱們看看ViewGroup類的onLayout()方法的實現。

ViewGroup.onLayout()

實際上ViewGroup類的onLayout()方法是abstract,這是由於不一樣的佈局管理器有着不一樣的佈局方式。
這裏咱們以decorView,也就是FrameLayout的onLayout()方法爲例,分析ViewGroup的佈局過程:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
  layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
  final int count = getChildCount();
  final int parentLeft = getPaddingLeftWithForeground();
  final int parentRight = right - left - getPaddingRightWithForeground();
  final int parentTop = getPaddingTopWithForeground();
  final int parentBottom = bottom - top - getPaddingBottomWithForeground();

  for (int i = 0; i < count; i++) {
    final View child = getChildAt(i);
    if (child.getVisibility() != GONE) {
      final LayoutParams lp = (LayoutParams) child.getLayoutParams();
      final int width = child.getMeasuredWidth();
      final int height = child.getMeasuredHeight();
      int childLeft;
      int childTop;
      int gravity = lp.gravity;

      if (gravity == -1) {
        gravity = DEFAULT_CHILD_GRAVITY;
      }

      final int layoutDirection = getLayoutDirection();
      final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
      final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

      switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
        case Gravity.CENTER_HORIZONTAL:
          childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
          break;

        case Gravity.RIGHT:
          if (!forceLeftGravity) {
            childLeft = parentRight - width - lp.rightMargin;
            break;
          }

        case Gravity.LEFT:
        default:
          childLeft = parentLeft + lp.leftMargin;

      }

      switch (verticalGravity) {
        case Gravity.TOP:
          childTop = parentTop + lp.topMargin;
          break;

        case Gravity.CENTER_VERTICAL:
          childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
          break;

        case Gravity.BOTTOM:
          childTop = parentBottom - height - lp.bottomMargin;
          break;

        default:
          childTop = parentTop + lp.topMargin;
      }
      child.layout(childLeft, childTop, childLeft + width, childTop + height);
    }
  }
}

 

在上面的方法中,parentLeft表示當前View爲其子View顯示區域指定的一個左邊界,也就是子View顯示區域的左邊緣到父View的左邊緣的距離,parentRight、parentTop、parentBottom的含義同理。肯定了子View的顯示區域後,接下來,用一個for循環來完成子View的佈局。
在確保子View的可見性不爲GONE的狀況下才會對其進行佈局。首先會獲取子View的LayoutParams、layoutDirection等一系列參數。上面代碼中的childLeft表明了最終子View的左邊緣距父View左邊緣的距離,childTop表明了子View的上邊緣距父View的上邊緣的距離。會根據子View的layout_gravity的取值對childLeft和childTop作出不一樣的調整。最後會調用child.layout()方法對子View的位置參數進行設置,這時便轉到了View.layout()方法的調用,若子View是容器View,則會遞歸地對其子View進行佈局。

到這裏,layout階段的大體流程咱們就分析完了,這個階段主要就是根據上一階段獲得的View的測量寬高來肯定View的最終顯示位置。顯然,通過了measure階段和layout階段,咱們已經肯定好了View的大小和位置,那麼接下來就能夠開始繪製View了。

draw階段

對於本階段的分析,咱們以decorView.draw()做爲分析的起點,也就是View.draw()方法,它的源碼以下:

public void draw(Canvas canvas) {
  . . . 
  // 繪製背景,只有dirtyOpaque爲false時才進行繪製,下同
  int saveCount;
  if (!dirtyOpaque) {
    drawBackground(canvas);
  }

  . . . 

  // 繪製自身內容
  if (!dirtyOpaque) onDraw(canvas);

  // 繪製子View
  dispatchDraw(canvas);

   . . .
  // 繪製滾動條等
  onDrawForeground(canvas);

}

簡單起見,在上面的代碼中咱們省略了實現滑動時漸變邊框效果相關的邏輯。實際上,View類的onDraw()方法爲空,由於每一個View繪製自身的方式都不盡相同,對於decorView來講,因爲它是容器View,因此它自己並無什麼要繪製的。dispatchDraw()方法用於繪製子View,顯然普通View(非ViewGroup)並不能包含子View,因此View類中這個方法的實現爲空。

ViewGroup 類的 dispatchDraw() 方法中會依次調用 drawChild() 方法來繪製子 View,drawChild() 方法的源碼以下:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
  return child.draw(canvas, this, drawingTime);
}

 

這個方法調用了View.draw(Canvas, ViewGroup,long)方法來對子View進行繪製。在draw(Canvas, ViewGroup, long)方法中,首先對canvas進行了一系列變換,以變換到將要被繪製的View的座標系下。完成對 canvas 的變換後,便會調用 View.draw(Canvas) 方法進行實際的繪製工做,此時傳入的canvas爲通過變換的,在將被繪製View的座標系下的canvas。

進入到View.draw(Canvas)方法後,會向以前介紹的同樣,執行如下幾步:

  • 繪製背景;
  • 經過onDraw()繪製自身內容;
  • 經過dispatchDraw()繪製子View;
  • 繪製滾動條

至此,整個View的繪製流程咱們就分析完了。若文中有敘述不清晰或是不許確的地方,但願你們可以指出,謝謝你們:)

 


做者:超超boy

連接:http://www.javashuo.com/article/p-zyizxgcv-gk.html

相關文章
相關標籤/搜索