您好,登錄后才能下訂單哦!
參考資料 << Android 開發藝術探索 >> 歡迎訪問我的個人博客 傳送門
在日常開發中,我們每天都在和各種 View 打交道,比如TextView,Button等,我們直接拿過來就可以使用,那么 Android 是怎么把 View 繪制到屏幕上呢,接下來我們結合源碼來具體分析。
在具體結合源碼分析前,先了解一個比較重要的概念 ViewRoot
先看一張圖 Android 窗口構成圖解
ViewRoot 對應于 ViewRootImpl 類,它是連接 WindowManager 和 根布局 DecorView(看上圖) 的紐帶, View 的三大流程均是通過 ViewRoot 來完成的。在 ActivityThread 中,當 Activity 對象被創建完畢后,會將 DecorView 添加到 Window 中,同時會創建 ViewRootImpl 對象,并將 ViewRootImpl 對象和 DecorView 建立關聯。
View 的繪制流程是從 ViewRoot 的 performTraversals 方法開始的,它經過 measure、layout 和 draw 三個過程才能最終將一個 View 繪制出來,其中 measure 用來測量 View 的寬和高,layout 用來確定 View 在父容器中的放置位置,而 draw 則負責將 View 繪制在屏幕上。針對 performTraversals的大致流程如下:
performTraversals 會依次調用 performMeasure、performLayout 和 performDraw 三個方法,這三個方法分別完成頂級 View 的 measure、layout 和 draw 這三大流程,其中在 performMeasure 中會調用 measure 方法,在 measure 方法中又會調用 onMeasure 方法,在 onMeasure 方法中則會對所有的子元素進行 measure 過程,這個時候 measure 流程就從父容器傳遞到子元素中了,這樣就完成了一次 measure 過程。接著子元素會重復父容器的 measure 過程,如此反復就完成了整個 View 樹的遍歷。同理,performLayout 和 performDraw 的傳遞流程和 performMeasure 是類似的,唯一不同的是,performDraw 的傳遞過程是在 draw 方法中通過 dispatchDraw 來實現的,不過這并沒有本質區別。
接下來結合源碼來分析這三個過程。
這里分兩種情況,View 的測量過程和 ViewGroup 的測量過程。
View 的 測量過程由其 measure 方法來完成,源碼如下:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//省略代碼...
if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} 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);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
可以看到 measure 方法是一個 final 類型的方法,這意味著子類不能重寫此方法。
在 13 行 measure 中會調用 onMeasure 方法,這個方法是測量的主要方法,繼續看 onMeasure 的實現
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension 方法的作用是設置 View 寬和高的測量值,我們主要看 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;
}
可以看到要得到測量的尺寸需要用到 MeasureSpec,MeasureSpec 是什么鬼呢,敲黑板了,重點來了。
MeasureSpec 決定了 View 的測量過程。確切來說,MeasureSpec 在很大程度上決定了一個 View 的尺寸規格。
來看 MeasureSpec 類的實現
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}
public static String toString(int measureSpec) {
//省略...
}
}
可以看出 MeasureSpec 中有兩個主要的值,SpecMode 和 SpecSize, SpecMode 是指測量模式,而 SpecSize 是指在某種測量模式下的規格大小。
SpecMode 有三種模式:
UNSPECIFIED
不限制:父容器不對 View 有任何限制,要多大給多大,這種情況比較少見,一般不會用到。
EXACTLY
限制固定值:父容器已經檢測出 View 所需要的精確大小,這個時候 View 的最終大小就是 SpecSize 所指定的值。它對應于 LayoutParams 中的 match_parent 和具體的數值這兩種模式。
MeasureSpec 中三個主要的方法來處理 SpecMode 和 SpecSize
不知道童鞋們之前有沒有注意到 onMeasure 有兩個參數 widthMeasureSpec 和 heightMeasureSpec,那這兩個值從哪來的呢,這兩個值都是由父視圖經過計算后傳遞給子視圖的,說明父視圖會在一定程度上決定子視圖的大小,但是最外層的根視圖 也就是 DecorView ,它的 widthMeasureSpec 和 heightMeasureSpec 又是從哪里得到的呢?這就需要去分析 ViewRoot 中的源碼了,在 performTraversals 方法中調了 measureHierarchy 方法來創建 MeasureSpec 源碼如下:
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//省略代碼...
}
里面調用了 getRootMeasureSpec 方法生成 MeasureSpec,繼續查看 getRootMeasureSpec 源碼
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;
}
通過上述代碼,DecorView 的 MeasureSpec 的產生過程就很明確了,具體來說其遵守如下規則,根據它的 LayoutParams 中的寬和高的參數來劃分。
對于 DecorView 而言, rootDimension 的值為 lp.width 和 lp.height 也就是屏幕的寬和高,所以說 根視圖 DecorView 的大小默認總是會充滿全屏的。那么我們使用的 View 也就是 ViewGroup 中 View 的 MeasureSpec 產生過程又是怎么樣的呢,在 ViewGroup 的測量過程中會具體介紹。
先回頭看 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;
}
現在理解起來是不是很簡單呢,如果 specMode 是 AT_MOST 或 EXACTLY 就返回 specSize,這也是系統默認的行為。之后會在 onMeasure 方法中調用 setMeasuredDimension 方法來設定測量出的大小,這樣 View 的 measure 過程就結束了,接下來看 ViewGroup 的 measure 過程。
ViewGroup中定義了一個 measureChildren 方法來去測量子視圖的大小,如下所示
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);
}
}
}
從上述代碼來看,除了完成自己的 measure 過程以外,還會遍歷去所有在頁面顯示的子元素,
然后逐個調用 measureChild 方法來測量相應子視圖的大小
measureChild 的實現如下
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);
}
measureChild 的思想就是取出子元素的 LayoutParams,然后再通過 getChildMeasureSpec 來創建子元素的 MeasureSpec,接著將 MeasureSpec 直接傳遞給 View 的 measure 方法來進行測量。
那么 ViewGroup 是如何創建來創建子元素的 MeasureSpec 呢,我們繼續看 getChildMeasureSpec 方法源碼:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
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) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上面的代碼理解起來很簡單,為了更清晰地理解 getChildMeasureSpec 的邏輯,這里提供一個表,表中對 getChildMeasureSpec 的工作原理進行了梳理,表中的 parentSize 是指父容器中目前可使用的大小,childSize 是子 View 的 LayoutParams 獲取的值,從 measureChild 方法中可看出
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);
表如下:
通過上表可以看出,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams,就可以快速地確定出子元素的 MeasureSpec 了,有了 MeasureSpec 就可以進一步確定出子元素測量后的大小了。
至此,View 和 ViewGroup 的測量過程就告一段落了。來個小結。
MeasureSpec 的模式和生成規則
MeasureSpec 中 specMode 有三種模式:
UNSPECIFIED
不限制:父容器不對 View 有任何限制,要多大給多大,這種情況比較少見,一般不會用到。
EXACTLY
限制固定值:父容器已經檢測出 View 所需要的精確大小,這個時候 View 的最終大小就是 SpecSize 所指定的值。它對應于 LayoutParams 中的 match_parent 和具體的數值這兩種模式。
生成規則:
MeasureSpec 測量過程:
measure 過程主要就是從頂層父 View 向子 View 遞歸調用 view.measure 方法,measure 中調 onMeasure 方法的過程。
說人話呢就是,視圖大小的控制是由父視圖、布局文件、以及視圖本身共同完成的,父視圖會提供給子視圖參考的大小,而開發人員可以在 XML 文件中指定視圖的大小,然后視圖本身會對最終的大小進行拍板。
那么測量過后,怎么獲取 View 的測量結果呢
一般情況下 View 測量大小和最終大小是一樣的,我們可以使用 getMeasuredWidth 方法和 getMeasuredHeight 方法來獲取視圖測量出的寬高,但是必須在 setMeasuredDimension 之后調用,否則調用這兩個方法得到的值都會是0。為什么要說是一般情況下是一樣的呢,在下文介紹 Layout 中會具體介紹。
測量結束后,視圖的大小就已經測量好了,接下來就是 Layout 布局的過程。上文說過 ViewRoot 的 performTraversals 方法會在 measure 結束后,執行 performLayout 方法,performLayout 方法則會調用 layout 方法開始布局,代碼如下
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
if (host == null) {
return;
}
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
Log.v(mTag, "Laying out " + host + " to (" +
host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
}
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
//...省略代碼
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
View 類中 layout 方法實現如下:
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);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
layout 方法接收四個參數,分別代表著左、上、右、下的坐標,當然這個坐標是相對于當前視圖的父視圖而言的,然后會調用 setFrame 方法來設定 View 的四個頂點的位置,即初始化 mLeft、mRight、mTop、mBottom 這四個值,View 的四個頂點一旦確定,那么 View 在父容器中的位置也就確定了,接著會調用 onLayout 方法,這個方法的用途是父容器確定子元素的位置,和 onMeasure 方法類似
onLayout 源碼如下:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
納尼,怎么是個空方法,沒錯,就是一個空方法,因為 onLayout 過程是為了確定視圖在布局中所在的位置,而這個操作應該是由布局來完成的,即父視圖決定子視圖的顯示位置,我們繼續看 ViewGroup 中的 onLayout 方法
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
可以看到,ViewGroup 中的 onLayout 方法竟然是一個抽象方法,這就意味著所有 ViewGroup 的子類都必須重寫這個方法。像 LinearLayout、RelativeLayout 等布局,都是重寫了這個方法,然后在內部按照各自的規則對子視圖進行布局的。所以呢我們如果要自定義 ViewGroup 那么就要重寫 onLayout 方法。
public class TestViewGroup extends ViewGroup {
public TestViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() > 0) {
View childView = getChildAt(0);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
}
}
xml 中使用
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<com.will.testdemo.customview.TestViewGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_blue_bright"
>
<TextView
android:id="@+id/tv_hello"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorPrimaryDark"
android:text="@string/hello_world"
android:textColor="@android:color/white"
android:textSize="15sp"
/>
</com.will.testdemo.customview.TestViewGroup>
</LinearLayout>
顯示效果如下:
不知道童鞋們發現了沒,我給自定義的 ViewGroup 設置了背景色,看效果貌似占滿全屏了,可是我在 xml 中設置的 wrap_content 啊,這是什么情況,我們回頭看看 ViewGroup 中 View 的 MeasureSpec 的創建規則
從表中可看出因為 ViewGroup 的父布局設置的 match_parent 也就是限制固定值模式,而 ViewGroup 設置的 wrap_content,那么最后 ViewGroup 使用的是 父布局的大小,也就是窗口大小 parentSize,那么如果我們給 ViewGroup 設置固定值就會使用 我們設置的值,來改下代碼。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<com.will.testdemo.customview.TestViewGroup
android:layout_width="200dp"
android:layout_height="100dp"
android:background="@android:color/holo_blue_bright"
>
<TextView
android:id="@+id/tv_hello"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorPrimaryDark"
android:text="@string/hello_world"
android:textColor="@android:color/white"
android:textSize="15sp"
/>
</com.will.testdemo.customview.TestViewGroup>
</LinearLayout>
效果如下:
表中的其他情況,建議童鞋們自己寫下代碼,會理解的更好。
之前說過,一般情況下 View 測量大小和最終大小是一樣的,為什么呢,因為最終大小在 onLayout 中確定,我們來改下代碼:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, childView.getMeasuredWidth()+100, childView.getMeasuredHeight()+200);
}
}
顯示效果
沒錯,onLayout 就是這么任性,所以要獲取 View 的真實大小最好在 onLayout 之后獲取。那么如何來獲取 view 的真實大小呢,可以通過下面的代碼來獲取
tv_hello.post(Runnable {
log(" getMeasuredWidth() = ${tv_hello.measuredWidth}")
log(" getWidth() = ${tv_hello.width}")
}
打印如下:
01-18 23:33:20.947 2836-2836/com.will.testdemo I/debugLog: getMeasuredWidth() = 239
01-18 23:33:20.947 2836-2836/com.will.testdemo I/debugLog: getWidth() = 339
可以看到實際高度和測試的高度是不一樣的,因為我們在 onLayout 中做了修改。
因為 View 的繪制過程和 Activity 的生命周期是不同步的,所以我們可能在 onCreate 中獲取不到值。這里提供幾種方法來獲取
1.Activity 的 onWindowFocusChanged 方法
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus){
//獲取 view 的大小
}
}
2.view.post(runnable) 也就是我上面使用的方法
3.ViewTreeObserver 這里童鞋們搜索下就可以找到使用方法,篇幅較長就不舉例子了
確定了 View 的大小和位置后,那就要開始繪制了,Draw 過程就比較簡單,它的作用是將 View 繪制到屏幕上面。View 的繪制過程遵循如下幾步:
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;
}
//省略代碼..
}
View 的繪制過程的傳遞是通過 dispatchDraw 實現的,dispatchdraw 會遍歷調用所有子元素的 draw 方法,如此 draw 事件就一層一層的傳遞下去。和 Layout 一樣 View 是不會幫我們繪制內容部分的,因此需要每個視圖根據想要展示的內容來自行繪制,重寫 onDraw 方法。具體可參考 TextView 或者 ImageView 的源碼。
View 的工作流程和原理到這就分析完了,難點主要是 MeasureSpec 測量過程,需要童鞋們認真揣摩。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。