Android自定義View系列
- Android自定義View之Paint繪製文字和線
- Android自定義View注意事項
- Android自定義View之Canvas
- Android自定義View之影像的色彩處理
- Android自定義View之圖片外形特效——輕鬆實現圓角和圓形圖片
- Android自定義View之雙緩衝機制和SurfaceView
- Android自定義View之invalidate方法和postInvalidate方法
- Android自定義View之requestLayout方法和invalidate方法
View的三大流程指的是measure(測量)、layout(佈局)、draw(繪製)。
下面我們來分別看看這三大流程
View的measure(測量)
MeasureSpec
MeasureSpec是View的一個內部靜態類
//view.class
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
...
/**
* 這種模式不用關心
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* 精確模式,對應的是match_parent和具體值,比如100dp
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* 最大模式,對應的就是wrap_content
*/
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);
}
}
/**
* 獲取測量的模式
*/
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
/**
* 獲取測量到的尺寸大小
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
...
}
複製程式碼
MeasureSpec總結起來就是:
- 它由2部分資料組成,分別為定義了View測量的模式和View的測量尺寸大小
- 其中EXACTLY精確模式表示的是match_parent和具體值;AT_MOST最大模式表示的是wrap_content的情況
View的measure過程
View的measure過程由其measure方法完成,在measure方法中會呼叫View的onMeasure方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製程式碼
setMeasuredDimension方法會設定View的測量寬高,所以我們知道getDefaultSize方法返回的就是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;
//對應的是wrap_content
case MeasureSpec.AT_MOST:
//對應的是match_parent和具體值,返回的是測量值
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
複製程式碼
從getDefaultSize方法我們可以看到,無論是測量模式無論是AT_MOST還是EXACTLY,返回的結果都是specSize這個測量後的大小。當View的測量模式是AT_MOST,也就是我們在佈局中給View設定的是wrap_content時,這個specSize實際上是父容器中的可用大小,也就相當於是和match_parent是一樣的效果了。所以我們在通過繼承View來自定義View時,就需要特別處理wrap_content的情況。
ViewGroup的measure過程
對於ViewGroup來說,除了完成自己的測量,還需要完成子元素的測量。ViewGroup是一個抽象類,為了測量子類,它提供了一個measureChildren方法:
//ViewGroup.class
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();
//用子元素的LayoutParams構建MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製程式碼
可以看出ViewGroup的measureChildren方法最終會迴圈呼叫子元素的measure方法完成子元素的測量。
ViewGroup並沒有定義自己的測量過程,因為它的測量過程要由子類自己完成,比如LinearLayout和RelativeLayout,顯然測量過程是不同的。有興趣的可以看看LinearLayout的onMearsure方法。
常見的在Activity中獲取View的寬高的方法
View的measure過程和Activity生命週期方法是不同步的,需要用特殊的方法才能準確獲取View的寬高
(1)onWindowFocusChanged方法中獲取
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = myView.getMeasuredWidth();
int height = myView.getMeasuredHeight();
}
}
複製程式碼
需要注意的是onWindowFocusChanged方法會被呼叫多次
(2)view.post(runnable)
通過view的post方法可以將一個runnable投遞到訊息佇列的尾部,當Looper呼叫此runnable時,View已經初始化好了
myView.post(new Runnable() {
@Override
public void run() {
int width = myView.getMeasuredWidth();
int height = myView.getMeasuredHeight();
}
});
複製程式碼
(3)ViewTreeObserver
利用ViewTreeObserver的OnGlobalLayoutListener回撥介面,當View樹發生狀態改變時會回撥這個介面
ViewTreeObserver viewTreeObserver = myView.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
myView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = myView.getMeasuredWidth();
int height = myView.getMeasuredHeight();
}
});
複製程式碼
View的layout(佈局)
layout的作用就是ViewGroup用來確定子元素的位置,ViewGroup的位置被確定後,就會呼叫onLayout方法,遍歷所有的子元素並呼叫其layout方法,在layout方法中又會呼叫onLayout方法。layout方法確定View本身的位置,而onLayout方法用來確定子元素的位置。
//View.class
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;
//確定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);
...
}
...
}
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
複製程式碼
layout方法主要就做了2件事,一個是呼叫setFrame方法確定自身的位置,另一個就是呼叫onLayout方法確定子元素的位置。
我們看到在View中並沒有實現onLayout方法,同樣的在ViewGroup中也沒有實現onLayout方法,這是因為onLayout的具體實現同樣和具體的佈局有關,所以需要子類根據具體情況去實現。大家有興趣可以看看LinearLayout的onLayout的實現。
需要注意的是預設情況下測量的寬高和最終的寬高是一樣的,也就是getMeasuredWidth和getWidth是一樣的。只不過一個獲取的是measure過程後得到的寬高,一個是layout過程後的寬高。所以如果measure過程需要進行多次或是認為改變了layout方法,就有可能2者不相等。不過絕大多數都是一樣的。
View的draw(繪製)
Draw說白了就是把View的內容繪製到螢幕上
@CallSuper
public void draw(Canvas canvas) {
...
/*
* 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);
}
...
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
//呼叫onDraw方法繪製內容
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
//呼叫dispatchDraw方法繪製子元素
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;
}
...
}
複製程式碼
從上面的draw方法中,我們可以看出,繪製過程遵循如下幾步:
(1)繪製背景:drawBackground(Canvas canvas)
(2)繪製自身內容:onDraw(canvas)
(3)繪製子元素:dispatchDraw(canvas)
(4) 繪製裝飾:onDrawForeground(canvas)
View的三大流程開始的地方---ViewRootImpl
上面這張圖是一張很經典的圖,很好的描述了View的繪製流程。ViewRootImpl中的performTraversals方法會呼叫performMeasure、performLayout、performDraw方法,開始View的測量、佈局和繪製過程。那ViewRootImpl中的performTraversals方法又是在什麼時候被呼叫的呢?這就需要理解一個視窗的概念,也就是Window。
Android中的Window
Window是一個抽象的概念,每一個Window都對應著一個View和ViewRootImpl,Window通過ViewRootImpl來和View建立聯絡。Android中所有的檢視都是通過都是通過Window來呈現的,不管是Activity、Dialog、還是Toast,它們的View都是附加在Window上的,因此Window實際上是View的直接管理者。比如我們觸控螢幕的事件,就是通過Window傳遞給DecorView,然後再由DecorView傳遞給我們的View。我們在Activity、Dialog中設定檢視內容的方法setContentView在底層也是通過Window來完成的
Window的新增過程
我們在啟動一個Activity或是一個Dialog時,系統都會為我們建立一個Window,並把建立的Window註冊到系統的WindowManagerService中。
Window的新增過程需要通過WindowManager的實現類WindowManagerImpl的addView方法來實現。只有在通過addView方法將View新增到Window中後,我們的View才和Window關聯起來,才能接收通過Window傳遞的各種輸入資訊
//WindowManagerImpl.class
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
複製程式碼
由WindowManagerImpl的原始碼我們發現,WindowManagerImpl把新增View的工作都交給了WindowManagerGlobal類處理。我們來簡單看看WindowManagerGlobal類
//WindowManagerGlobal.class
//所有Window所對應的View
private final ArrayList<View> mViews = new ArrayList<View>();
//所有Window對應的ViewRootImpl
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
//多有Window對應的佈局引數
private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>();
//正在被刪除的View
private final ArraySet<View> mDyingViews = new ArraySet<View>();
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
//建立ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
//將Window的一系列物件新增到對應的列表中
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
//呼叫ViewRootImpl的setView方法
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
複製程式碼
原始碼中已經做了相應的註釋了。這裡我們看到WindowManagerGlobal在addView方法中建立ViewRootImpl後,最後呼叫了ViewRootImpl的setView方法。下面我們來看看ViewRootImpl的setView方法
//ViewRootImpl.class
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
...
//呼叫了requestLayout進行View的繪製
requestLayout();
...
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
//這裡呼叫WindowSession的addToDisplay方法註冊Window
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
mAdded = false;
mView = null;
mAttachInfo.mRootView = null;
mInputChannel = null;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
throw new RuntimeException("Adding window failed", e);
} finally {
if (restore) {
attrs.restore();
}
}
...
}
}
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
複製程式碼
在ViewRootImpl的setView方法中主要是做了2件事,一個是呼叫requestLayout方法啟動View的繪製流程;另一個是呼叫WindowSession的addToDisplay方法請求WindowManagerService新增Window,這是一次IPC呼叫。
至此,我們就分析完Window的新增過程了,總結如下:
(1)View的展示以及處理觸控點選事件離不開Window,2者通過ViewRootImpl進行關聯
(2)我們在啟動Activity、建立Dialog或是彈出Toast時都會建立一個Window,然後會通過WindowManagerGlobal類的addView方法來建立ViewRootImpl類,並將Window、View、ViewRootImpl關聯起來,
(3)在建立完ViewRootImpl後,接著會呼叫ViewRootImpl的setView方法,在setView方法中通過requestLayout方法最終呼叫到performTraversals方法開啟View的三大流程;通過WindowSession的addToDisplay方法向WindowManagerService發起遠端IPC呼叫,完成Window的新增。
總結
(1)在通過繼承View的方式自定義View時,需要特別處理wrap_content的情況,因為View中預設相當於沒處理(和match_parent效果一樣)
(2)在Activity中獲取View的寬高需要用特殊的方式:onWindowFocusChanged、view.post(runnable)、ViewTreeObserver的OnGlobalLayoutListener
(3)我們的View的顯示離不來Window,無論是Activity、Dialog還是Toast,都對應著一個Window。View和Window通過ViewRootImpl來建立關聯。我們顯示、更新、隱藏介面,比如Dialog的show和dismiss,說到底是Window中新增、更新和刪除View的過程。
(4)我們通過setContentView方法新增View,其實是對應Window的新增View的過程,Window會建立ViewRootImpl來執行註冊Window、開啟View的繪製流程的操作。
(5)所以綜上,我們顯示一個介面的過程為:建立Window-->建立ViewRootImpl-->新增View-->繪製View、註冊Window
歡迎關注我的微信公眾號,和我一起每天進步一點點!
複製程式碼