Android学习进阶 - 搞懂LayoutInflater的inflate过程

本文主要介绍:LayoutInflater

本文适合对象:打算搞懂LayoutInflater的inflate的开发者

本文字数:约2.5万,阅读时间:约2H

问题

先从表象入手

在Android开发过程中,很多地方都不可避免的使用到inflate方法,如在使用RecycleView的时候构建ViewHolder对象,给Fragment进行CreateView

我们通常是inflater.inflate(R.layout.xxx, container, false)或者LayoutInflater.from(parent.context).inflate(R.layout.xxx, parent, false)来调用inflate方法的,不难发现,inflate方法的作用是将一个 xml 布局文件变成一个 view 对象。然而仅仅是根据模板,按照固定的”规律”去修改某些参数来实现目标,只能叫做是「使用」而已

那么,我们就来将它「分解」成明确的「问题」,来具体的「学习」吧

  1. LayoutInflaterinflater 这些语句的「头部」是什么?怎么来的?
  2. inflate 方法的「参数」是什么意思,有什么用?
  3. 这些语句是怎么实现转换 xmlview 的?
  4. 我除了常见的用法还能怎么用它

Question One:「头部」

官方文档

思考的First Step,问其所来

官方文档LayoutInflater的说明如下:

官方文档

简单的翻译过来就是:

  1. 这玩意是用来将 xml 转换为 view
  2. 这玩意不能直接new初始化,通过ActivitySystemService获取,你也可以自定义他的工厂方法
  3. 因为性能问题,他只能把写在layout里被预处理过的 xml 转换为 view ,不能随便找个xml文件就让他转换

回归表象

那好了,第一个问题解决了,LayoutInflater是一个不能直接new的类,他来管 xml 转换为 view ,我们在adapter里通过LayoutInflater.from(context)获取实例,fragment则是直接使用了FragmentManager调用Fragment.onCreateView的时候传过来的inflater对象

Question Two:「方法」

inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) 方法有三个参数,第一个参数很好理解,就是前文所说的, xml 转换为 viewlayout xml 对应的资源ID。第二第三个参数又什么意思呢?我转换成View为什么需要它呢?

官方文档

官方文档

简单翻译:

  1. root是要转换的 xml 将要存在的外部ViewGroup
  2. xml 转换成 view 后要不要 addViewroot ( ViewGroup )

这是个啥意思呢,看了之后似懂非懂,我还是一脸懵逼。

测试

纸上学来终觉浅,只是看文档还是不行,不如自己上手试试,把自己脑子里的可能性都弄出来试试看效果,跑出来啥样就是啥样了。

按排列组合来说,我们一共有四种(如果你想到更多可能性,不妨自己写出来跑跑看)

  1. root = null, attachToRoot = false
  2. root = null, attachToRoot = true
  3. root = viewgroup, attachToRoot = false
  4. root = viewgroup, attachToRoot = true

接下来我们一个个实验,实验的过程为,通过activitygetLayoutInflater()方法获取inflater对象,调用其inflate方法,传递不同的参数,将得到的view添加到activity布局的viewgroup中,查看结果。

首先是布局展示,activity的布局只有一个蓝底的ViewGroup,而要加载的view也只是一个黄色的View

ViewGroup
View

注意我给蓝底加了一句android:paddingTop="32dp",黄底加了一句android:layout_margin="4dp"

测试①

View

我们看到黄色的view几乎填满了整个activityviewwidthheightmargin都无效,但是viewgrouppadding是有效的。

但是我们还不能确定是root = nullattachToRoot = false中哪个的原因,我们继续测试

测试②

View

我们可以看到黄色的view里面设置的width height margin还是无效的,但是viewgrouppadding是有效的。

通过这两个测试,我猜测root的效果是控制 xml 里关于layoutparam的设置是否有效,但是不是这样还要看接下来的测试。而viewgroup的padding参数是不受影响的,这个也好理解,因为是ViewGroup的属性,在onDraw方法里处理的。

测试③

View

我们可以看到黄色的view里面设置的width height margin也都有效了

也就是说,root的猜测基本是坐实了,接下来就剩attachToRoot还是一头雾水了

测试④

View

Crash!出问题了,看看报错信息:

The specified child already has a parent. You must call removeView() on the child's parent first.

这娃儿已经有个爹了,你要当他爹得先让他现在的爹removeView()

啥意思,已经有个爹了?这爹是谁,他转换的过程也就接触到一个viewgroup啊,难道说attachToRoot = true的话就直接addView()了?试试看

测试⑤

View

果然和我们想的一样……那么,可以总结一下了

总结

  1. root参数将决定viewlayoutparam,如果为null,那xml里定义的最外层viewlayoutparam将全部无效
  2. attachToRoot表示是否需要一键addView(),如果root为null,那这个参数将被自动忽略

Question Three:「How?」

源码

弄懂了工作的效果,为了知道工作的原理,我们就要看看工作的流程了

接下来我将在代码中用注释方式来解释代码内容

inflate(resource,root,attachToRoot)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
//利用inflater对象构建的时候的context获取资源
final Resources res = getContext().getResources();

//DEBUG模式的日志
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}

//XML解析器
final XmlResourceParser parser = res.getLayout(resource);
try {
//之所以要try的原因是会抛出转换异常,比如你xml写了矛盾的东西
//但是try了之后没有catch,所以出了异常还是崩溃
//try的意义仅仅是为了finally里关闭资源
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}

由于XmlResourceParser只是XML解析器,对应生成对象,所以这里不写,有兴趣的可以自己深入去看。

这里我们发现跳转到了一个同名的重载方法inflate(parser, root, attachToRoot),接下来看看这个吧

inflate(parser,root,attachToRoot)

仔细查看源码发现,这是真正的转换方法,所有转换最后都会汇聚到这个方法。在看这个方法前,我们可以先了解一下他的JDoc文档

JDoc

简单翻译如下:

  1. xml 转换成 view 的过程重度依赖xml在编译期间预处理生成的文档,这样能提高运行时生成View的速度
  2. 出于以上考虑,不能在运行时转换没有预处理的 xmlview

如果能直接转换xml的话,也许热更新又多了一条歪路也说不定,23333

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
// 锁mConstructorArgs,相同的inflater对象同步进行
// 因为下面会对这个变量进行修改,不过结束的时候会复原
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

// mContext是LayoutInflater.from(context)的context
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
// mConstructorArgs[0]临时使用inflaterContext(mContext)的值,
// 而其原先值临时存到lastContext中
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;

// 置为root,如果root != null && attachToRoot = true
// 那么addView()后就直接返回root
View result = root;

try {
// 查找根节点
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// 跳过所有非START_TAG(<...>)和END_DOCUMENT
}

// 可能出现的情况有:
// 1.找到了START_TAG;
// 2.没找到START_TAG(遇到END_DOCUMENT结束)
// 而没有START_TAG意味着xml内容错误,将抛出异常
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}

// 解析根布局名,如刚才Q2的测试,这里就会是LinearLayout,View
final String name = parser.getName();

if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}

// <merge>标签单独判断
if (TAG_MERGE.equals(name)) {
// <merge>只能在root!=null且attachToRoot=true时使用
// 否则抛异常
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 {
// 不是<merge>表明可以作为根元素,命名为temp
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
// 创建了LayoutParams,需要用root来实例化
ViewGroup.LayoutParams params = null;
// 判断root是否为空
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// 用root来构建temp的LayoutParams
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// attachToRoot为false,那就直接设置LayoutParams
// 否则之后会执行addView()
temp.setLayoutParams(params);
}
}

if (DEBUG) {
System.out.println("-----> start inflating children");
}

// 等于是把temp作为新的root,attachToRoot设为true
// 转换剩下来的部分
rInflacomildren(parser, temp, attrs, true);

if (DEBUG) {
System.out.println("-----> done inflating children");
}

// 前面说的,root != null && attachToRoot = true的情况
// 不会直接直接设置LayoutParams,而是addView()
if (root != null && attachToRoot) {
root.addView(temp, params);
}

// 不需要addView的话,就返回temp
if (root == null || !attachToRoot) {
result = temp;
}
}

} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;

Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

return result;
}
}

这样我们就对inflate方法有了一个大致了解,也知道我们实验出来的结果的原理了。

那么接下来我们看看inflate方法里调用的另外俩方法吧

rInflate(parser,parent,context,attrs,finishInflate)

想查文档,但是好像是因为是default的方法,官网只有public的方法文档……

不过我在源文件里找到了文档,一起来看看:

1
2
3
4
5
6
7
/**
* Recursive method used to descend down the xml hierarchy and instantiate
* views, instantiate their children, and then call onFinishInflate().
* <p>
* <strong>Note:</strong> Default visibility so the BridgeInflater can
* override it.
*/

简单来说:rInflater 指的是 Recursive 递归xml布局来初始化view,可以想象xml布局嵌套层数一高会造成多大的性能问题……

源码,请:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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;

// 遍历到遇到END_TAG,或者END_DOCUMENT为止
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();

// 如果设置了TAG_REQUEST_FOCUS
if (TAG_REQUEST_FOCUS.equals(name)) {
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)) {
// 子View不能有merge标签,merge只能用在根布局
throw new InflateException("<merge /> must be the root element");
} else {
//通过createViewFromTag构建这个view
final View view = createViewFromTag(parent, name, context, attrs);
// 利用parent作为ViewGroup,构建出LayoutParams,并赋值予子View
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflacomildren(parser, view, attrs, true);
// addView到ViewGroup
viewGroup.addView(view, params);
}
}

// TAG_REQUEST_FOCUS的处理
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}

// finishInflate是方法传参进来的
if (finishInflate) {
parent.onFinishInflate();
}
}

看起来和inflate差不多,就真的只是递归而已,真正生成view的方法应该就是那个createViewFromTag

View createViewFromTag

这个方法的签名有点长,就不写子标题上了

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr)

最后一个参数可不写,默认为false

同样,这也是一个default的方法,文档在代码里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Creates a view from a tag name using the supplied attribute set.
* <p>
* <strong>Note:</strong> Default visibility so the BridgeInflater can
* override it.
*
* @param parent the parent view, used to inflate layout params
* @param name the name of the XML tag used to define the view
* @param context the inflation context for the view, typically the
* {@code parent} or base layout inflater context
* @param attrs the attribute set for the XML tag used to define the view
* @param ignoreThemeAttr {@code true} to ignore the {@code android:theme}
* attribute (if set) for the view being inflated,
* {@code false} otherwise
*/

简单来说,就是根据提供的attribute set,标签名,来构建View

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}

// 判断ignoreThemeAttr,给context配置Theme
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}

// !?这啥,1995?party?
// 仔细一看发现BlinkLayout还是LayoutInflater的私有内部静态类
// 试了一下发现是一个ViewGroup,效果是0.5秒闪烁一次
// 可以可以,这很1995的patry,disco舞厅闪光灯
// 看源码送彩蛋,可还行233333
if (name.equals(TAG_1995)) {
// 下面这个是官方注释↓
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}

try {
View view;
// 这是一个找factory的过程
// 找mFactory2,mFactory,mPrivateFactory
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 {
// 没有'.'就表示是原生的View,不需要库文件的
if (-1 == name.indexOf('.')) {
// onCreateView其实就调用了
// createView(name, prefix:"android.view.", attrs)
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
// 结束,返回
return view;
} catch (InflateException e) {
throw e;

} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;

} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}

发现这是一个解析tag的,还是不是创建view的,不过发现了一个彩蛋,蛮有意思的,写了一个,大概是这样的效果:

彩蛋

createView应该就是今天源码的终点了,来看看吧

View createView(name,prefix,attrs)

这次有官方文档了

文档

说的是,这个是根据名字实例化View的底层方法,虽然说是public,但是直接调用的时候必须处理 抛出异常返回值为 null 的两种情况

搓手手看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
// 全局静态HashMap缓存
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;

try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
// 查看缓存是否存在
if (constructor == null) {
// 没得缓存,自行反射加载
clazz = 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);
}
}

// 反射获取类构造方法
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
// 添加到缓存
sConstructorMap.put(name, constructor);
} else {
// 就算已经缓存了,也要通过mFilter进行检查
if (mFilter != null) {
// filter对象:咱们见过吗?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// 是新对象! -- 不管行不行,先记小本本上
clazz = 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实例,args是自定义主题相关的变量
final View view = constructor.newInstance(args);
// 如果是ViewStub
if (view instanceof ViewStub) {
// 等会用同一个Context加载这个ViewStub的LayoutInflater
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
mConstructorArgs[0] = lastContext;
return view;

} catch (NoSuchMethodException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;

} catch (ClassCastException e) {
// If loaded class is not a View subclass
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassNotFoundException e) {
// If loadClass fails, we should propagate the exception.
throw e;
} catch (Exception e) {
final InflateException ie = new InflateException(
attrs.getPositionDescription() + ": Error inflating class "
+ (clazz == null ? "<unknown>" : clazz.getName()), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

稳了,总算看到了怎么实例化view的,也不过是用反射,不过还做了两层缓存,一层全局,一层在inflater对象里

总结

inflate方法先把最外层的root弄好,然后用rInflate去递归把子view都弄好,子viewcreateViewFromTag方法去解析tag,用createView反射出view,全都弄完再返回。

很明显,整个过程中最耗时(ANR)的地方有两处:

  1. 解析XML
  2. 反射获取实例

而Google做的优化是:

  1. 预编译
  2. 缓存

而我们可以做的就是,减少布局层次,降低复杂度。其实这样除了能减少inflate的时间,还能减少measure layout draw的时间,不过不是本期重点,就不细讲了。

Question Four:「So what?」

好了,我现在懂了,有什么用呢?

不难想象,在开发中,总能见到一些非常奇怪的需求,这些需求实现起来味如嚼蜡,又不得不做,接下来我就给大家讲一个我遇到的奇怪需求:

收藏页

这是一个音乐收藏,产品的需求是,按心情->日期->用户自定义Tag 来进行分类

可能粗看之下发现不了其中的坑点,我来给大家讲讲

  1. 数据处理的逻辑
    数据库中有一个表,存收藏歌曲,信息除了歌曲的基本信息外,还有收藏心情收藏日期自定义Tag。当我要打开这个页面的时候,我需要去数据库中把信息都读出来,作为一个List<Music>,然后再进行分类。说的夸张点,把数据处理合在一起:①心情 Map<Mood,List<Music>>,②日期(需要排序) Map<Mood,List<Pair<Date,List<Music>>>>,③自定义Tag Map<Mood,List<Pair<Date,Map<String,List<Music>>>>>。数一数发现有5层<>
  2. 绘制页面
    这个页面特殊的地方,肯定只有中间的列表,这个列表是一个”常见”的嵌套列表
    而不同于常见嵌套列表的一点在于,他的第二层列表,是以类似CardView的卡片包裹的,也就是说,是不能通过给RecycleView设置ViewType来解决问题的。

抛开实现说一下第二点的坑,如果某天用户心血来潮,收藏了10多首歌曲,那这个卡片也是真的很长了(笑)

第一点问题这里不表,第二点就用到了本期所说的新操作。

我的想法是,外层是一个RecycleView,然后里面是CardView套一个LinearLayout,通过数据动态的添加一行Music的View

先看布局,再看代码

CardView

这是最外层RecycleView的ViewHolder得到的View布局,CardView套LinearLayout,那个时间是一个Music只有一个,所以直接写上去了。

Item

第二个是ItemView,代码就不用看了,总之这是最后的效果

Title

这是用户自定义TAG的标题

好了,前戏就到这里,我们可以看代码的具体实现了,因为是实际开发的真实代码,开发语言是Kotlin,所以代码是不是Java写的……同样,还是会写上注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private fun View.initView(date: String, stars: Map<String, List<Music>>) {
tv_date.text = date

// 为了防止重复利用ViewHolder的时候直接添加过多的item
ll_container.apply {
if (childCount > 1) {
val view = getChildAt(0)
removeAllViews()
addView(view)
}
}

val inflater = LayoutInflater.from(context)
val g = Glide.with(context)

stars.forEach { (describe, list) ->
// 先添加一个自定义Tag,因为有对view进行其他的处理,所以不能直接设attachToRoot为true
ll_container.addView(inflater.inflate(R.layout.item_star_item_title, ll_container, false).apply {
// 一些没必要看的处理
tv_describe.text = describe
})
// 再foreach这个tag下的所有view,添加进去
list.forEachWithIndex { i, it ->
ll_container.addView(inflater.inflate(R.layout.item_star_item_music, ll_container, false).apply {
// 一些没必要看的处理
g.load(it.album.blurPicUrl).into(riv_music_img)
tv_music_name.text = it.name
tv_singer_name.text = it.artistNameStr
setOnClickListener {
onMusicSelect(list, i)
}
})
}
}
}

当然,这只是举例了inflate的一种用法,他的存在让你可以直接在代码里将xml转为view。只要你的思维足够广泛,就能玩出各种各样的花样,尤其是写自定义View的时候

写在最后

这是我加入红岩网校学习一年后,发在红岩网校工作站知乎专栏下的第一篇”博客/技术专栏”,在写这篇文章的时候,我能感受到红岩网校带给我的成长,成长到我已经可以自己去分析去理解,去把这些技术写出来,分享出来了,真开心啊

感谢你的阅读,最后请容我加上一句:

Redrock NB!

文章目录
  1. 1. 问题
  2. 2. Question One:「头部」
    1. 2.1. 官方文档
    2. 2.2. 回归表象
  3. 3. Question Two:「方法」
    1. 3.1. 官方文档
    2. 3.2. 测试
      1. 3.2.1. 测试①
      2. 3.2.2. 测试②
      3. 3.2.3. 测试③
      4. 3.2.4. 测试④
      5. 3.2.5. 测试⑤
    3. 3.3. 总结
  4. 4. Question Three:「How?」
    1. 4.1. 源码
      1. 4.1.1. inflate(resource,root,attachToRoot)
      2. 4.1.2. inflate(parser,root,attachToRoot)
      3. 4.1.3. rInflate(parser,parent,context,attrs,finishInflate)
      4. 4.1.4. View createViewFromTag
      5. 4.1.5. View createView(name,prefix,attrs)
      6. 4.1.6. 总结
  5. 5. Question Four:「So what?」
  • 写在最后