系列文章傳送門 (持續更新中..) :
自定義控制元件(一) Activity的構成(PhoneWindow、DecorView)
在上一篇中我們詳細分析了 View 工作三大流程中最複雜的 measure 流程, 掌握了 measure 流程後, layout 和 draw 流程就相對比較簡單些了。
- 在佈局流程中, 當 ViewGroup 的
layout
方法被父容器呼叫後它的位置將被確定下來, 然後它在onLayout
中遍歷所有的子元素並呼叫它的layout
方法對子元素進行擺放, 而在 layout 中onLayout
方法又被呼叫, 如此反覆直到佈局完成。
簡單講:layout 方法確定 View 本身的位置, onLayout 確定所有子元素的位置
layout 過程 :
在看原始碼之前,先提出一個問題, View 的 getWidth()
和 getMeasuredWidth()
有什麼區別?
public void layout(int l, int t, int r, int b) {
...
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);
...
}
複製程式碼
layout 方法中, 先呼叫 setFrame()
給自己的四個頂點賦值, 就確定了自己的位置。然後呼叫 onLayout
方法對子元素進行擺放
protected boolean setFrame(int left, int top, int right, int bottom) {
...
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
...
}
複製程式碼
看到沒,mLeft、mTop 、mRight 、mBottom
就是這個 View 的四個頂點,當四個頂點的值唄確定,View 的位置就擺放完了。
由於在 View 中 onLayout()
方法是空實現, ViewGroup 的 onLayout()
是抽象方法, 所以就挑一個 ViewGroup 常用的子類 FrameLayout 看一下 (其它都類似, 自己可以去看下):
#FrameLayout - onLayout
protected void onLayout(boolean changed, int left, int top, int right, int bottom){
layoutChildren(left, top, right, bottom, false);
}
複製程式碼
onLayout
的引數直接傳給 layoutChildren
,繼續走:
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
...
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();
...
child.layout(childLeft, childTop, childLeft + width, childTop + height);
...
複製程式碼
很明顯, 在 layoutChildren 遍歷所有的子元素, 並呼叫其 layout 方法來擺放子元素,這樣父容器在 layout 方法中完成自己的擺放後,通過 onLayout 方法去遍歷呼叫子元素的 layout 方法,子元素又會通過 layout 方法確定自己的位置,這樣一層一層傳遞下去從而完成整個 View 樹的 layout 過程。
- 注意看呼叫 layout 傳的引數,這裡傳入的
width
和height
, 其實就是這個 view 的測量寬/高。前面分析了 layout 中傳入的引數會對 View 的四個頂點(mLeft、mTop 、mRight 、mBottom)
賦值來確定位置,這裡我們來看一下getWidth()
的返回值:
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
複製程式碼
-
結合我們剛剛的原始碼分析不難看出來,mRight - mLeft 和 mBottom - mTop 的返回值不就分別是 view 的測量寬高麼, 所以在系統 View 的預設實現中,以及開發中我們可以直接認為 getWidth() = getMeasuredWidth(),只是賦值時間不同, getHeight 和 getMeasuredHeight 同理,
getMeasuredWidth()
是在onMeasure()
方法中執行完成測量流程後並儲存尺寸的時候被賦值,getWidth()
是在layout
方法中確定自己位置的時候被賦值。 -
當然也存在兩種情況會出現不相等:一種是某些極端情況系統需要多次執行measure流程,這時則除了最後一次measure,前幾次的measure結果就可能存在不相等。另一種則是在 onLayout() 中呼叫 layout 時, 對傳入的四個頂點值做了一些運算處理, 則這兩個值也是不相等的,如下
protected void onLayout(boolean changed, int left, int top, int right, int bottom){
...
child.layout(childLeft, childTop, childLeft + width + 100, childTop + height + 100);
...
}
// 或重寫 layout 方法
public void layout(int l, int t, int r, int b) {
super.layout(l, t, r + 100, b + 100);
}
複製程式碼
draw 過程 :
繪製過程就是將 View 繪製到螢幕上, 它分為4步:
- (1) 繪製背景(私有方法不能重寫)
- (2) 主體繪製(一般重寫此方法)
- (3) 繪製子元素
- (4) 繪製前景和滑動相關(繪製前景的支援是在 Android 6.0 之後)
具體從 draw
方法中可以明瞭的看出來:
public void draw(Canvas canvas) {
...
drawBackground(canvas);
...
onDraw(canvas);
...
dispatchDraw(canvas);
...
onDrawForeground(canvas);
...
}
複製程式碼
繪製過程的傳遞是通過 dispatchDraw
來實現,dispatchDraw
中會遍歷所有子元素的 draw
方法,如此反覆下去直到繪製完成。
setWillNotDraw :
這是 View 的一個特殊方法,具體看原始碼:
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
複製程式碼
從註釋大致能看出來,如果一個 View 不需要對自己進行任何繪製,設定這個標誌位為 true 後,系統會進行相應的優化,即繞過 draw()
方法,換而直接執行 dispatchDraw()
,以此來簡化繪製流程。預設情況下 View 沒有設定這個標誌位, 而 ViewGroup 預設會啟動這個標誌位。
- 在實際開發中, 如果自定義控制元件繼承於 ViewGroup 並且本身不具備繪製功能時,就可以開啟這個標誌讓系統進行繪製優化。 但是當明確知道一個 ViewGroup 需要在它的除
dispatchDraw()
以外的任何一個繪製方法內繪製內容,你顯示的關閉這個 WILL_NOT_DRAW 這個標誌位:View.setWillNotDraw(false)
;
覺得有用的話,點個贊再走唄~