前言:念念不忘,必有回響,永遠(yuǎn)堅(jiān)持你所堅(jiān)持的!
上一篇對(duì) onMeasure()
方法做了深入分析,說實(shí)在的,自定義 View 的 onMeasure
看那一篇就足夠了。對(duì)于想深入學(xué)習(xí)學(xué)習(xí)源碼的話,后期我也會(huì)更新源碼分析專欄的。onMeasure 方法是最難理解的一個(gè),理解了 onMeasure() 那么 onLayout() 和 onDraw() 就非常簡(jiǎn)單了。本篇就一氣呵成,對(duì) onLayout() 和 onDraw() 進(jìn)行深入分析。這兩個(gè)方法雖然簡(jiǎn)單,但是不太好講,也只能貼一點(diǎn)源碼在這里講了,對(duì)初學(xué)者可能有點(diǎn)不夠友好。結(jié)尾再給個(gè)簡(jiǎn)單案例幫助理解,希望能加深點(diǎn)印象。
你應(yīng)該清楚的是,一個(gè) Activity 通過 setContentView 之后,視圖的展示可以通過如下圖來表示:
- mDecor 是 Activity 的頂層窗體,他是 FramLayout 的子類對(duì)象;
- mContentRoot 是根據(jù)設(shè)置給窗體加載的整個(gè) Activity 可見的視圖,這個(gè)視圖包含標(biāo)題欄(如果主題設(shè)置有標(biāo)題),用于容納我們自定義 layout 的 id 為 content 的容器,mContentRoot 被添加到了頂層窗口 mDecor 中;
- mContentParent 是 mContentRoot 中 id 為 content 的容器,這個(gè)容器就是用來添加我們寫的 layout 布局文件的,mContentParent 是嵌套在 mContentRoot 中,mContentRoot 嵌套在 mDecor。所以在上面第⑥步可以直接調(diào)用 findViewById() 找到 mContentParent。( 跟蹤 findViewById() 方法,發(fā)現(xiàn)調(diào)用的是 PhoneWindow 中 mDecor 這個(gè)頂層窗口的 findViewById() 方法 )
1、onLayout() 分析
你應(yīng)該知道,View 的布局在 ViewRootImpl.performLayout() 發(fā)起的:
ViewRootImpl.performLayout() 請(qǐng)求布局過程
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
...
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}
ViewRootImpl.performLayout() 方法中會(huì)調(diào)用 mView( Activity 根窗口 mDecor )的 layout() 方法,為窗口中所有的子控件安排顯示的位置,由于不同的容器有不同的布局策略,所以在布局之前首先要確定所有子控件的大小,才能適當(dāng)?shù)臑樽涌丶才盼恢茫@就是為什么測(cè)量過程需要在布局過程之前完成。接著我們看看 DecorView 的layout() 方法( layout 方法繼承自 View ):
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;
}
//記錄上一次布局后的左上右下的坐標(biāo)值
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//為控件重新設(shè)置新的坐標(biāo)值,并判斷是否需要重新布局
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//onLayout()方法在View中是一個(gè)空實(shí)現(xiàn),各種容器需要重寫onLayout()方法,為子控件布局
onLayout(changed, l, t, r, b);
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;
}
從上面的代碼中,我們注意到關(guān)于布局有兩個(gè)重要的方法,View.layout() 和 View.onLayout(),這兩個(gè)方法有什么關(guān)系?各自的作用是什么呢?他們都是定義在 View 中的,不同的是 layout() 方法中有很長(zhǎng)一段實(shí)現(xiàn)的代碼,而 onLayout() 確實(shí)一個(gè)空的實(shí)現(xiàn),里面什么事也沒做。
??首先我們要明確布局的本質(zhì)是什么,布局就是為 View 設(shè)置四個(gè)坐標(biāo)值,這四個(gè)坐標(biāo)值保存在View的成員變量 mLeft、mTop、mRight、mBottom 中,方便 View 在繪制(onDraw)的時(shí)候知道應(yīng)該在那個(gè)區(qū)域內(nèi)繪制控件。而我們看到 layout() 方法中實(shí)際上就是為這幾個(gè)成員變量賦值的,所以到底真正設(shè)置坐標(biāo)的是layout()方法,那onLayout()的作用是什么呢?
??onLayout()都是由ViewGroup的子類實(shí)現(xiàn)的,他的作用就是確定容器中每個(gè)子控件的位置,由于不同的容器有不容的布局策略,所以每個(gè)容器對(duì)onLayout()方法的實(shí)現(xiàn)都不同,onLayout()方法會(huì)遍歷容器中所有的子控件,然后計(jì)算他們左上右下的坐標(biāo)值,最后調(diào)用child.layout()方法為子控件設(shè)置坐標(biāo);由于layout()方法中又調(diào)用了onLayout()方法,如果子控件child也是一個(gè)容器,就會(huì)繼續(xù)為它的子控件計(jì)算坐標(biāo),如果child不是容器,onLayout()方法將什么也不做,這樣下來,只要Activity根窗口mDecor的layout()方法執(zhí)行完畢,窗口中所有的子容器、子控件都將完成布局操作。
其實(shí)布局過程的調(diào)用方式和測(cè)量過程是一樣的,ViewGroup的子類都要重寫onMeasure()方法遍歷子控件調(diào)用他們的measure()方法,measure()方法又會(huì)調(diào)用onMeasure()方法,如果子控件是普通控件就完成了測(cè)量,如果是容器將會(huì)繼續(xù)遍歷其孫子控件。
繼續(xù)查看DecorView.onLayout()方法:
DecorView .onLayout()
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
getOutsets(mOutsets);
if (mOutsets.left > 0) {
offsetLeftAndRight(-mOutsets.left);
}
if (mOutsets.top > 0) {
offsetTopAndBottom(-mOutsets.top);
}
}
DecorView是FrameLayout的子類,F(xiàn)rameLayout又是ViewGroup的子類,F(xiàn)rameLayout重寫了onLayout()方法,DecorView也重寫了onLayout()方法,但是調(diào)用的是super.onLayout(),然后做了一些邊界判斷,下面我們看FrameLayout.onLayout():
FrameLayout .onLayout()
@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) {
//獲取子控件數(shù)量
final int count = getChildCount();
//獲取padding值
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
//遍歷子控件,為其計(jì)算左上右下坐標(biāo),由于不同容器的布局特性,下面的計(jì)算過程都是根據(jù)容器的布局特性計(jì)算的
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;
}
//調(diào)用其layout()方法為子控件設(shè)置坐標(biāo)
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
所有的布局容器的onLayout方法都是一樣的流程,都是先遍歷子控件,然后計(jì)算子控件的坐標(biāo),最后調(diào)用子控件的layout()方法設(shè)置布局坐標(biāo),但是不同的布局容器有不同的布局策略,所以區(qū)別就在于計(jì)算子控件坐標(biāo)時(shí)的差異。比如LinearLayout線性布局,如果是水平布局,第一個(gè)子控件的l值是0,r是100,那第二個(gè)子控件的l就是101(只是打個(gè)比方),而FrameLayout,如果沒有設(shè)置padding,子控件也沒設(shè)置margin,第一個(gè)子控件的l值就是0,第二個(gè)子控件的l還是0,這就是不同容器的計(jì)算區(qū)別。
FrameLayout.onLayout()方法執(zhí)行完畢后,整個(gè)Activity的根窗口的布局過程也就完成了。接下來進(jìn)入第三個(gè)過程–繪制過程:
onDraw()分析
ViewRootImpl.performDraw()控件繪制過程:
你應(yīng)該清楚,View的繪制起點(diǎn)在ViewRootImpl.performDraw()開始的。
private void performDraw() {
...
mIsDrawing = true;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
...
}
private void draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
final Rect dirty = mDirty;
...
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
...
//使用硬件渲染
mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);
} else {
...
// 通過軟件渲染.
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
}
...
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
final Canvas canvas;
try {
...
canvas = mSurface.lockCanvas(dirty);
...
} catch (Surface.OutOfResourcesException e) {
return false;
} catch (IllegalArgumentException e) {
return false;
}
...
try {
...
try {
...
mView.draw(canvas);
...
} finally {
...
}
} finally {
...
}
return true;
}
ViewRootImpl的performDraw()方法調(diào)用draw(boolean),在這個(gè)過程中主要完成一些條件判斷,surface的設(shè)置準(zhǔn)備,以及判斷使用硬件渲染還是軟件渲染等操作,由于我們主要研究繪制代碼流程層面,所以直接看drawSoftware()方法,對(duì)于硬件渲染具體是怎樣的有興趣可以跟蹤一下。drawSoftware()方法中,通過mSurface.locakCanvas(dirty)拿到畫布,然后調(diào)用mView.draw(canvas),這里的mView就是Activity的根窗口DecorView類型的對(duì)象。
DecorView.draw()
public void draw(Canvas canvas) {
super.draw(canvas);
if (mMenuBackground != null) {
mMenuBackground.draw(canvas);
}
}
DecorView重寫了View的draw()方法,增加了繪制菜單背景的內(nèi)容,因?yàn)锳ctivity根窗口上會(huì)有一些菜單按鈕(比如屏幕下方的返回鍵等),draw()方法中調(diào)用了super.draw(cancas),所以我們看看View的draw()方法:
View.draw()
public void draw(Canvas canvas) {
...
/*
* 繪制遍歷執(zhí)行幾個(gè)繪圖步驟,必須以適當(dāng)?shù)捻樞驁?zhí)行:
* 1.繪制背景
* 2.如果有必要,保存畫布的圖層,以準(zhǔn)備失效
* 3.繪制視圖的內(nèi)容
* 4.繪制子控件
* 5.如果必要,繪制衰落邊緣和恢復(fù)層
* 6.繪制裝飾(比如滾動(dòng)條)
*/
// Step 1, 繪制背景
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 通常情況請(qǐng)?zhí)^2和5步
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, 繪制本控件的內(nèi)容
if (!dirtyOpaque) onDraw(canvas);
// Step 4, 繪制子控件
dispatchDraw(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);
// we're done...
return;
}
//下面的代碼是從第一步到第六步的完整流程
...
}
可以看到,第一步 // Step 1, 繪制背景,這一步的作用是對(duì)視圖的背景進(jìn)行繪制。這里會(huì)先得到一個(gè)mBGDrawable對(duì)象:
然后根據(jù)layout過程確定的視圖位置來設(shè)置背景的繪制區(qū)域,之后再調(diào)用Drawable的draw()方法來完成背景的繪制工作。那么這個(gè)mBGDrawable對(duì)象是從哪里來的呢?其實(shí)就是在XML中通過android:background屬性設(shè)置的圖片或顏色。當(dāng)然你也可以在代碼中通過setBackgroundColor()、setBackgroundResource()等方法進(jìn)行賦值。
接下來的第三步是在第34行執(zhí)行的,這一步的作用是對(duì)視圖的內(nèi)容進(jìn)行繪制。可以看到,這里去調(diào)用了一下onDraw()方法,那么onDraw()方法里又寫了什么代碼呢?進(jìn)去一看你會(huì)發(fā)現(xiàn),原來又是個(gè)空方法啊。其實(shí)也可以理解,因?yàn)槊總€(gè)視圖的內(nèi)容部分肯定都是各不相同的,這部分的功能交給子類來去實(shí)現(xiàn)也是理所當(dāng)然的。
第三步完成之后緊接著會(huì)執(zhí)行第四步,這一步的作用是對(duì)當(dāng)前視圖的所有子視圖進(jìn)行繪制。但如果當(dāng)前的視圖沒有子視圖,那么也就不需要進(jìn)行繪制了。因此你會(huì)發(fā)現(xiàn)View中的dispatchDraw()方法又是一個(gè)空方法,而ViewGroup的dispatchDraw()方法中就會(huì)有具體的繪制代碼。
以上都執(zhí)行完后就會(huì)進(jìn)入到第六步,也是最后一步,這一步的作用是對(duì)視圖的滾動(dòng)條進(jìn)行繪制。那么你可能會(huì)奇怪,當(dāng)前的視圖又不一定是ListView或者ScrollView,為什么要繪制滾動(dòng)條呢?其實(shí)不管是Button也好,TextView也好,任何一個(gè)視圖都是有滾動(dòng)條的,只是一般情況下我們都沒有讓它顯示出來而已。繪制滾動(dòng)條的代碼邏輯也比較復(fù)雜,這里就不再貼出來了,因?yàn)槲覀兊闹攸c(diǎn)是第三步過程。
通過以上流程分析,相信大家已經(jīng)知道,View是不會(huì)幫我們繪制內(nèi)容部分的,因此需要每個(gè)視圖根據(jù)想要展示的內(nèi)容來自行繪制。如果你去觀察TextView、ImageView等類的源碼,你會(huì)發(fā)現(xiàn)它們都有重寫onDraw()這個(gè)方法,并且在里面執(zhí)行了相當(dāng)不少的繪制邏輯。繪制的方式主要是借助Canvas這個(gè)類,它會(huì)作為參數(shù)傳入到onDraw()方法中,供給每個(gè)視圖使用。Canvas這個(gè)類的用法非常豐富,基本可以把它當(dāng)成一塊畫布,在上面繪制任意的東西。
對(duì)于有孩子控件的ViewGroup,需要層級(jí)繪制:
protected void dispatchDraw(Canvas canvas) {
...
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
...
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
}
...
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
dispatchDraw()方法中首先獲取子控件的數(shù)量childrenCount,然后遍歷所有子控件,調(diào)用drawChild()方法,drawChild()方法中只是簡(jiǎn)單的調(diào)用child.draw(),這樣就完成了子控件的繪制。細(xì)心的你可以發(fā)現(xiàn)child的draw()方法又會(huì)執(zhí)行之前DecorView.draw()的六步(draw()是在View里面實(shí)現(xiàn)的),所以說,所有的控件在繪制的時(shí)候都會(huì)調(diào)用draw()方法,draw()方法中會(huì)先調(diào)用onDraw()方法繪制自己,然后調(diào)用dispatchDraw()繪制它的子控件(如果有孩子的話),如果此控件不是ViewGroup的子類,也就是說是葉子控件,dispatchDraw()`什么也不做。
那么我們?cè)谧远x自己的View 的時(shí)候我們應(yīng)該怎么做呢?下面一張圖能更好的幫助你理解:
講了這么多的理論,接下來就做一點(diǎn)小小的實(shí)戰(zhàn)吧,來幫助學(xué)習(xí)上面的理論。分別對(duì)應(yīng)onLayout和onMeasure:
onLayout簡(jiǎn)單案例:
自定義的這個(gè)布局目標(biāo)很簡(jiǎn)單,只要能夠包含一個(gè)子視圖,并且讓子視圖正常顯示出來就可以了。那么就給這個(gè)布局起名叫做SimpleLayout吧,代碼如下所示:
public class SimpleLayout extends ViewGroup {
public SimpleLayout(Context context, AttributeSet attrs) {
super(context, 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());
}
}
}
代碼非常的簡(jiǎn)單,我們來看下具體的邏輯吧。你已經(jīng)知道,onMeasure()方法會(huì)在onLayout()方法之前調(diào)用,因此這里在onMeasure()方法中判斷SimpleLayout中是否有包含一個(gè)子視圖,如果有的話就調(diào)用measureChild()方法來測(cè)量出子視圖的大小。
接著在onLayout()方法中同樣判斷SimpleLayout是否有包含一個(gè)子視圖,然后調(diào)用這個(gè)子視圖的layout()方法來確定它在SimpleLayout布局中的位置,這里傳入的四個(gè)參數(shù)依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分別代表著子視圖在SimpleLayout中左上右下四個(gè)點(diǎn)的坐標(biāo)。其中,調(diào)用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中測(cè)量出的寬和高。
這樣就已經(jīng)把SimpleLayout這個(gè)布局定義好了,下面就是在XML文件中使用它了,如下所示:
<com.example.viewtest.SimpleLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher"
/>
</com.example.viewtest.SimpleLayout>
可以看到,我們能夠像使用普通的布局文件一樣使用SimpleLayout,只是注意它只能包含一個(gè)子視圖,多余的子視圖會(huì)被舍棄掉。這里SimpleLayout中包含了一個(gè)ImageView,并且ImageView的寬高都是wrap_content。現(xiàn)在運(yùn)行一下程序,結(jié)果如下圖所示:
OK!ImageView成功已經(jīng)顯示出來了,并且顯示的位置也正是我們所期望的。如果你想改變ImageView顯示的位置,只需要改變childView.layout()方法的四個(gè)參數(shù)就行了。
在onLayout()過程結(jié)束后,我們就可以調(diào)用getWidth()方法和getHeight()方法來獲取視圖的寬高了。說到這里,我相信很多朋友長(zhǎng)久以來都會(huì)有一個(gè)疑問,getWidth()方法和getMeasureWidth()方法到底有什么區(qū)別呢?它們的值好像永遠(yuǎn)都是相同的。其實(shí)它們的值之所以會(huì)相同基本都是因?yàn)椴季衷O(shè)計(jì)者的編碼習(xí)慣非常好,實(shí)際上它們之間的差別還是挺大的。
首先getMeasureWidth()方法在measure()過程結(jié)束后就可以獲取到了,而getWidth()方法要在layout()過程結(jié)束后才能獲取到。另外,getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進(jìn)行設(shè)置的,而getWidth()方法中的值則是通過視圖右邊的坐標(biāo)減去左邊的坐標(biāo)計(jì)算出來的。
public final int getWidth(){
return mRight - mLeft;//視圖右邊的坐標(biāo)減去左邊的坐標(biāo)
}
觀察SimpleLayout中onLayout()方法的代碼,這里給子視圖的layout()方法傳入的四個(gè)參數(shù)分別是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),因此getWidth()方法得到的值就是childView.getMeasuredWidth() - 0 = childView.getMeasuredWidth() ,所以此時(shí)getWidth()方法和getMeasuredWidth() 得到的值就是相同的,但如果你將onLayout()方法中的代碼進(jìn)行如下修改:
@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, 200, 200);
}
}
這樣getWidth()方法得到的值就是200 - 0 = 200,不會(huì)再和getMeasuredWidth()的值相同了。當(dāng)然這種做法充分不尊重measure()過程計(jì)算出的結(jié)果,通常情況下是不推薦這么寫的。getHeight()與getMeasureHeight()方法之間的關(guān)系同上,就不再重復(fù)分析了。
onDraw簡(jiǎn)單案例:
這里簡(jiǎn)單起見,我只是創(chuàng)建一個(gè)非常簡(jiǎn)單的視圖,并且用Canvas隨便繪制了一點(diǎn)東西,代碼如下所示:
public class MyView extends View {
private Paint mPaint;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.YELLOW);
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
mPaint.setColor(Color.BLUE);
mPaint.setTextSize(20);
String text = "Hello View";
canvas.drawText(text, 0, getHeight() / 2, mPaint);
}
}
可以看到,我們創(chuàng)建了一個(gè)自定義的MyView繼承自View,并在MyView的構(gòu)造函數(shù)中創(chuàng)建了一個(gè)Paint對(duì)象。Paint就像是一個(gè)畫筆一樣,配合著Canvas就可以進(jìn)行繪制了。這里我們的繪制邏輯比較簡(jiǎn)單,在onDraw()方法中先是把畫筆設(shè)置成黃色,然后調(diào)用Canvas的drawRect()方法繪制一個(gè)矩形。然后在把畫筆設(shè)置成藍(lán)色,并調(diào)整了一下文字的大小,然后調(diào)用drawText()方法繪制了一段文字。
就這么簡(jiǎn)單,一個(gè)自定義的視圖就已經(jīng)寫好了,現(xiàn)在可以在XML中加入這個(gè)視圖,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.viewtest.MyView
android:layout_width="200dp"
android:layout_height="100dp"
/>
</LinearLayout>
將MyView的寬度設(shè)置成200dp,高度設(shè)置成100dp,然后運(yùn)行一下程序,結(jié)果如下圖所示:
圖中顯示的內(nèi)容也正是MyView這個(gè)視圖的內(nèi)容部分了。由于我們沒給MyView設(shè)置背景,因此這里看不出來View自動(dòng)繪制的背景效果。
到此為止,我們把視圖繪制流程的第三階段也分析完了。整個(gè)視圖的繪制過程就全部結(jié)束了,你現(xiàn)在是不是對(duì)View的理解更加深刻了呢?