View原始碼繪製流程
本文基於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/