凑个热闹-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:
每个方法都和 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)的三个参数:
被广泛讨论的是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不同传参的影响:
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标签。其中:
在ListView的自定义Adapter中,应该都有用到过View的setTag方法,即:使用ViewHolder来重复利用View。
parseViewTag方法:
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);} 复制代码createViewFromTag方法
正常View标签都是通过createViewFromTag来创建对应的View对象的。
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的内部类——两个接口:
Factory2继承了Factory,增加了一个带View parent参数的onCreateView重载方法。它们是在createViewFromTag中被调用的,默认为null,说明开发人员可以自定义这两个Factory,则通过它们可以改造待加载XML布局中的View标签,来使用自定义规则创建View。
来看一下它们的设置方法:
可以看到Factory和Factory2只能设置一次,否则会抛异常。
这两个Factory的区别是什么?
那如何应用呢?
Factory2/Factory的应用
AppCompatActivity中的应用
先看一张图:
这个布局中使用的是正常的标签<TextView>和<Button>,但通过Layout Inspector工具分析页面会发现它们被替换成了AppCompatTextView和AppCompatButton。
跟踪一下AppCompatActivity的onCreate方法:
委托到了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类:
通过name参数拿到TextView标签后,直接替换成了AppCompatTextView。
通过这波操作,将一些 widget 自动变成兼容widget (例如将 TextView 变成 AppCompatTextView)以便于向下兼容新版本中的特性。
那我们也可以仿照AppCompatActivity来自定义Factory实现自己需要的替换效果。
自定义Factory2
大多换肤功能的实现就是通过实现自定义Factory,拦截特定View,然后修改这些View的属性值,或者直接返回自定义的View。举个栗子:
先看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");}public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {return onCreateView(name, context, attrs);}public 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相关分析的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: sqlmap手册
- 下一篇: 精通Quartz-入门-Job