本文基於Android API 28
繪製三大方法
onMeasure
onMeasure(int, int)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在onMeasure方法用來測量view和他的內容,來決定view的寬高,該方法被measure(int, int)調用,並且需要被子類重寫去測量view的內容的精確、有效的寬高;
重寫該方法時,必須調用setMeasureDimension(int, int)方法去保存該view測量的寬和高;如果不這樣做,measure(int, int)方法會拋出IllegalStateException異常,調用父類的onMeasure(int, int)方法可以避免這樣;
基類實現了測量默認的背景尺寸,除非MeasureSpec允許更大的尺寸,子類應該重寫onMeasure(int, int)方法去提供更好的測量他們的內容;
如果重寫該方法,子類有責任確保實測寬高至少是view的最小寬高;
該方法在View類裏面直接調用setMeasureDimension方法去保存默認的view的尺寸,參數直接調用getDefaultSize方法獲取默認寬高;
getDefaultSize(int, int);
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;
}
這個方法返回默認的尺寸,如果MeasureSpec沒有添加約束,則使用提供的尺寸,如果MeasureSpec允許,則將尺寸放大;即當MeasureSpec的Mode爲UNSPECIFIED時,沒有約束,使用默認的大小尺寸,爲AT_MOST和EXACTLY時,需要計算出尺寸的大小;
getSuggestedMinimumHeight() /Width()
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
這兩個方法返回view建議的最小寬高以及背景的最小寬高的最大值;調用這兩個方法時,需要注意返回的寬高應該在父View的要求範圍內;
setMeasuredDimension(int, int)
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);
}
該方法必須由onMeasure(int, int)方法調用來保存實測的view寬高,否則將拋出異常;這個方法裏面涉及到要判斷View及其父View是否爲ViewGroup及是否有光學效果,來決定是否要處理關於這些效果產生的尺寸影響;
isLayoutModeOptical(Object)
public static boolean isLayoutModeOptical(Object o) {
return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
}
該方法返回該View是否爲ViewGroup,並且是否由光效效果,例如陰影、亮光等,這些也將影響View的尺寸測量;
getOpticalInsets()
public Insets getOpticalInsets() {
if (mLayoutInsets == null) {
mLayoutInsets = computeOpticalInsets();
}
return mLayoutInsets;
}
該方法返回當前View的插圖屬性,如果爲空,則去計算;
setOpticalInsets(Insets)
public void setOpticalInsets(Insets insets) {
mLayoutInsets = insets;
}
爲該View設置Insets,該方法不請求layout,如果手動調用該方法設置了Insets,則需要調用requestLayout()方法;
computeOpticalInsets()
Insets computeOpticalInsets() {
return (mBackground == null) ? Insets.NONE : mBackground.getOpticalInsets();
}
如果當前View沒有背景圖片,則返回Insets.NONE,否則去計算背景圖片的光學效果範圍;
setMeasuredDimensionRaw(int, int)
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
該方法中最終保存View的尺寸,並設置標誌位爲PFLAG_MEASURED_DIMENSION_SET;
MeasureSpec
MeasureSpec類封裝了從父View傳遞到子View的佈局要求;包含size和mode;
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
三種Mode
·UNSPECIFIED(00....)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
父佈局對子View沒有任何限制,子View可以是任何大小;
·EXACTLY(01....)
public static final int EXACTLY = 1 << MODE_SHIFT;
父佈局給了子View一個準確的大小約束,不管子View想要多大,都會受到該限制;
·AT_MOST(10....)
public static final int AT_MOST = 2 << MODE_SHIFT;
子View可是達到他想要的任何指定大小;
makeMeasureSpec(int, int)
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);
}
}
該方法提供了Size和Mode的算法;
getMode(int)、getSize(int)
這兩個方法分別獲取View的MeasureSpec的Mode和Size;
onLayout
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
該方法是一個空實現,需要子View自己去實現是否需要爲他們的子View重新計算大小和位置;
onDraw
protected void onDraw(Canvas canvas) {
}
該方法也是一個空實現,子View自己定義繪製內容;
View創建
加載xml佈局
LayoutInflater.from(context).inflate(R.layout.activity_base, null);
以上代碼用來加載定義好的xml文件,加載佈局;
LayoutInflater#from(context)
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
在代碼中,通過Context的getSystemService方法去獲取到LayoutInflater的實例;
LayoutInflater#inflate()
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
這裏inflate通過內容調用到另一個重載的inflate方法,這裏獲取到了Resource資源,並且獲取到XmlResourceParser對象實例,用來解析Xml佈局,然後最終調用到了inflate(XmlPullParser, ViewGroup, boolean)方法;
LayoutInflater#inflate(XmlPullParser, ViewGroup, boolean)
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
... ...
final String name = parser.getName();
... ...
if (TAG_MERGE.equals(name)) {
... ...
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
... ....
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
... ...
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
... ...
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
... ...
return result;
}
}
這裏省略掉部分代碼,使用Xml解析器去解析Xml,獲取Xml標籤,先忽略Xml開始和結束等標籤,直接看創建View的部分,這裏判斷是否爲merge標籤,進入else分支,這裏的核心代碼是調用createViewFromTag(root, name, inflaterContext, attrs)方法去創建View,後面是判斷是否有父佈局,獲取父佈局參數等信息,將創建好的View添加到父佈局;
LayoutInflater#createViewFromTag(View, String, Context, AttributeSet, boolean)
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {
... ...
try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
... ...
}
在該方法中,首先先判斷了三個factory,這兩個factory在通過LayoutInflater.from().inflate()方法在加載佈局的時候都爲空,這個Factory是在創建View的時候可以去手動設置View的創建規則,實現自己定義View的創建,例如在AppComAppCompatActivity中就會初始化一個Factory對象去創建屬於AppCompat風格的View,下面分析;
所以方法進入到下面if處,這裏判斷name中是否包含“.”,即是在判斷是否爲自定義View,其實兩個方法最終調用的都是同一個方法,只是在系統的View時,會爲其加上android.view的包名;
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
LayoutInflater#creatView(String, String, AttrbuteSet)
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
... ...
Class<? extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);
... ...
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);
... ...
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
final View view = constructor.newInstance(args);
... ...
return view;
... ...
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
這是是通過反射拿到View類,通過反射去調用對應的View的構造方法去創建VIew,所以這裏明白了通過Xml創建佈局的時候系統是通過反射來創建的;走到這裏就可以回到View的構造方法了;
通過setContentView創建
在Activity的onCreate()方法中,通過setContentView()方法將佈局加載進去,Activity繼承自AppCompatActivity;
AppCompatActivity#setContentView()
public void setContentView(@LayoutRes int layoutResID) {
this.getDelegate().setContentView(layoutResID);
}
這裏調用geDelegate()的setContentView方法;
public AppCompatDelegate getDelegate() {
if (this.mDelegate == null) {
this.mDelegate = AppCompatDelegate.create(this, this);
}
return this.mDelegate;
}
AppCompatDelegate類中的getDelegate()方法返回一個AppCompatDelegate的實現類AppCompatDelegateImpl,所以方法的具體實現由AppCompatDelegateImpl類完成;
AppCompatDelegateImpl#setContentView()
public void setContentView(int resId) {
this.ensureSubDecor();
ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290);
contentParent.removeAllViews();
LayoutInflater.from(this.mContext).inflate(resId, contentParent);
this.mOriginalWindowCallback.onContentChanged();
}
到這裏又看到了LayoutInflater的inflate()方法,流程又回到了步驟2.1中;不過這裏需要注意的是,在Xml創建的流程中,在createViewFromTag()方法中有一個對factory的判斷,在Xml創建時爲空,但是在這裏就不爲空了,這裏在AppCompatActivity中對其做了處理,在Factory中去創建自己風格的View,因爲AppCompatActivity和之前的Activity的風格是略有不一樣;
Activity繼承自AppCompatActivity,看onCreate()方法;
AppCompatActivity#onCreate()
protected void onCreate(@Nullable Bundle savedInstanceState) {
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
... ...
super.onCreate(savedInstanceState);
}
這裏初始化了一個Factory對象,具體實現還是在AppCompatDelegateImpl中;
AppCompatDelegateImpl#installViewFactory()
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i("AppCompatDelegate", "The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's");
}
}
在這裏爲LayoutInflater設置了一個Factory2對象;在創建的View的時候,判斷factory不爲空,就通過factory去創建View,最終如果是自定義View,則通過反射去創建View,如果是系統自身的View,則對應到AppCompatViewInflater的createView()方法中,匹配要創建的View,分別有每個View對應的實現類;
通過代碼new
通過代碼new的話,直接去調用對應的View的構造方法;
文章已同步至GitPress博客:https://gitpress.io/@yangshijie/