凑个热闹-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.mzph.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的…