該篇分爲上下結構,上部分主要講解 View 的工做原理,下部分主要以案例的形式講解自定義 View。php
ps:該篇文章大部分內容會參考 Android 開發藝術探索。html
在介紹 View 繪製的三大流程以前咱們有必要先了解下 ViewRootImpl 和 DecorView 基本概念,ViewRootImpl 它是鏈接 WindowManager 和 DecorView 的紐帶,View 的三大繪製流程也是在 ViewRootImpl 中完成的。在 ActivityThread 中,當 Activity 建立完成並執行 onCreate 生命週期的調用,在用戶主動調用 setContentView 以後,會初始化 DecorView 實例,DecorView 至關因而整個 Activity 中 View 的父類。ViewRootImpl 是在 ActivityThread 執行 handleResumeActivity 函數以後的 WindowManagerGlobal 中進行初始化的,能夠把它理解爲是 Activity 的 View 樹管理者,最後並將 ViewRootImpl 和 DecorView 創建關聯,這個過程能夠參考下面源碼,代碼以下:java
//ActivityThread#handleResumeActivity -> ViewManager#addView -> WindowManagerGlobal#andView
//WindowManagerGlobal.java
/** * * @param view DecorView * @param params * @param display * @param parentWindow */
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
...
/** * 1. 實例化 ViewRootImpl 對象,並賦值給 root 變量 */
root = new ViewRootImpl(view.getContext(), display);
...
try {
/** * 2. 將 DecorView 和窗口的參數經過ViewRootImpl setView 方法設置到 ViewRootImpl * 中 */
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
...
}
}
}
複製代碼
經過上面代碼咱們能夠知道在 Activity 執行 onResume 生命週期以後,首先會執行上面的代碼邏輯,進行關聯。android
View 的繪製流程是從 ViewRootImpl 的 performTraversals 方法開始的,它通過了 measure 、layout、draw 三個過程才能最終將一個 View 繪製出來,其中 measure 用來測量 View 的寬高,layout 肯定 View 在父容器中的放置位置,draw 就是根據前面 measure 和 layout 的步驟把 View 繪製在屏幕上。針對 performTraversals 的大體流程,能夠看下圖:git
經過上圖咱們知道 ViewRootImpl#performTraversals 會依次調用 performMeasure, performLayout , performDraw 三個方法,這三個方法會分別調用頂級 View 的 measure, layout,draw 方法以完成 View 的繪製工做。因爲在 ViewRootImpl # performTraversals 中的三大方法會在 onMeasure ,onLayout,onDraw 中分別對全部子 View 進行操做,因爲每一步都類似,這裏就拿 onMeasure 測量舉例 ,經過上圖咱們知道在 performMeasure 中會調用 measure 方法,在 measure 方法中又會調用 onMeasure 方法,在 onMeasure 方法中則會對全部的子元素進行 measure 測量過程,這個時候 measure 流程就從父容器傳遞到子元素中了,這樣就完成了一次 measure 過程。接着子元素會重複父容器的 measure 過程,如此反覆就完成了整個 View 樹的遍歷。github
measure: measure 過程決定了 View 的實際寬高, Measure 測量完了以後,能夠經過 getMeasuredWidth/getMeasuredHeight 方法來獲取到 View 測量後的寬高,在幾乎全部的狀況下它都等同於 View 的最終寬高,可是有一種狀況下除外,好比在 Activity 中的 onCreate ,onStart, onResume 中若是直接獲取 View 的寬高,你會發現獲取都是 0,0,爲何會這樣呢?這個後面會進行說明。canvas
Layout: layout 過程決定了 View 的四個頂點的座標和實際的 View 的寬高,完成之後,能夠經過 getTop,getBottom,getLeft,和 getRight 分別獲取對應的值,並經過 getWidth 和 getHeight 方法來拿到 View 最終寬高。app
Draw: draw 過程則決定了 View 的顯示,只有 draw 方法完成之後 View 的內容才能顯示到屏幕上。ide
前面咱們講解了 ViewRootImpl 的基本概念,下面咱們來看下 DecorView 在 Activity 中的做用,先來看一張圖:函數
DecorView 是一個應用窗口的根容器,它本質上是一個 FrameLayout。DecorView 有惟一一個子 View,它是一個垂直 LinearLayout,包含兩個子元素,一個是 TitleView(ActionBar的容器),另外一個是 ContentView(窗口內容的容器)。關於 ContentView,它是一個 FrameLayout(android.R.id.content),咱們日常用的 setContentView 就是設置它的子 View。上圖還表達了每一個 Activity 都與一個 Window(具體來講是PhoneWindow)相關聯,用戶界面則由 Window 所承載。View 層的事件傳遞也都要先通過 DecorView ,而後才傳遞給咱們的 View。
爲了更好的理解 View 的測量過程,咱們還須要理解 MeasureSpec 它的含義,從名字上得出的解譯,看起來像是 「測量規則」 ,其實它就是決定 View 在測量的時候的一個尺寸規格。
MeasureSpec 表明一個 32 位 int 值,高 2 位表明 SpecMode ,低 30 位表明 SpecSize ,SpecMode 是指測量模式,而 SpecSize 是指在某種測量代碼模式下的規格大小,下面先看一下 MeasureSpec 的定義,代碼以下:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
@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);
}
...
}
複製代碼
MeasureSpec 經過將 SpecMode 和 SpecSize 打包成一個 int 值來避免過多的對象內存分配,爲了方便操做,其提供了打包和解包方法。SpecMode 和 SpecSize 也是一個 int 值,一組 SpecMode 和 SpecSize 能夠打包爲一個 MeasureSpec , 而一個 MeasureSpec 能夠經過解包的形式來解出其原始的 SpecMode 和 SpceSize ,須要注意的是這裏提到的 MeasureSpec 是指 MeasureSpec 所表明的 int 值,而並不是 MeasureSpec 自己。
SpecMode 有三類,每一類都表示特殊的含義,以下所示。
UNSPECIFIED: 父容器不對 View 有任何的限制,原始多大就是多大。
EXACTLY: 父容器已經檢測出 View 所須要的精確大小,這個時候 View 的最終大小就是 SpecSize 所指定的值。它對應於 LayoutParams 中的 match_parent 和 具體數值 這 2 中模式。
AT_MOST: 父容器指定了一個可用大小即 SpecSize , View 的大小不能大於這個值,具體是什麼值看不一樣 View 的具體實現。它對應於 LayoutParams 中的 wrap_content 。
在上面提到,系統內部是經過 MeasureSpec 來進行 View 的測量,可是正常狀況下咱們使用 View 指定 MeasureSpec, 儘管如此,可是咱們能夠給 View 設置 LayoutParams 。在 View 測量的時候,系統會將 LayoutParams 在父容器的約束下轉換成對應的 MeasureSpec ,而後在根據這個 MeasureSpec 來肯定 View 的測量後的寬高,須要注意的是,MeasureSpec 不是惟一由 LayoutParams 決定的,LayoutParams 須要和父容器一塊兒才能決定 View 的 MeasureSpec ,從而進一步決定 View 的寬高。另外,對於頂級 View (DecorView) 和普通 View 來講,MeasureSpec 的轉換過程略有不一樣。對於 DecorView ,其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 來共同決定;對於普通 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 來共同決定,MeasureSpec 一旦肯定後,onMeasure 中就能夠肯定 View 的測量寬、高。
對於 DecorView 來講,在 ViewRootImpl 中的 measureHierarchy 方法中有以下一段代碼,它展現了 DecorView 的 MeasureSpec 的建立過程,其中 desiredWindowWidth 和 desiredWindowHeight 是屏幕的尺寸;
//ViewRootImpl.java
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
...
/** * 根據父容器的 size 和 LayoutParsms 寬高來獲得子 View 的測量規格 */
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
複製代碼
咱們在繼續看下 getRootMeasureSpec 的源碼實現,代碼以下:
//ViewRootImpl.java
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
/** *其實就是 XML 佈局中定義的 MATCH_PARENT */
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
/** *其實就是 XML 佈局中定義的 WRAP_CONTENT */
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
/** *其實就是 XML 佈局中定義的 絕對 px */
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
複製代碼
經過上面代碼,DecorView 的 MeasureSpec 的產生過程就很明確了,具體其準守以下規則,根據它的 LayoutParams 中的寬高的參數來劃分。
那麼對於普通 View 也就是 XML 佈局中的 View 是怎麼測量的呢?咱們先來看一下 ViewGroup 的 measureChildWithMargins 方法,由於 View 的 measure 過程就是由它給傳遞過來的,代碼以下:
//ViewGroup.java
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//獲得子元素的 MeasureSpec
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);
//對子View 開始進行 measure
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼
上述方法首先會拿到子 View 的 LayoutParams 佈局中定義的參數,而後根據父容器的 MeasureSpec 、 子元素的 LayoutParams 、子元素的 padding 等而後拿到對子元素的測量規格,能夠看 getChildMeasureSpec 代碼具體實現:
//ViewGroup.java
//padding:指父容器中已佔用的大小
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;
//根據父容器的測量規格和 View 自己的 LayoutParams 來肯定子元素的 MeasureSpec
switch (specMode) {
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製代碼
這一段代碼雖然比較多,可是仍是比較容易理解,它主要的工做就是先拿到在父容器中可用的尺寸,而後根據父元素的測量規格和子元素中 LayoutParams 參數來決定當前子 View 的 MeasureSpec。
上面的代碼,若是用一張表格來表示的話,應該更好理解,請看下錶:
parentSpecMode / childLaoutParams | EXACTLY(精準模式) | AT_MOST(最大模式) | UNSPECIFIED(精準模式) |
---|---|---|---|
dp/px | EXACTLY childSize | EXACTLY childSize | EXACTLY childSize |
match_parent | EXACTLY parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
Warap_content | AT_MOST parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
經過此表能夠更加清晰的看出,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams, 就能夠快速的肯定出子元素的 MeasureSpec 了,有了 MeasureSpec 就能夠進一步肯定出子元素測量後的大小了。
View 的工做流程主要是指 measure、layout 、draw 這三個流程,即 測量 -> 佈局 -> 繪製,其中 measure 肯定 View 測量寬高,layout 肯定 View 的最終寬高和四個頂點的位置,而 draw 則將 View 繪製到屏幕上。
在講解 View 的繪製流程以前,咱們有必要知道 View 的 measure 什麼時候觸發,其實若是對 Activity 生命週期源碼有所瞭解的應該知道,在 onCreate 生命週期中,咱們作了 setContentView 把 XML 中的節點轉爲 View 樹的過程,而後在 onResume 能夠交互的狀態,開始觸發繪製工做,能夠說 Activity 的 onResume 是開始繪製 View 的入口也不爲過,下面看入口代碼:
//ActivityThread.java
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
...
/** * 1. 最終會調用 Activity onResume 生命週期函數 */
r = performResumeActivity(token, clearHide, reason);
...
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
/** * 2. 調用 ViewManager 的 addView 方法 */
wm.addView(decor, l);
} else {
...
} else {
...
}
}
複製代碼
經過上面代碼咱們知道,首先會調用註釋 1 performResumeActivity 方法,其內部會執行 Activity onResume 生命週期方法, 而後會執行將 Activity 全部 View 的父類 DecorView 添加到 Window 的過程,咱們看註釋 2 代碼它調用的是 ViewManager#addView 方法,在講解 WindowManager 源碼的時候,咱們知道了 WindowManager 繼承了 ViewManager 而後它們的實現類就是 WindowManagerImpl 因此咱們直接看它內部 addView 實現:
//WindowManagerImpl.java
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
/** * 委託給 WindowManagerGlobal 來處理 addView */
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
複製代碼
內部處理又交給了 WindowManagerGlobal 對象,咱們繼續跟蹤,代碼以下:
//WindowManagerGlobal.java
/** * * @param view DecorView * @param params * @param display * @param parentWindow */
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
/** * 1. 根據 WindowManager.LayoutParams 的參數來對添加的子窗口進行相應的調整 */
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
...
}
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
...
/** * 2. 實例化 ViewRootImpl 對象,並賦值給 root 變量 */
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
/** * 3. 添加 view 到 mViews 列表中 */
mViews.add(view);
/** * 4. 將 root 存儲在 ViewRootImp 列表中 */
mRoots.add(root);
/** * 5. 將窗口的參數保存到佈局參數列表中。 */
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
/** * 6. 將窗口和窗口的參數經過 setView 方法設置到 ViewRootImpl 中 */
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
複製代碼
咱們直接註釋 6 ViewRootImpl#setView 方法,代碼以下:
//ViewRootImpl.java
/** * We have one child */
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
//1. 請求刷新佈局
requestLayout();
...
}
複製代碼
該類 setView 代碼比較多,咱們直接直接找咱們須要的核心代碼註釋 1 ,咱們看它內部實現,代碼以下:
//ViewRootImpl.java
@Override
public void requestLayout() {
//若是 onMeasure 和 onLayout 工做還沒完成,那麼就不容許調用 執行
if (!mHandlingLayoutInLayoutRequest) {
//檢查線程,是不是主線程
checkThread();
mLayoutRequested = true;
//開始遍歷
scheduleTraversals();
}
}
複製代碼
上面代碼首先是檢查是否能夠開始進入繪製流程,咱們看 checkThread 方法實現,代碼以下:
//ViewRootImpl.java
/** * 檢查當前線程,若是是子線程就拋出異常。 */
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
複製代碼
是否是在程序中這個異常常常碰見?如今知道它是從哪裏拋出來的了吧,下面咱們接着看 scheduleTraversals 方法的實現,代碼以下:
//ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
/** * mChoreographer:用於接收顯示系統的 VSync 信號,在下一幀渲染時控制執行一些操做, * 用於發起添加回調 在 mTraversalRunnable 的 run 中具體實現 */
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
複製代碼
這裏代碼的意思就是若是收到系統 VSYNC 信號,那麼就會在 mTraversalRunnable run 方法執行,代碼以下:
//ViewRootImpl.java
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//1. 刪除當前接收 SYNCBarrier 信號的回調
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//2. 繪製入口
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
複製代碼
在文章的開始部分咱們知道 performTraversals 就是測量 layout draw 入口,那麼咱們繼續看它的實現,代碼以下:
//ViewRootImpl.java
private void performTraversals() {
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
//1. 執行測量流程
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
//2. 執行佈局流程
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
//3. 執行繪製流程
performDraw();
}
//說明 1.
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
//調用 View 的 measure 方法
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
//說明 2.
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
...
final View host = mView;//表明 DecorView
...
//內部在調用 View onLayout
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
//說明 3.
private void performDraw() {
...
//內部經過調用 GPU/CPU 來繪製
draw(fullRedrawNeeded);
...
}
複製代碼
到這裏上面對應的函數會對應調用 View 的 onMeasure -> onLayout -> ondraw 方法,下面咱們就具體來講明下繪製過程。
measure 過程要分狀況來看,若是一個原始的 View ,那麼經過 measure 方法就完成了其測量過程,若是是一個 ViewGroup ,除了完成本身的測量過程外,還會遍歷它全部的子 View 的 measure 方法,各個子元素在遞歸去執行這個流程(有子 View 的狀況),下面針對這兩種狀況分別討論。
//View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
....
}
複製代碼
View 的 measure 過程由其 measure 方法來完成,經過 View#measure 源碼能夠知道 它是被 final 修飾的,那麼就表明了子類不能重寫,經過上面源碼咱們知道在 View#measure 內部又會去調用 onMeasure 方法,咱們接着看它的源碼實現,代碼以下:
//View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製代碼
上面的代碼就作了一件事兒,就是設置測量以後的寬高值,咱們先來看看 getDefaultSize 方法,代碼以下:
//View.java
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//根據 measureSpec 拿到當前 View 的 specMode
int specMode = MeasureSpec.getMode(measureSpec);
//根據 measureSpec 拿到當前 View 的 specSize
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED: //通常用於系統內部的測量過程
result = size;//直接返回傳遞進來的 size
break;
case MeasureSpec.AT_MOST:// wrap_content 模式,大小由子類決定可是不能超過父類
case MeasureSpec.EXACTLY://精準模式
result = specSize;//返回測量以後的大小
break;
}
return result;
}
複製代碼
經過上面代碼能夠看出,getDefaultSize 內部邏輯很少,也比較簡單,對於咱們來講只須要關心 AT_MOST, EXACTLY 這兩種狀況就行,其最終就是返回測量以後的大小。這裏要注意的是這裏測量以後的大小並非最終 View 的大小,最終大小是在 layout 階段肯定的,因此這裏必定要注意。
咱們來看一下 getSuggestedMinimumXXXX() 源碼實現:
//View.java
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
複製代碼
能夠看到 getSuggestedMinimumXXX 內部的代碼意思就是,若是 View 沒有設置背景,那麼返回 android:minWidth 這個屬性所指定的值,這個值能夠爲 0,若是 View 設置了背景,則返回 android:minWidth 和背景的 最小寬度/最小高度 這二者的者中的最大值,它們返回的就是 UNSPECIFIED 狀況下的寬高。
從 getDefaultSize 方法實現來看, View 的寬高由 specSize 決定,因此咱們能夠獲得以下結論:既然 measure 被 final 修飾不能重寫,但是咱們在它內部也發現了新大陸 onMeasure 方法,咱們能夠直接繼承 View 而後重寫 onMeasure 方法並設置自身大小。
這裏在重寫 onMeasure 方法的時候設置自身寬高須要注意一下,若是在 View 在佈局中使用 wrap_content ,那麼它的 specMode 是 AT_MOST 模式,在這種模式下,它的寬高等於 specSize,也就是父類控件空剩餘可使用的空間大小,這種效果和在佈局中使用 match_parent 徹底一致,那麼如何解決這個問題勒,能夠參考下面代碼:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
/** * 說明在佈局中使用了 wrap_content 模式 */
if (widthMeasureSpec == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,mHeight)
}else if (widthMeasureSpec == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,heightSize)
}else if (heightMeasureSpec == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSize,mHeight)
}else {
setMeasuredDimension(widthSize,heightSize)
}
}
複製代碼
在上面代碼中,咱們只須要給 View 指定一個默認的內部寬高(mWidth、mHeight),並在 wrap_content 的時候設置此寬高便可。對於非 wrap_content 情形,咱們就沿用系統的測量值便可。
對於 ViewGroup 來講,初了完成本身的 measure 過程之外,還會去遍歷調用全部的子 View 的 measure 方法,各個元素遞歸去執行這個過程,和 View 不一樣的是 ViewGroup 是一個抽象類,所以它沒有重寫 View 的 onMeasure 方法,可是它定義了一個 measureChild 方法,代碼以下:
//ViewGroup.java
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
//拿到全部子 View
final View[] children = mChildren;
//遍歷子 View
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
//依次對子 View 進行測量
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
複製代碼
上面代碼也很簡單,ViewGroup 在 measure 時,會對每個子元素進行 measure ,以下代碼所示:
//ViewGroup.java
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
//拿到該子 View 在佈局XML中或者代碼中定義的屬性
final LayoutParams lp = child.getLayoutParams();
//經過 getChildMeasureSpec 方法,根據父元素的寬高測量規則拿到子元素的測量規則
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//拿到子元素的測量規則以後傳遞到 View 中,開始 measure 流程
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼
measureChild 代碼邏輯也很容易理解,首先取出設置的 LayoutParams 參數,而後經過 getChildMeasureSpec 方法,根據父元素的寬高測量規格拿到子元素的測量規格,最後將拿到的測量規格直接傳遞給 View#measure 來進行測量。
measure 小總結:
Activity/View#onWindowFocusChanged
、view.post(runnable)
、ViewTreeObserver 的 onGlobalLayoutListener 回調
、手動調用 view.measure(int width,int height)
,最終使用哪一個以實際狀況來定。measure 完以後就是 layout 肯定子 View 的位置,當 ViewGroup 位置肯定之後,它在 onLayout 中會遍歷全部的子元素並調用其 layout 方法,在 layout 方法中 onLayout 方法又會被調用,咱們直接看 View 的 layout 方法,代碼以下:
//View.java
@SuppressWarnings({"unchecked"})
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;
/** * 1. 經過 setFrame 來初始化四個點的位置 */
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
/** * 2. 肯定 子View 位置 */
onLayout(changed, l, t, r, b);
...
}
}
複製代碼
layout 方法大體流程首先會經過 setFrame 方法來設定 View 四個頂點位置,View 的四個頂點一旦確認了那麼就會接着調用 onLayout 方法,這個方法的用途是父容器肯定子元素的位置。
measure 和 layout 過程肯定了以後就該執行繪製的最後一個流程了 draw,它的做用就是將 View 繪製到屏幕上面,View 的繪製過程遵循如下幾點:
下面咱們從源碼的角度來看一下 draw 實現,代碼以下:
//ViewRootImpl.java
private void performDraw() {
...
draw(fullRefrawNeeded);
...
}
private void draw(boolean fullRedrawNeeded) {
....
//使用硬件加速繪製,mView -> DecorView
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
...
//使用 CPU 繪製
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
複製代碼
這裏咱們直接看 CPU 繪製
//ViewRootImpl.java
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) {
...
mView.draw(canvas);
...
}
複製代碼
上面代碼內部會調用 View#draw 方法,咱們直接看內部實現,代碼以下:
//View.java
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;
int saveCount;
if (!dirtyOpaque) {
/** * 1. 繪製背景 */
drawBackground(canvas);
}
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
/** * 2. 調用 ondraw 繪製 View 內容 */
if (!dirtyOpaque) onDraw(canvas);
/** *3. 繪製 View 的子 View */
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
/** * 4. 繪製 View 的裝飾 */
onDrawForeground(canvas);
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
...
}
複製代碼
到目前爲止,View 的繪製流程就介紹完了。根節點是 DecorView,整個 View 體系就是一棵以 DecorView 爲根的View 樹,依次經過遍從來完成 measure、layout 和 draw 過程。而若是要自定義 view ,通常都是經過重寫onMeasure(),onLayout(),onDraw() 來完成要自定義的部分,整個繪製流程也基本上是圍繞着這幾個核心的地方來展開的。
下面咱們將詳細介紹自定義 View 。自定義 View 的做用不用多說,這個你們應該都比較清楚,若是你想作出比較絢麗華彩的 UI 那麼僅僅依靠系統的控件是遠遠不夠的,這個時候就必須經過自定義 View 來實現這個絢麗的效果。自定義 View 是一個綜合性技術體系,它涉及 View 的層次結構、事件分發、和 View 工做原理等,這些技術每一項又都是初學者難以掌握的,因此前面 2 篇文章咱們分別講解了 View 基礎,事件分發以及該篇文章的 View 工做原理等知識,有了這些知識以後再來學習自定義 View 那將面對複雜的 UI 效果也能一一應對了,下面咱們就來認識自定義 View 在該小節末尾也會給出實際例子,以供你們參考。
自定義 View 的分類標準不惟一,這裏則把它分爲四大類,請看下面:
繼承 View 重寫 onDraw 方法
這個方法主要用於實現一些不規則的效果,即這種效果不方便經過佈局的組合方式達到,每每須要靜態或者動態地顯示一些不規則的圖形。很顯然這須要經過繪製的方式實現,即重寫 onDraw 方法,採用這種方式須要本身支持 wrap_content ,而且 padding 也須要本身處理。
繼承 ViewGroup 派生特殊的 Layout
這種方式主要用於實現自定義的佈局,即除了基本系統佈局以外,咱們從新定義一種新的佈局,當某種效果看起來像幾種 View 組合在一塊兒的時候,能夠採用這種方式來實現。採用這種方式稍微複雜一些,須要合適的處理 ViewGroup 的 measure 、佈局這兩個過程,並同時處理子元素的測量和佈局過程。
繼承特定的 View (好比 TextView)
這種方式比較常見,通常是用於擴展某種已有的 View 的功能,好比 TextView ,這種方法比較容易實現。也不須要本身支持 wrap_content 和 padding 等。
繼承特定的 ViewGroup(好比 LinearLayout)
這種方式也比較常見,當某種效果看起來很像幾種 View 組合在一塊兒的時候,能夠採用這種方法來實現。採用這種方法不須要本身處理 ViewGroup 的測量和佈局這 兩個過程,須要注意的這種方法和方法 2 的區別,通常來講方法 2 能實現的效果方式 4 也能實現,兩則的區別在於方法 2 更接近 View 的底層。
該小節主要介紹 自定義 View 過程當中的一些注意事項,這些問題若是處理很差有可能直接致使 View 的正常使用,具體事項以下:
讓 View 支持 wrap_content
這是由於直接繼承 View 或者 ViewGroup 的控件,若是不在 onMeasure 中對 wrap_content 作特殊處理,那麼當外界在佈局中使用 wrap_content 屬性時就沒法達到預期的效果,具體處理能夠參考該篇文章的 View 的 measure 過程
。
若是有必要,讓你的 View 支持 padding
這是由於直接繼承 View 的控件,若是不在 draw 方法中處理 padding ,那麼 padding 屬性時沒法起做用的。另外直接繼承 ViewGroup 的控件須要在 onMeasure 和 onLayout 中考慮 padding 和子元素的 margin 對其形成的影響,否則將致使 padding 和 子元素的 margin 失效。
儘可能不要在 View 中使用 Handler
這是由於 View 內部自己就提供了 post 系列的方法,徹底能夠替代 Handler 的做用,固然除非你很明確須要使用 handler 來發送消息。
View 中若是有線程或者動畫,須要及時中止,參考 View#onDetachedFromWindow
這一條也很好理解,若是有線程或者動畫須要中止時,那麼 onDetachedFromWindow 是一個很好的時機。當包含此 View 的 Activity 退出或者當前 View 被 remove 時,View 的 onDetachedFromWindow 方法會被調用,和此方法對應的是 onAttachedToWindow ,當包含此 View 的 Activity 啓動時,View 的 onAttachedToWindow 方法會被調用,同時,當 View 變得不可見時咱們也須要中止線程和動畫,若是不及時處理這種問題,將有可能會形成內存泄漏。
View 帶有滑動嵌套情形時,須要處理好滑動衝突
若是有滑動衝突的話,那麼就須要合適的處理滑動衝突,不然將嚴重影響 View 的效果,具體處理請看高級 UI 成長之路 (二) 深刻理解 Android 8.0 View 觸摸事件分發機制
下面經過示例代碼來一塊兒學習自定義 View, 下面仍是以自定義分類來具體體現。
繼承 View 重寫 onDraw 方法
爲了更好的展現一些平時不容易注意到的問題,這裏先實現一個很簡單的自定義控件,咱們先繪製一個圓,儘管如此,須要注意的細節仍是不少的,爲了實現一個規範控件,在實現過程必須考慮 wrap_content 模式以及 padding ,同時爲了便捷性,還要對外提供自定義屬性,咱們先來看一下代碼實現,以下:
class CircleView2: View {
val color = Color.RED
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
constructor(context: Context) : super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init()
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
val height = height
val width = width
val radius = Math.min(width, height) / 2f
canvas.drawCircle(width/2f,height/2f,radius,paint)
}
private fun init() {
paint.setColor(color)
paint.isAntiAlias = true
}
}
複製代碼
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
<com.devyk.customview.sample_1.CircleView2 android:layout_width="match_parent" android:layout_height="100dp"/>
</LinearLayout>
複製代碼
上面簡單繪製了一個以當前寬高的一半的最小值在本身的中心點繪製一個紅色的實心圓,其實上面並非一個規範的自定義控件爲何這麼說呢?咱們經過調整佈局參數再來看一下
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
<com.devyk.customview.sample_1.CircleView2 android:layout_width="match_parent" android:background="#000" android:layout_height="100dp"/>
<com.devyk.customview.sample_1.CircleView2 android:layout_width="match_parent" android:background="#9C27B0" android:layout_margin="20dp" android:layout_height="100dp"/>
</LinearLayout>
複製代碼
運行效果如上,能夠看到 margin 屬性是有效果的,這是由於 margin 屬性是由父容器控制的,所以不須要再 CircleView2 中作特殊處理。咱們如今在來調整它的佈局參數,爲其設置 20dp 的 padding,以下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:background="#FF9800" android:layout_width="match_parent" android:layout_height="match_parent">
<com.devyk.customview.sample_1.CircleView2 android:layout_width="match_parent" android:background="#000" android:layout_height="100dp"/>
<com.devyk.customview.sample_1.CircleView2 android:layout_width="match_parent" android:background="#9C27B0" android:layout_margin="20dp" android:layout_height="100dp"/>
<com.devyk.customview.sample_1.CircleView2 android:layout_width="match_parent" android:background="#2196F3" android:layout_margin="20dp" android:padding="20dp" android:layout_height="100dp"/>
</LinearLayout>
複製代碼
運行效果以下:
能夠看到 第三個圓 咱們在佈局中設置了 padding 結果根本沒有無效,這就是咱們在前面提到的直接繼承自 View 和 ViewGroup 的控件,padding 是默認沒法生效的,須要本身處理,咱們在將其寬度設置爲 wrap_content ,以下:
<com.devyk.customview.sample_1.CircleView2 android:layout_width="wrap_content" android:background="#8BC34A" android:layout_margin="20dp" android:padding="20dp" android:layout_height="100dp"/>
複製代碼
運行效果以下:
結果發現 wrap_content 並無達到預期的效果,對比圖上其它的 MATCH_PARENT 屬性繪製的圓其實跟 wrap_content 同樣,其實的確是這樣,這一點在源碼中也講解到了,能夠看下面:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//根據 measureSpec 拿到當前 View 的 specMode
int specMode = MeasureSpec.getMode(measureSpec);
//根據 measureSpec 拿到當前 View 的 specSize
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED: //通常用於系統內部的測量過程
result = size;
break;
case MeasureSpec.AT_MOST:// wrap_content 模式,大小由子類決定可是不能超過父類
case MeasureSpec.EXACTLY://精準模式
result = specSize;
break;
}
return result;
}
複製代碼
其實無論是 AT_MOST 或者 EXACTLY 都是按照 specSize 賦值,大小都是同樣的,因此爲了解決這個問題,咱們須要重寫 onMeasure 而且在 draw 方法中拿到 padding 而後減去該值 ,先來看代碼實現:
/** * 解決 wrap_content */
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthSpecMode = View.MeasureSpec.getMode(widthMeasureSpec)
val widthSpecSize = View.MeasureSpec.getSize(widthMeasureSpec)
val heightSpecMode = View.MeasureSpec.getMode(heightMeasureSpec)
val heightSpecSize = View.MeasureSpec.getSize(heightMeasureSpec)
if (widthSpecMode == View.MeasureSpec.AT_MOST && heightSpecMode == View.MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200)
} else if (widthSpecMode == View.MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize)
} else if (heightSpecMode == View.MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200)
}
}
/** * 解決 padding */
override fun draw(canvas: Canvas) {
super.draw(canvas)
val paddingLeft = paddingLeft
val paddingRight = paddingRight
val paddingBottom = paddingBottom
val paddingTop = paddingTop
val height = height - paddingBottom - paddingTop
val width = width - paddingLeft - paddingRight
val radius = Math.min(width, height) / 2f
canvas.drawCircle(paddingLeft + width/2f,paddingTop + height/2f,radius,paint)
}
複製代碼
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:background="#FF9800" android:layout_width="match_parent" android:layout_height="match_parent">
<com.devyk.customview.sample_1.CircleView2 android:layout_width="match_parent" android:background="#000" android:layout_height="100dp"/>
<com.devyk.customview.sample_1.CircleView2 android:layout_width="match_parent" android:background="#9C27B0" android:layout_margin="20dp" android:layout_height="100dp"/>
<com.devyk.customview.sample_1.CircleView2 android:layout_width="match_parent" android:background="#2196F3" android:layout_margin="20dp" android:padding="20dp" android:layout_height="100dp"/>
<com.devyk.customview.sample_1.CircleView3 android:layout_width="wrap_content" android:background="#8BC34A" android:layout_margin="20dp" android:padding="30dp" android:layout_height="100dp"/>
</LinearLayout>
複製代碼
運行效果以下:
經過咱們在 draw 中減去了 各自的 padding 解決了 padding 的問題,經過重寫 onMeasure 對該 View 設置寬高,解決了 wrap_content 屬性的效果。
最後爲了咱們的自定義控件的擴展性,咱們須要給它實現自定義屬性,步驟以下所示:
//1. 咱們在 values 目錄下建立一個 attrs 開頭的文件夾,而後在建立一個 attrs_circle 的文件,
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_view_color" format="color"></attr>
</declare-styleable>
</resources>
//2. 代碼中進行解析屬性值
private fun initTypedrray(context: Context, attrs: AttributeSet) {
//拿到自定義屬性組
val obtainStyledAttributes = context.obtainStyledAttributes(attrs, R.styleable.CircleView)
color = obtainStyledAttributes.getColor(R.styleable.CircleView_circle_view_color, Color.RED)
obtainStyledAttributes.recycle()
}
//3. 佈局中聲明自定義屬性的空間,在根佈局中添加以下屬性
xmlns:app="http://schemas.android.com/apk/res-auto"
//4. 在自定義 View 中配置該屬性值
<com.devyk.customview.sample_1.CircleView3 android:layout_width="wrap_content" android:background="#8BC34A" android:layout_margin="20dp" app:circle_view_color = "#3F51B5" android:padding="20dp" android:layout_height="100dp"/>
複製代碼
運行效果以下:
自定義屬性也配置完成了。這樣作的好處是在不修改原始代碼的狀況下,可讓用戶自定義顏色值,擴展性比較強。
繼承 ViewGroup 派生特殊的 Layout
咱們先看一下咱們須要實現的效果->流式佈局
定義用於裝 x 軸 View,y 軸 height, 容器中全部子 View 的容器
/** * 定義一個裝全部子 View 的容器 */
protected var mAllViews: MutableList<List<View>> = ArrayList<List<View>>()
/** * 定義行高 */
protected var mLineHeight: MutableList<Int> = ArrayList()
/** * 定義行寬 */
protected var mLineWidth: MutableList<Int> = ArrayList()
/** * 當前行上的子 View 控件 */
protected var mLinViews: MutableList<View> = ArrayList<View>()
複製代碼
重寫 View onMeasure 方法,測量每個子 View 的寬高,而且計算 X 軸上每個子 View 的寬度是否操出總的 width
/** * 1. 肯定全部子 View 的寬高 */
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//根據當前寬高的測量模式,拿到寬高和當前模式
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightsize = MeasureSpec.getSize(heightMeasureSpec)
//若是當前容器 XML 佈局定義的 wrap_content 那麼就須要本身解決實際測量高度
var width = 0
var height = 0
//當前行高/寬
var lineWidth = 0
var lineHeight = 0
//拿到全部子 View 總數
val allViewCount = childCount
//遍歷進行對子 View 進行測量
for (child in 0..allViewCount -1 ){
//拿到當前 View
var childView = getChildAt(child)
//判斷當前 view 是否隱藏狀態
if (childView.visibility == View.GONE) {
//若是是最後一個,拿到當前行高
if (child == allViewCount - 1){
width = Math.max(lineWidth,width)
height += lineHeight
}
continue
}
//對 childView 進行測量
measureChild(childView,widthMeasureSpec,heightMeasureSpec)
//拿到當前子 View 佈局參數
val marginLayoutParams = childView.layoutParams as MarginLayoutParams
//拿到測量以後的寬、高 + 設置的 margin
val childWidth = childView.measuredWidth + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin
val childHeight = childView.measuredHeight + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin
//說明已經放不下
if (lineWidth + childWidth > widthSize - paddingLeft - paddingRight){
//拿到當前行最大的寬值
width = Math.max(width,lineWidth)
//當前行的寬度
lineWidth = childWidth
//子 View 總高度
height += lineHeight
//當前行的高度
lineHeight = childHeight
}else{
//將子 View 的寬度累計相加
lineWidth += childWidth
//拿到當前行最大的高度
lineHeight = Math.max(lineHeight,childHeight)
}
}
//設置當前容器的寬高
setMeasuredDimension(
//判斷是不是 match——parent 模式若是不是,那麼就是 wrap_content 或者 精準 dp 模式,須要全部子 View 寬/高 相加
if (widthMode === MeasureSpec.EXACTLY) widthSize else width + paddingLeft + paddingRight,
if (heightMode === MeasureSpec.EXACTLY) heightsize else height + paddingTop + paddingBottom
)
}
複製代碼
將測量好的子 View 開始放入 ViewGroup 中
/** * 2. 肯定全部子 View 的位置 */
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
//清空容器裏面的數據
mAllViews.clear()
mLineHeight.clear()
mLineWidth.clear()
mLinViews.clear()
//拿到控件的寬
var width = width
//當前行寬
var lineWidth = 0
//當前行高
var lineHeight = 0
//當前 childCount
val childCount = childCount
//遍歷子 View
for (childIndex in 0..childCount-1){
var childView = getChildAt(childIndex)
if(childView.visibility == View.GONE)continue
val marginLayoutParams = childView.layoutParams as MarginLayoutParams
//拿到最後 View 真實寬高
val measuredWidth = childView.measuredWidth
val measuredHeight = childView.measuredHeight
//當前子 View 的寬+ 當前行寬再加當前 margin 若是大於當前總寬的話 說明放不下了,須要換行
if (measuredWidth + lineWidth + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin > width - paddingRight-paddingLeft){
//當前行的最大的高
mLineHeight.add(lineHeight)
//當前行總寬度
mLineWidth.add(lineWidth)
//這裏面裝的是每一行全部的子View,該容器的 size 取決於 有多少行
mAllViews.add(mLinViews)
//將下一行的寬設置爲 0 初始高度
lineWidth = 0
//將下一行的高度初始爲第一個子 View 的高度
lineHeight = measuredHeight
//初始化一個容器,用於裝下一行全部的子 View
mLinViews = ArrayList<View>()
Log.d(TAG,"lineWidth:$lineWidth lineHeight:$lineHeight")
}
//依次加當前 View 佔用的寬
lineWidth += measuredWidth
//找出當前子 View 最大的height
lineHeight = Math.max(lineHeight,measuredHeight + marginLayoutParams.bottomMargin + marginLayoutParams.topMargin)
//將行上的 VIew 添加到容器裏面
mLinViews.add(childView)
Log.d(TAG,"--- lineWidth:$lineWidth lineHeight:$lineHeight")
}
mLineHeight.add(lineHeight)
mLineWidth.add(lineWidth)
mAllViews.add(mLinViews)
var left = paddingLeft
var top = paddingTop
//拿到當前全部的子 VIew
for (curAllView in 0..mAllViews.size -1){
mLinViews = mAllViews.get(curAllView) as ArrayList
lineHeight = mLineHeight.get(curAllView)
val curLinewidth = mLineHeight.get(curAllView)
when(mGravity){
LEFT -> left = paddingLeft
CENTER -> (width - curLinewidth)/2 + paddingLeft
RIGHT -> {
left = width - (curLinewidth + paddingLeft) - paddingRight
Collections.reverse(mLinViews)
}
}
mLinViews.forEach lit@{
if (it.visibility == View.GONE)return@lit
val lp = it.layoutParams as MarginLayoutParams
var lc = left + lp.leftMargin
var tc = top + lp.topMargin
var rc = lc + it.measuredWidth
var bc = tc + it.measuredHeight
Log.d(TAG,"lc:$lc tc:$tc rc:$rc bc:$bc");
//開始放入子 VIew
it.layout(lc,tc,rc,bc)
left += it.measuredWidth + lp.leftMargin + lp.rightMargin
}
top += lineHeight
}
}
複製代碼
添加子 View
mFlowLayout.setAdapter(object : TagAdapter<String>(mVals) {
override fun getView(parent: FlowLayout, position: Int, s: String): View {
val tv = LayoutInflater.from(applicationContext).inflate(
R.layout.tv,
mFlowLayout, false
) as TextView
tv.text = s
Log.d(TAG,s);
return tv
}
})
複製代碼
到這裏就已經將流式佈局繪製出來了,該源碼我參考的是 hongyangAndroid/FlowLayout 上面代碼跟源碼略有不一樣,我這個是 kotlin 版本,須要看全部代碼,請移步源代碼倉庫。
經過學習該案例你將學習到自定義 ViewGroup 的流程和若是測量子 View 及如何放置子 View.建議不會都必定要敲一遍,才能加深對自定義 View 的認識。
到這裏,自定義 View 相關的知識都已經介紹完了,在閱讀該篇文章以前首先要對 View 有一個總體的認識,好比若是在 View 、ViewGroup 中進行 measure ,如何解決 xml 中定義的 wrap_content 和 padding 邊距。繪製流程是如何進行的。我相信看完該篇文章你對 View 的認識會更加深入。
感謝你的閱讀,謝謝!