上篇文章複習總結了Android中常見的佈局和佈局參數,這篇文章就來複習總結下自定義View(固然只是簡單的)。那麼何時須要使用自定義View? 當現有的組件沒法知足咱們的須要的咱們就可能得使用自定義View。java
view的工做流程指的是View的三大方法measure、layout、draw。其中measure用來測量View的寬和高,layout用來決定View的位置,draw用於繪製View,下面先從入口開始提及android
既然View顯示在Activity內,那麼先從Activity啓動提及,這裏省略前面的相關步驟直接從handleLaunchActivity
開始canvas
// ActivityThread
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
Activity a = performLaunchActivity(r, customIntent);
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);
}
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
Context appContext = createBaseContextForActivity(r, activity);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window);
mInstrumentation.callActivityOnCreate(activity, r.state);
}
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
r = performResumeActivity(token, clearHide, reason);
// wm爲WindowManagerImpl實例,decor爲DecorView實例
wm.addView(decor, l);
}
// WindowManagerImpl
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
// mGlobal爲WindowMangerGlobal實例
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
// WindowMangerGlobal
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
}
// ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
requestLayout();
// 設置Activity的decorView的parent爲ViewRootImpl實例
view.assignParent(this);
}
public void requestLayout() {
scheduleTraversals();
}
void scheduleTraversals() {
// 在屏幕刷新信號到來之後會調用mTraversalRunnable.run()
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
void doTraversal() {
// 該方法內部真正進行View的三大流程
performTraversals();
}
private void performTraversals() {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, mWidth, mHeight);
performDraw();
}
複製代碼
知道了入口了之後,咱們首先來看看View的measure過程吧bash
View的測量從DecorView開始,一層層的進行遞歸直到調用了全部View的onMeasure方法,繼續從performTraversals
開始app
private void performTraversals() {
// 對於DecorView來講其onMeasure的兩個參數由窗口大小和WindowManger.LayoutParams決定
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼
performMeasure
須要兩個參數,都是經過getRootMeasureSpec
獲取的ide
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
複製代碼
根據窗口的大小和佈局參數決定,繼續看看performMeasure
佈局
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
// 這裏的mView就是DecorView
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
複製代碼
裏面調用了onMeasure
, 對於View只須要測量自身便可,可是對於ViewGroup須要測量全部的子View,首先看看View的onMeasure
post
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 調用了該方法之後該View的大小就被測量完了
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
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;
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
複製代碼
因而可知如下兩點ui
setMeasureDimension
就是用來設置mMeasuredWidth
和mMeasuredHeight
的,默認View的onMeasure
實現測量模式爲MeasureSpec.AT_MOST
與MeasureSpec.EXACTLY
時取的大小是同樣的,也就是說在佈局文件中設置爲wrap_content
與match_parent
效果是同樣的MeasureSpec.UNSPECIFIED
時View沒有設置背景就返回自動最小寬/高,否則返回背景的最小寬/高和自身最小寬/高直接的最大值下面再看看ViewGroup因爲其須要測量全部子View,並根據本身的規則決定最後須要多少尺寸,並且每一個ViewGroup的規則都不盡相同所以ViewGroup並無重寫onMeasure
,可是定義了一個measureChildren
this
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];
// 若是View的Visibility不是Gone就measureChild
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
// 根據父View的measureSpec和子View的LayoutParams,以及對應方向的padding來決定子View的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
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) {
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);
}
複製代碼
咱們經過一張表格來概括下getChildMeasureSpec
的結果
父View的measureSpec | 子View的LayoutParams | 結果 |
---|---|---|
測量模式:MeasureSpec.EXACTLY 尺寸:A | 固定值B | 測量模式:MeasureSpec.EXACTLY 尺寸:固定值B |
測量模式:MeasureSpec.EXACTLY 尺寸:A | MATCH_PARENT | 測量模式:MeasureSpec.EXACTLY 尺寸:A-padding |
測量模式:MeasureSpec.EXACTLY 尺寸:A | WRAP_CONTENT | 測量模式:MeasureSpec.AT_MOST 尺寸:A-padding |
測量模式:MeasureSpec.AT_MOST 尺寸:A | 固定值B | 測量模式:MeasureSpec.EXACTLY 尺寸:固定值B |
測量模式:MeasureSpec.AT_MOST 尺寸:A | MATCH_PARENT | 測量模式:MeasureSpec.AT_MOST 尺寸:A-padding |
測量模式:MeasureSpec.AT_MOST 尺寸:A | WRAP_CONTENT | 測量模式:MeasureSpec.AT_MOST 尺寸:A-padding |
測量模式:MeasureSpec.UNSPECIFIED 尺寸:A | 固定值B | 測量模式:MeasureSpec.EXACTLY 尺寸:固定值B |
測量模式:MeasureSpec.UNSPECIFIED 尺寸:A | MATCH_PARENT | 測量模式:MeasureSpec.UNSPECIFIED 尺寸:A-padding |
測量模式:MeasureSpec.UNSPECIFIED 尺寸:A | WRAP_CONTENT | 測量模式:MeasureSpec.UNSPECIFIED 尺寸:A-padding |
Layout方法的做用是爲了肯定元素的位置,接着看看performLayout
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
// host就是decorView
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}
複製代碼
該方法的做用就是拿到decorView測量完的長/寬而後給出DecorView在屏幕中的位置要求其爲它的子View進行定位
public void layout(int l, int t, int r, int b) {
onLayout(changed, l, t, r, b);
}
複製代碼
而onLayout
方法在View裏面是一個空實現,由於每一個ViewGroup都有其本身的佈局方式
Draw方法的做用是用來繪製UI,接着看看performDraw
private void performDraw() {
draw(fullRedrawNeeded);
}
private void draw(boolean fullRedrawNeeded) {
drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) {
mView.draw(canvas);
}
複製代碼
這裏又調用了View的draw
方法
public void draw(Canvas canvas) {
/* * 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) */
drawBackground(canvas);
onDraw(canvas);
// 去調用子View的draw方法
dispatchDraw(canvas);
onDrawForeground(canvas);
}
複製代碼
這裏主要就是繪製背景、調用onDraw
、調用子View的draw
、繪製前景色
最基本的自定義View須要進行如下兩個步驟
首先,自定義View的時候咱們通常會選擇繼承自現有的View的子類或者直接繼承View,在繼承的時候得注意必定要有兩個參數(Context、AttributeSet)的構造方法除非這個View不在xml裏面使用,由於當LayoutInflate在解析xml的時候會經過反射調用兩個參數的構造器來建立View,若是找不到該構造器將致使程序crash
咱們能夠在values目錄下面新建一個declare-styleable
來定義屬性,而後在佈局文件中使用注意須要引用如下命名空間,而後在自定義View的構造器中經過obtainStyledAttributes
獲取屬性值
xmlns:app="http://schemas.android.com/apk/res-auto"
複製代碼
其次,咱們須要重寫幾個方法,通常咱們自定義View時須要重寫onMeasure()
、onDraw()
,自定義ViewGroup則是須要重寫onMeasure
、onLayout
,三個方法的做用以下所示
詳見之前寫的一個自定義ViewPager和TabLayout