湊個熱鬧-LayoutInflater相關分析

前言

最近給組內同學做了一次“動態換膚和換文案”的主題分享,其中的核心就是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:

  1. Activity.getLayoutInflater()
  2. LayoutInflater.from(context)
  3. 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)的三個參數:

  1. XmlPullParser parser:很顯然是一個 XML 解析器,這個解析器就是 LayoutInflater 所要加載的 XML 布局轉化來的,通過 PULL 方式解析。
  2. ViewGroup root:裝載要加載的 XML 布局的根容器,比如,在 Activity 的setContentView方法中就是 id 為android.R.id.content的 FrameLayout 根布局了。
  3. boolean attachToRoot:是否將所解析的布局添加到根容器中,同時也影響了所解析布局的寬高。

被廣泛討論的是rootattachToRoot的不同傳參對被加載的布局文件的影響,下面看代碼。

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;}}
復制代碼

從代碼中可以看出rootattachToRoot不同傳參的影響:

  1. 如果root不為null,attachToRoot設為true,則會將加載的布局添加到一個父布局中,即root,并且返回root;
  2. 如果root不為null,attachToRoot設為false,則會對布局文件最外層的所有layout屬性進行設置,并且返回該布局的根View,當該view被添加到父view當中時,這些layout屬性會自動生效;
  3. 如果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標簽。其中:

  1. <requestFocus>是重新定位焦點的,調用的consumeChildElements方法其實沒干什么事,只是簡單的把該標簽消費結束掉。
  2. <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);}
復制代碼
  1. <include>標簽不能是根標簽,parseInclude方法單獨分析。
  2. <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);}
復制代碼
  1. include里必須設置layout屬性,且layout的id必須合法;
  2. include里設置的id優先級高于layout里設置的id,即:兩者同時設置時,后者會失效;
  3. include里設置的width和height屬性優先級高于layout里設置的寬高;
  4. 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) {...}}
復制代碼
  1. 優先通過Factory2和Factory來創建View,這兩個Factory等會再說;
  2. 通過標簽名中是否包含'.'來區分待創建的View是自定義View還是系統View;
  3. 系統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方法加載布局文件的整體流程就分析完了。但出現了Factory2Factory類,它們會優先創建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);}}
復制代碼

可以看到FactoryFactory2只能設置一次,否則會拋異常。
這兩個Factory的區別是什么?

  1. Factory2 是API 11 被加進來的;
  2. Factory2 繼承自 Factory,也就說現在直接使用Factory2即可;
  3. Factory2 可以對創建 View 的 Parent 進行操作;

那如何應用呢?

Factory2/Factory的應用

AppCompatActivity中的應用

先看一張圖:

這個布局中使用的是正常的標簽<TextView><Button>,但通過Layout Inspector工具分析頁面會發現它們被替換成了AppCompatTextViewAppCompatButton
跟蹤一下AppCompatActivityonCreate方法:

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.setFactory2AppCompatDelegateImpl設置到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。舉個栗子:

  1. 替換TextView的文字顏色;
  2. 在不創建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的相關分析就這么多,文章有點長,慢慢看吧!

  1. LayoutInflater的inflate的過程的核心方法是:createViewFromTag 和 createView 方法;
  2. LayoutInflater通過PULL解析器來解析XML布局文件,通過反射來創建View對象;
  3. LayoutInflater.Factory只能設置一次,可以用來替換View;

參考資料

  1. 換膚、全局字體替換、無需編寫shape、selector 的原理Factory小結mp.weixin.qq.com/s/1ua0geFnr…
  2. Android系統源碼分析--View繪制流程之-inflatejuejin.im/post/5bfa7f…
  3. Android LayoutInflater Factory 源碼解析www.jianshu.com/p/9c16bbaee…
  4. Android LayoutInflater 源碼解析www.jianshu.com/p/f0f3de2f6…

關注微信公眾號,最新技術干貨實時推送

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/389012.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/389012.shtml
英文地址,請注明出處:http://en.pswp.cn/news/389012.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

ASP.NET Core文件上傳、下載與刪除

首先我們需要創建一個form表單如下: <form method"post" enctype"multipart/form-data" asp-controller"UpLoadFile" asp-action"FileSave"> <div> <div> <p>Form表單多個上傳文件:</p> <input type…

8 一點就消失_消失的莉莉安(26)

文|明鳶Hi&#xff0c;中午好&#xff0c;我是暖叔今天是免費連載《消失的莉莉安》第26章消失的莉莉安??往期鏈接&#xff1a;▼ 向下滑動閱讀1&#xff1a;“消失的莉莉安(1)”2&#xff1a; 消失的莉莉安(2)3&#xff1a;“消失的莉莉安(3)”4&#xff1a;“消失的莉莉安…

透明的WinForm窗體

this.Location new System.Drawing.Point(100, 100); this.Cursor System.Windows.Forms.Cursors.Hand; // 定義在窗體上&#xff0c;光標顯示為手形 this.Text "透明的WinForm窗體&#xff01;"; // 定義窗體的標題…

mysql那本書適合初學者_3本書適合初學者

mysql那本書適合初學者為什么要書籍&#xff1f; (Why Books?) The internet is a treasure-trove of information on a variety of topics. Whether you want to learn guitar through Youtube videos or how to change a tire when you are stuck on the side of the road, …

junit與spring-data-redis 版本對應成功的

spring-data-redis 版本:1.7.2.RELEASE junit 版本:4.12 轉載于:https://www.cnblogs.com/austinspark-jessylu/p/9366863.html

語音對話系統的設計要點與多輪對話的重要性

這是阿拉燈神丁Vicky的第 008 篇文章就從最近短視頻平臺的大媽與機器人快寶的聊天說起吧。某銀行內&#xff0c;一位阿姨因等待辦理業務的時間太長&#xff0c;與快寶機器人展開了一場來自靈魂的對話。對于銀行工作人員的不滿&#xff0c;大媽向快寶說道&#xff1a;“你們的工…

c讀取txt文件內容并建立一個鏈表_C++鏈表實現學生信息管理系統

可以增刪查改&#xff0c;使用鏈表存儲&#xff0c;支持排序以及文件存儲及數據讀取&#xff0c;基本可以應付期末大作業&#xff08;狗頭&#xff09; 界面為源代碼為一個main.cpp和三個頭文件&#xff0c;具體為 main.cpp#include <iostream> #include <fstream>…

注冊表啟動

public void SetReg() { RegistryKey hklmRegistry.LocalMachine; RegistryKey runhklm.CreateSubKey("Software/Microsoft/Windows/CurrentVersion/Run"); //定義hklm指向注冊表的LocalMachine,對注冊表的結構&#xff0c;可以在windows的運行里&#…

閻焱多少身價_2020年,數據科學家的身價是多少?

閻焱多少身價Photo by Christine Roy on Unsplash克里斯汀羅伊 ( Christine Roy) 攝于Unsplash Although we find ourselves in unprecedented times of uncertainty, current events have shown just how valuable the fields of Data Science and Computer Science truly are…

Django模型定義參考

字段 對字段名稱的限制 字段名不能是Python的保留字&#xff0c;否則會導致語法錯誤字段名不能有多個連續下劃線&#xff0c;否則影響ORM查詢操作Django模型字段類 字段類說明AutoField自增ID字段BigIntegerField64位有符號整數BinaryField存儲二進制數據的字段&#xff0c;對應…

精通Quartz-入門-Job

JobDetail實例&#xff0c;并且&#xff0c;它通過job的類代碼引用這個job來執行。每次調度器執行job時&#xff0c;它會在調用job的execute(..)方法之前創建一個他的實例。這就帶來了兩個事實&#xff1a;一、job必須有一個不帶參數的構造器&#xff0c;二、在job類里定義數據…

單據打印_Excel多功能進銷存套表,自動庫存單據,查詢打印一鍵操作

Hello大家好&#xff0c;我是幫幫。今天跟大家分享一張Excel多功能進銷存管理套表&#xff0c;自動庫存&#xff0c;單據打印&#xff0c;查詢統算一鍵操作。為了讓大家能更穩定的下載模板&#xff0c;我們又開通了全新下載方式(見文章末尾)&#xff0c;以便大家可以輕松獲得免…

卡爾曼濾波濾波方程_了解卡爾曼濾波器及其方程

卡爾曼濾波濾波方程Before getting into what a Kalman filter is or what it does, let’s first do an exercise. Open the google maps application on your phone and check your device’s current location.在了解什么是卡爾曼濾波器或其功能之前&#xff0c;我們先做一個…

js中的new()到底做了些什么??

要創建 Person 的新實例&#xff0c;必須使用 new 操作符。以這種方式調用構造函數實際上會經歷以下 4個步驟&#xff1a;(1) 創建一個新對象&#xff1b;(2) 將構造函數的作用域賦給新對象&#xff08;因此 this 就指向了這個新對象&#xff09; &#xff1b;(3) 執行構造函數…

Candidate sampling:NCE loss和negative sample

在工作中用到了類似于negative sample的方法&#xff0c;才發現我其實并不了解candidate sampling。于是看了一些相關資料&#xff0c;在此簡單總結一些相關內容。 主要內容來自tensorflow的candidate_sampling和卡耐基梅隆大學一個學生寫的一份notesNotes on Noise Contrastiv…

golang key map 所有_Map的底層實現 為什么遍歷Map總是亂序的

Golang中Map的底層結構其實提到Map&#xff0c;一般想到的底層實現就是哈希表&#xff0c;哈希表的結構主要是Hashcode 數組。存儲kv時&#xff0c;首先將k通過hashcode后對數組長度取余&#xff0c;決定需要放入的數組的index當數組對應的index已有元素時&#xff0c;此時產生…

樸素貝葉斯分類器 文本分類_構建災難響應的文本分類器

樸素貝葉斯分類器 文本分類背景 (Background) Following a disaster, typically you will get millions and millions of communications, either direct or via social media, right at the time when disaster response organizations have the least capacity to filter and…

第二輪沖次會議第六次

今天早上八點我們進行了站立會議 此次站立會議我們開了30分鐘 參加會議的人員&#xff1a; 黃睿麒 侯熙磊 會議內容&#xff1a;我們今天討論了如何分離界面&#xff0c;是在顯示上進行限制從而達到不同引用展現不同便簽信息&#xff0c;還是單獨開一個界面從而實現顯示不同界面…

markdown 鏈接跳轉到標題_我是如何使用 Vim 高效率寫 Markdown 的

本文僅適合于對vim有一定了解的人閱讀&#xff0c;沒有了解的人可以看看文中的視頻我使用 neovim 代替 vim &#xff0c;有些插件是 neovim 獨占&#xff0c; neovim 和 vim 的區別請自行 google系統: Manjaro(Linux)前言之前我一直使用的是 vscode 和 typora 作為 markdown 編…

nginx運用

1、nginx的 命令 start nginx 這樣&#xff0c;nginx 服務就啟動了。打開任務管理器&#xff0c;查看 nginx.exe 進程&#xff0c;有二個進程會顯示&#xff0c;占用系統資源&#xff0c;那是相當的少。然后再打開瀏覽器&#xff0c;輸入 http://127.0.0.1/ 就可以看到nginx的…