前言
最近給組內同學做了一次“動態換膚和換文案”的主題分享,其中的核心就是LayoutInflater類,所以把LayoutInflater源碼梳理了一遍。巧了,這周掘金新榜和部分公眾號都發布了LayoutInflater或者換膚主題之類的文章。那只好站在各位大佬的肩膀上,也來湊個熱鬧,分析一下LayoutInflater類。(前方長文預警,會有很多源碼分析,源碼基于Android 9.0)
LayoutInflater簡介
官方文檔 developer.android.com/reference/a…
我們在加載布局的時候都會主動或者被動的用到 LayoutInflater ,比如 Activity 的setContentView
方法和Fragment的onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
回調等。LayoutInflater 的作用就是把布局文件xml實例化為相應的View組件。我們可以通過三種方法獲取 LayoutInflater:
Activity.getLayoutInflater()
;LayoutInflater.from(context)
;context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
;
每個方法都和 Context 相關聯,其中方法1和方法2最終都會通過方法3來實現。
獲取到 LayoutInflater 后,通過調用inflate
方法來實例化布局。而inflate
方法由很多重載,我們常用的是inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
,所有 inflate 方法最終會調用到 inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
。下面就從這個方法入手,開始分析 LayoutInflater 的源碼。
源碼分析
inflate方法
先看一下inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
的三個參數:
- XmlPullParser parser:很顯然是一個 XML 解析器,這個解析器就是 LayoutInflater 所要加載的 XML 布局轉化來的,通過 PULL 方式解析。
- ViewGroup root:裝載要加載的 XML 布局的根容器,比如,在 Activity 的
setContentView
方法中就是 id 為android.R.id.content
的 FrameLayout 根布局了。 - boolean attachToRoot:是否將所解析的布局添加到根容器中,同時也影響了所解析布局的寬高。
被廣泛討論的是root
和attachToRoot
的不同傳參對被加載的布局文件的影響,下面看代碼。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {synchronized (mConstructorArgs) {final Context inflaterContext = mContext;// 將parser轉成AttributeSet接口,用來讀取xml中設置的View屬性final AttributeSet attrs = Xml.asAttributeSet(parser);Context lastContext = (Context) mConstructorArgs[0];mConstructorArgs[0] = inflaterContext;View result = root; // 此方法返回的View,默認是roottry {// Look for the root node.int type;while ((type = parser.next()) != XmlPullParser.START_TAG &&type != XmlPullParser.END_DOCUMENT) {// Empty}...final String name = parser.getName(); // 獲取當前的標簽名...if (TAG_MERGE.equals(name)) { // 處理<merge>標簽if (root == null || !attachToRoot) {throw new InflateException("<merge /> can be used only with a valid "+ "ViewGroup root and attachToRoot=true");}// 遞歸處理rInflate(parser, root, inflaterContext, attrs, false);} else {// Temp is the root view that was found in the xml// 創建View對象final View temp = createViewFromTag(root, name, inflaterContext, attrs);ViewGroup.LayoutParams params = null;if (root != null) {...// Create layout params that match root, if suppliedparams = root.generateLayoutParams(attrs); // 獲取根View的寬高if (!attachToRoot) { // 如果attachToRoot為false,則給根View設置寬高// 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不空,且attachToRoot為true,則將根View添加到容器中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) {// 如果root空或者attachToRoot為false,則將返回結果設置為根Viewresult = temp;}}} catch (XmlPullParserException e) {...} catch (Exception e) {...} finally {// Don't retain static reference on context.mConstructorArgs[0] = lastContext;mConstructorArgs[1] = null;Trace.traceEnd(Trace.TRACE_TAG_VIEW);}// 要么是root,要么是創建的根Viewreturn result;}}
復制代碼
從代碼中可以看出root
和attachToRoot
不同傳參的影響:
- 如果root不為null,attachToRoot設為true,則會將加載的布局添加到一個父布局中,即root,并且返回root;
- 如果root不為null,attachToRoot設為false,則會對布局文件最外層的所有layout屬性進行設置,并且返回該布局的根View,當該view被添加到父view當中時,這些layout屬性會自動生效;
- 如果root為null,attachToRoot將失去作用,設置任何值都沒有意義,返回的也是要加載的布局的根View;
rInflate方法
從上面的方法中可以看到處理<merge>
標簽時會調用rInflate
,處理子View時會調用rInflateChildren
方法。其實rInflateChildren
中調用的是rInflate
,而rInflate
也調用了rInflateChildren
,從而形成了遞歸調用,也就是遞歸處理子View。
void rInflate(XmlPullParser parser, View parent, Context context,AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {final int depth = parser.getDepth();int type;boolean pendingRequestFocus = false;while (((type = parser.next()) != XmlPullParser.END_TAG ||parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {if (type != XmlPullParser.START_TAG) {continue;}final String name = parser.getName();if (TAG_REQUEST_FOCUS.equals(name)) {// 處理<requestFocus>標簽pendingRequestFocus = true;consumeChildElements(parser);} else if (TAG_TAG.equals(name)) {// 處理<tag>標簽parseViewTag(parser, parent, attrs);} else if (TAG_INCLUDE.equals(name)) {// 處理<include>標簽if (parser.getDepth() == 0) {throw new InflateException("<include /> cannot be the root element");}parseInclude(parser, context, parent, attrs);} else if (TAG_MERGE.equals(name)) { // <merge>標簽異常throw new InflateException("<merge /> must be the root element");} else { // 創建View對象final View view = createViewFromTag(parent, name, context, attrs);final ViewGroup viewGroup = (ViewGroup) parent;final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);rInflateChildren(parser, view, attrs, true); // 遞歸處理孩子節點viewGroup.addView(view, params); // 將View添加到父布局中}}if (pendingRequestFocus) { // 父布局處理焦點parent.restoreDefaultFocus();}if (finishInflate) { // 結束加載parent.onFinishInflate();}}
復制代碼
該方法中會處理<requestFocus>
、<tag>
、<include>
、<merge>
和普通View標簽。其中:
<requestFocus>
是重新定位焦點的,調用的consumeChildElements
方法其實沒干什么事,只是簡單的把該標簽消費結束掉。<tag>
標簽一般很少用,它主要用來標記View,給View設置一個標簽值,例如:
<TextViewandroid:id="@+id/tv"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Hello World!" ><tag android:id="@+id/tag"android:value="hello" /></TextView>findViewById(R.id.tv).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// 試一下tag標簽Toast.makeText(MainActivity.this, (String) v.getTag(R.id.tag), Toast.LENGTH_SHORT).show();}});
復制代碼
在ListView的自定義Adapter中,應該都有用到過View的setTag方法,即:使用ViewHolder來重復利用View。parseViewTag
方法:
private void parseViewTag(XmlPullParser parser, View view, AttributeSet attrs)throws XmlPullParserException, IOException {final Context context = view.getContext();final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ViewTag);// 讀取tag的idfinal int key = ta.getResourceId(R.styleable.ViewTag_id, 0);// 讀取tag的值final CharSequence value = ta.getText(R.styleable.ViewTag_value);// 給View設置該tagview.setTag(key, value);ta.recycle();// 結束該標簽(子View無效)consumeChildElements(parser);}
復制代碼
<include>
標簽不能是根標簽,parseInclude
方法單獨分析。<merge>
標簽只能是根標簽,這里會拋異常。
parseInclude方法
private void parseInclude(XmlPullParser parser, Context context, View parent,AttributeSet attrs) throws XmlPullParserException, IOException {int type;if (parent instanceof ViewGroup) { // 必須在ViewGroup里才有效// 處理theme屬性...// If the layout is pointing to a theme attribute, we have to// massage the value to get a resource identifier out of it.// 拿到layout指定的布局int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);...if (layout == 0) { // 必須是合法的idfinal String value = attrs.getAttributeValue(null, ATTR_LAYOUT);throw new InflateException("You must specify a valid layout "+ "reference. The layout ID " + value + " is not valid.");} else { // 類似于inflate的處理// 拿到layout的解析器final XmlResourceParser childParser = context.getResources().getLayout(layout);try {final AttributeSet childAttrs = Xml.asAttributeSet(childParser);while ((type = childParser.next()) != XmlPullParser.START_TAG &&type != XmlPullParser.END_DOCUMENT) {// Empty.}if (type != XmlPullParser.START_TAG) {throw new InflateException(childParser.getPositionDescription() +": No start tag found!");}// layout的根標簽final String childName = childParser.getName();if (TAG_MERGE.equals(childName)) { // 處理<merge>// The <merge> tag doesn't support android:theme, so// nothing special to do here.rInflate(childParser, parent, context, childAttrs, false);} else { // 處理Viewfinal View view = createViewFromTag(parent, childName,context, childAttrs, hasThemeOverride);final ViewGroup group = (ViewGroup) parent;final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Include);// 獲取<include>里設置的idfinal int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);// 獲取<include>里設置的visibilityfinal int visibility = a.getInt(R.styleable.Include_visibility, -1);a.recycle();ViewGroup.LayoutParams params = null;try { // 獲取<include>里設置的寬高params = group.generateLayoutParams(attrs);} catch (RuntimeException e) {// Ignore, just fail over to child attrs.}if (params == null) {// 獲取layout里設置的寬高params = group.generateLayoutParams(childAttrs);}// <include>里設置的寬高優先于layout里設置的view.setLayoutParams(params);// Inflate all children.rInflateChildren(childParser, view, childAttrs, true);if (id != View.NO_ID) {// include里設置的id優先級高view.setId(id);}// include里設置的visibility優先級高switch (visibility) {case 0:view.setVisibility(View.VISIBLE);break;case 1:view.setVisibility(View.INVISIBLE);break;case 2:view.setVisibility(View.GONE);break;}group.addView(view);}} finally {childParser.close();}}} else {throw new InflateException("<include /> can only be used inside of a ViewGroup");}LayoutInflater.consumeChildElements(parser);}
復制代碼
- include里必須設置layout屬性,且layout的id必須合法;
- include里設置的id優先級高于layout里設置的id,即:兩者同時設置時,后者會失效;
- include里設置的width和height屬性優先級高于layout里設置的寬高;
- include里設置的visibility屬性優先級高于layout設置的visibility。
createViewFromTag方法
正常View標簽都是通過createViewFromTag
來創建對應的View對象的。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {if (name.equals("view")) { // 真正的View標簽名存在class屬性中name = attrs.getAttributeValue(null, "class");}...try {View view;if (mFactory2 != null) { // 先使用Factory2view = mFactory2.onCreateView(parent, name, context, attrs);} else if (mFactory != null) { // 再使用Factoryview = 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 {// 通過標簽名中是否包含'.'來區分是否為自定義Viewif (-1 == name.indexOf('.')) {// 處理系統Viewview = onCreateView(parent, name, attrs);} else { // 自定義View用的是全限定類名// 處理自定義Viewview = createView(name, null, attrs);}} finally {mConstructorArgs[0] = lastContext;}}return view;} catch (InflateException e) {...} catch (ClassNotFoundException e) {...} catch (Exception e) {...}}
復制代碼
- 優先通過Factory2和Factory來創建View,這兩個Factory等會再說;
- 通過標簽名中是否包含'.'來區分待創建的View是自定義View還是系統View;
- 系統View會在
onCreateView
方法中添加android.view.
前綴,然后交由createView
處理。
createView方法
public final View createView(String name, String prefix, AttributeSet attrs)throws ClassNotFoundException, InflateException {// 有緩存Constructor<? extends View> constructor = sConstructorMap.get(name);if (constructor != null && !verifyClassLoader(constructor)) {constructor = null;sConstructorMap.remove(name);}Class<? extends View> clazz = null;try {if (constructor == null) { // 第一次則通過反射創建constructor// Class not found in the cache, see if it's real, and try to add itclazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);if (mFilter != null && clazz != null) {boolean allowed = mFilter.onLoadClass(clazz);if (!allowed) {failNotAllowed(name, prefix, attrs);}}// 使用的是包含Context, AttributeSet這兩個參數的構造函數constructor = clazz.getConstructor(mConstructorSignature);constructor.setAccessible(true);sConstructorMap.put(name, constructor); // 添加到緩存中} else { // 命中緩存// If we have a filter, apply it to cached constructorif (mFilter != null) { // 先過濾// Have we seen this name before?Boolean allowedState = mFilterMap.get(name);if (allowedState == null) {// New class -- remember whether it is allowedclazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);boolean allowed = clazz != null && mFilter.onLoadClass(clazz);mFilterMap.put(name, allowed);if (!allowed) {failNotAllowed(name, prefix, attrs);}} else if (allowedState.equals(Boolean.FALSE)) {failNotAllowed(name, prefix, attrs);}}}Object lastContext = mConstructorArgs[0];if (mConstructorArgs[0] == null) {// Fill in the context if not already within inflation.mConstructorArgs[0] = mContext;}Object[] args = mConstructorArgs;args[1] = attrs;// 反射創建View實例對象final View view = constructor.newInstance(args);if (view instanceof ViewStub) {// 如果是ViewStub則懶加載// Use the same context when inflating ViewStub later.final ViewStub viewStub = (ViewStub) view;viewStub.setLayoutInflater(cloneInContext((Context) args[0]));}mConstructorArgs[0] = lastContext;return view;} ...}
復制代碼
通過反射待創建View的構造函數(兩個參數:Context和AttributeSet的構造函數)來實例化View對象,如果是ViewStub對象還會進行懶加載。
LayoutInflater.Factory/Factory2
通過以上流程,使用LayoutInflater的infalte方法加載布局文件的整體流程就分析完了。但出現了Factory2
和Factory
類,它們會優先創建View,我們來看看著兩個類到底是什么!
它們都是LayoutInflater的內部類——兩個接口:
public interface Factory {public View onCreateView(String name, Context context, AttributeSet attrs);}public interface Factory2 extends Factory {public View onCreateView(View parent, String name, Context context, AttributeSet attrs);}
復制代碼
Factory2
繼承了Factory
,增加了一個帶View parent
參數的onCreateView
重載方法。它們是在createViewFromTag
中被調用的,默認為null,說明開發人員可以自定義這兩個Factory,則通過它們可以改造待加載XML布局中的View標簽,來使用自定義規則創建View。
來看一下它們的設置方法:
public void setFactory(Factory factory) {if (mFactorySet) {throw new IllegalStateException("A factory has already been set on this LayoutInflater");}// 和setFactory2類似...}}public void setFactory2(Factory2 factory) {if (mFactorySet) { // 只能設置一次throw new IllegalStateException("A factory has already been set on this LayoutInflater");}if (factory == null) {throw new NullPointerException("Given factory can not be null");}mFactorySet = true;if (mFactory == null) {mFactory = mFactory2 = factory;} else { // 合并原有的FactorymFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);}}
復制代碼
可以看到Factory
和Factory2
只能設置一次,否則會拋異常。
這兩個Factory的區別是什么?
- Factory2 是API 11 被加進來的;
- Factory2 繼承自 Factory,也就說現在直接使用Factory2即可;
- Factory2 可以對創建 View 的 Parent 進行操作;
那如何應用呢?
Factory2/Factory的應用
AppCompatActivity中的應用
先看一張圖:
這個布局中使用的是正常的標簽<TextView>
和<Button>
,但通過Layout Inspector工具分析頁面會發現它們被替換成了AppCompatTextView
和AppCompatButton
。
跟蹤一下AppCompatActivity
的onCreate
方法:
protected void onCreate(@Nullable Bundle savedInstanceState) {AppCompatDelegate delegate = this.getDelegate();delegate.installViewFactory();delegate.onCreate(savedInstanceState);...super.onCreate(savedInstanceState);}
復制代碼
委托到了AppCompatDelegate
類,并且調用了installViewFactory
方法。找到這個類的一個實現AppCompatDelegateImpl
(不同版本的源碼這個實現類的名字不同):
class AppCompatDelegateImpl extends AppCompatDelegate implements Callback, Factory2
復制代碼
看到關鍵的Factory2
了,直接看installViewFactory
方法:
public void installViewFactory() {LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);if (layoutInflater.getFactory() == null) { // 設置自身到LayoutInflaterLayoutInflaterCompat.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");}}
復制代碼
通過LayoutInflaterCompat.setFactory2
將AppCompatDelegateImpl
設置到LayoutInflater中。繼續跟蹤onCreateView
的實現,會走到createView
方法:
public View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {if (this.mAppCompatViewInflater == null) {TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);String viewInflaterClassName = a.getString(styleable.AppCompatTheme_viewInflaterClass);if (viewInflaterClassName != null && !AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {try {Class viewInflaterClass = Class.forName(viewInflaterClassName);this.mAppCompatViewInflater = (AppCompatViewInflater)viewInflaterClass.getDeclaredConstructor().newInstance();} catch (Throwable var8) {Log.i("AppCompatDelegate", "Failed to instantiate custom view inflater " + viewInflaterClassName + ". Falling back to default.", var8);this.mAppCompatViewInflater = new AppCompatViewInflater();}} else {this.mAppCompatViewInflater = new AppCompatViewInflater();}}...return this.mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed());}
復制代碼
會創建一個AppCompatViewInflater
類,并且調用了它的createView
方法,看樣子找到源頭了。
來到AppCompatViewInflater
類:
public class AppCompatViewInflater {...final View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {Context originalContext = context;...View view = null;byte var12 = -1;switch(name.hashCode()) {...case -938935918:if (name.equals("TextView")) {var12 = 0;}break;...}switch(var12) {case 0:view = this.createTextView(context, attrs);this.verifyNotNull((View)view, name);break;...default:view = this.createView(context, name, attrs);}...return (View)view;}@NonNullprotected AppCompatTextView createTextView(Context context, AttributeSet attrs) {return new AppCompatTextView(context, attrs);}...
}
復制代碼
通過name
參數拿到TextView標簽后,直接替換成了AppCompatTextView。
通過這波操作,將一些 widget 自動變成兼容widget (例如將 TextView 變成 AppCompatTextView)以便于向下兼容新版本中的特性。
那我們也可以仿照AppCompatActivity來自定義Factory實現自己需要的替換效果。
自定義Factory2
大多換膚功能的實現就是通過實現自定義Factory,攔截特定View,然后修改這些View的屬性值,或者直接返回自定義的View。舉個栗子:
- 替換TextView的文字顏色;
- 在不創建selector文件前提下實現圓角按鈕;
先看XML:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:id="@+id/container"android:orientation="vertical"tools:ignore="MissingPrefix"><TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:paddingTop="10dp"android:paddingBottom="10dp"android:gravity="center"android:textSize="20sp"android:textColor="@color/third_tv_text_color"android:text="測試設置的Factory"/><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="10dp"android:padding="20dp"android:layout_gravity="center_horizontal"android:background="#ffbccc"android:text="蘋果獼猴桃牛油果榴蓮"android:textSize="15sp"app:cornerRadius="5dp"app:strokeWidth="1dp"app:strokeColor="#ccffcc"/></LinearLayout>
復制代碼
注意:這里用的是系統View的標簽,但屬性里用到了自定義屬性。
<resources><declare-styleable name="MyTextView"><attr name="android:textColor"/></declare-styleable><declare-styleable name="RoundButton"><attr name="cornerRadius" format="dimension" /><attr name="strokeWidth" format="dimension" /><attr name="strokeColor" format="color" /></declare-styleable></resources>
復制代碼
注意:如果想直接替換Android自帶屬性,需要在自定義屬性里加上android:
前綴。
public class MyFactory implements LayoutInflater.Factory2 {public LayoutInflater.Factory mOriginalFactory;// 這個模擬新資源private Map<String, String> mColorMap;public MyFactory(LayoutInflater.Factory factory) {this.mOriginalFactory = factory;mColorMap = new HashMap<>();// 模擬新的皮膚資源——新文字顏色mColorMap.put("third_tv_text_color", "#0000ff");}@Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {return onCreateView(name, context, attrs);}@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {View view = null;if (mOriginalFactory != null) {view = mOriginalFactory.onCreateView(name, context, attrs);}if ("TextView".equals(name)) {TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);// 注意這里的屬性名:android:textColor,不用自定義命名空間int resourceId = ta.getResourceId(R.styleable.MyTextView_android_textColor, -1);String resourceName = context.getResources().getResourceName(resourceId);resourceName = resourceName.substring(resourceName.lastIndexOf('/') + 1);view = new TextView(context, attrs); // 可以直接修改原TextView的屬性ta.recycle();// 這里模擬替換原TextView的textColor屬性值String color = mColorMap.get(resourceName);((TextView) view).setTextColor(Color.parseColor(color));}if ("Button".equals(name)) {view = new Button(context, attrs);// 讀取自定義的屬性值TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RoundButton);float radius = ta.getDimension(R.styleable.RoundButton_cornerRadius, 0);float strokeWidth = ta.getDimension(R.styleable.RoundButton_strokeWidth, 0);int strokeColor = ta.getColor(R.styleable.RoundButton_strokeColor, -1);// 構造圓角按鈕GradientDrawable drawable = new GradientDrawable();drawable.setCornerRadius(DensityUtil.dip2px(context, radius));drawable.setStroke(DensityUtil.dip2px(context, strokeWidth), strokeColor);view.setBackground(drawable);ta.recycle();}return view;}
}
復制代碼
注意:新資源可以通過其他方式存儲和獲取,從而實現動態熱換膚;如果使用的是AppCompatActivity,自定義Factory必須在調用super.onCreate
之前設置,因為它已經有了一個Factory;如果使用的是Activity,則必須在調用setContentView
方法之前設置。
效果:
可以看到TextView文字的顏色變成了藍色;在不提供自定義drawable的xml文件以及不使用自定義View標簽的前提下,實現了圓角按鈕。
本文的相關示例代碼都在:zjxstar的GitHub上,感興趣的同學可以看下。
總結
LayoutInflater的相關分析就這么多,文章有點長,慢慢看吧!
- LayoutInflater的inflate的過程的核心方法是:createViewFromTag 和 createView 方法;
- LayoutInflater通過PULL解析器來解析XML布局文件,通過反射來創建View對象;
- LayoutInflater.Factory只能設置一次,可以用來替換View;
參考資料
- 換膚、全局字體替換、無需編寫shape、selector 的原理Factory小結mp.weixin.qq.com/s/1ua0geFnr…
- Android系統源碼分析--View繪制流程之-inflatejuejin.im/post/5bfa7f…
- Android LayoutInflater Factory 源碼解析www.jianshu.com/p/9c16bbaee…
- Android LayoutInflater 源碼解析www.jianshu.com/p/f0f3de2f6…