“队友呢队友呢 救救我!我这边动态加载的view layout属性怎么失效了…”
“兄弟,你真的懂LayoutInflater. inflate()吗?”
”嗯?“
”且听我娓娓道来“
进入主题前先了解几个概念:
- 在xml布局文件里面的 layout_witdh等都是 指 我这个view 在容器里面的大小, 如果前提要容器,而容器就相当于是我们的父view。比如:我这里的 textview的父view 就是我的Linearlayout,也就是我textview的容器
- 根布局是布局文件的最外层布局,如下面代码的LinearLayout (id=“@+id/mroot”)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mroot"
android:orientation="vertical"
android:layout_height="match_parent"
android:layout_width="match_parent">
<TextView
android:id="@+id/sonview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
懂了这个概念我们再来看一下layoutinflater的inflate方法
inflate有四个重载方法,而常用的有俩个。
先来讲3个参数的,懂了这个就懂其他重载方法了
第一个参数:resource, 是我们要加载的布局文件
第二个参数:root,为第一个参数resource这个布局文件的根布局(即布局文件最外层的view)指定父view为root (是否指定还要看第三个参数)
第三个参数:attachToRoot,是否让root成为resource根布局的父布局(也就是作为根布局的容器)
先讲一下结论:后再根据源码分析
分为三种情况:
- root 为 null, 也就是我这个resource没有父布局(即没有容器可以放),那么我的resource布局文件的最外层的布局(根布局)所有属性都会失效。 (此时返回值为resource的根布局,即最外层的布局)
- root不为null,且attachToRoot为true,将root指定为resource的父布局,resource的根布局属性生效。(有了父容器) (此时返回值为root,即第二个参数)
- root不为null,且attachToRoot为false, 不将root指定为resource根布局的父布局,但此时resource的根布局属性生效。(这里因为root不为null,有root帮忙生成layoutparams也就是属性,然后设置给我这个resource的根布局) (此时返回值为resource的根布局,即最外层布局)
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mroot"
android:orientation="vertical"
android:layout_height="match_parent"
android:layout_width="match_parent">
</LinearLayout>
layout_text.xml
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mbt"
android:text="按钮"
android:layout_gravity="center"
android:layout_height="399dp"
android:layout_width="200dp"
>
</Button>
注意看我的button的高度和宽度
第一种情况:root等于null (此时inflate返回值是最外层的布局,但最外层的布局属性值无效)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewGroup parentView = findViewById(R.id.mroot);
View view = LayoutInflater.from(this).inflate(R.layout.layout_text, null, true);
parentView.addView(view);
}
在这里不管我第三个参数是false,还是true,我这个button的属性的失效了
不管怎么调整大小,button都将是这个样子
第二种情况:root != null && attchToRoot == true
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewGroup parentView = findViewById(R.id.mroot);
View view = LayoutInflater.from(this).inflate(R.layout.layout_text, parentView, true);
// parentView.addView(view);
}
效果:
button属性值生效了。 (因为此时我的resource的根布局(在这里是button)有了容器)
此时细心的人会发现我把addview这一行给注释掉了。
那么为什么要注释掉呢?
因为我第三个参数为true时已经将root指定为resource的父布局了,此时返回值就是root,这里root就是我们传进来的parentView,因此此处的parentView和view是同一个实例。
如果我不注释掉这行代码会发生什么事呢?
看到了吧,它会报错。 原因是此时俩个实例是一样的。
第三种情况:root != null && attchToRoot == false
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewGroup parentView = findViewById(R.id.mroot);
View view = LayoutInflater.from(this).inflate(R.layout.layout_text, parentView, false);
parentView.addView(view);
}
此时button的属性生效。
此时细心的同学又会发现我的addview又 没有注释了。
这里没有注释 是因为 第三个参数为false的时候 就没有把root指定为resource根布局的父布局,返回值就是我们resource这个布局文件的根布局,因此这里的linearlayout(activity_main的根布局)需要手动addview一下,否则inflate加载出来的resource(即button)就不会显示在linearlayout里面了。
再然后又有疑问?
我即然给出了root,为什么还要将attachToRoot弄为false呢? 直接传个root = null不就行了。
从源码分析并不能这样。当root = null就成了第一种情况了。 至于为什么这里为false的时候属性还生效呢,是因为 当 root != null && attchToRoot == false 的时候,root就会为resource的根布局生成属性参数,并且设置给它,因此此时的属性才可以生效。
源码:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
advanceToRootNode(parser);
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
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
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
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) {
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(
getParserStateDescription(inflaterContext, attrs)
+ ": " + 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;
}
}
从上面的源码可以看出,当root != null && attchToRoot == true的时候,会执行这一行代码:
if (root != null && attachToRoot) {
root.addView(temp, params);
}
这时root会把resource解析出来的view add进去,此时的params为null, 由上面的源码可以看出。
而当root != null && attachToRoot = false的时候会执行这个:
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
此时我resource根布局的属性值 由 root帮忙生成,并设置进去。因此button的属性值(resource根布局的属性值)生效了。 根据源码的注释也可以清晰的看出
然后又有人有疑问了,我Linearlayout根布局为什么会生效,
跟踪一下源码:
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
发现setContentViews 实际上调用的也是inflate这个方法,但此时是二位参数的,我们再跟踪一下,如下:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
事实上不管是几个参数的inflate方法,最后都会调用inflate三个参数的方法。
此时更加清晰了。 这里setContentViews()其实执行的是上面的第二种情况。
那么问题来了,我这个linearlayout的最外层已经没有父布局了啊,按理说这里的linear应该不生效才对!
其实是这样子的。你看到的上面activity_main的linearlayout没有父布局其实是一种假象。
我们的页面中有一个顶级View叫做DecorView,DecorView中包含一个竖直方向的LinearLayout,LinearLayout由两部分组成,第一部分是标题栏,第二部分是内容栏,内容栏是一个FrameLayout,我们在Activity中调用setContentView就是将View添加到这个FrameLayout中
由上面的代码也可以看出
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
因此此时根布局的属性生效。
当然这个大家可以到sdk->tools->monitor.bat下查看布局就可以清晰的看到补局内容了。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/0cc8d8b99ee93aa04ed8daa5f40a8dc9.png)
最顶层的View这里现实了FrameLayout是因为DecorView是继承至FrameLayout
或者也可以去搜搜其他文章 写关于view布局相关的文章。
最后关于返回值可以通过下面源码分析和代码实践得出:
源码:
根据注释也可以知temp是resource的根布局。
从源码的注释也可以清晰的看的出,当满足一或三的条件是返回值就是resource的根布局(最外层布局),而满足第二个条件的时候,返回的是root,即第二个参数。(可能是因为已经执行了root.addView(temp, params)了,所以直接返回root更合理一点。)
代码:
son_layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/mSonParent"
android:layout_height="50dp"
android:layout_width="50dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<Button android:layout_height="300dp"
android:id="@+id/mson"
android:text="按键"
android:layout_width="300dp"
android:layout_gravity="center"
xmlns:android="http://schemas.android.com/apk/res/android">
</Button>
</LinearLayout>
parent_layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/mparent"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android">
</LinearLayout>
setContentView(R.layout.activity_parent);
ViewGroup mParent = (ViewGroup)findViewById(R.id.mparent);
Log.d("jian", "onCreate: " + "parent : " + mParent.getId());
View mSon = getLayoutInflater().inflate(R.layout.activity_mson, null, false);
Log.d("jian", "onCreate: " + "mSon : " + mSon.getId());
View son = mSon.findViewById(R.id.mson);
Log.d("jian", "onCreate: " + "son : " + son.getId());
mParent.addView(mSon);
这下诸如 什么layout属性不生效,但在外面嵌套了个父布局如linearlayout等就后属性又生效了的情况就知道了吧!!!
文章最后再来唠一唠 在自定义view 中的 onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
LayoutParams layoutParams = getLayoutParams();
- LayoutParams和MeasureSpec有什么关系?
这里简要说一些:
- LayoutParams指的是xml布局文件里已layout开头的属性。
- 而MeasureSpec则是 一种 压缩 数据。 由Mode + size组成。 比如上面的widthMeasureSpec。 它是int类型,也就是32位。 这里做高俩位用来存放mode,后面30位用来存放size。
- 而mode有三种:
-
UNSPECIFIED:父容器不做限制,View要多大给多大,一般是系统内部使用。
-
EXACTLY:父容器已经检测出View所需大小,具体值由SpeacSize决定。我们定义View的Layout_width,layout_height 为match_parent 或者具体数值时,就是EXACTLY。
-
AT_MOST:父容器不知道View要多大,根据View实际情况取值,但是最大不超过父容器的宽高。即使用wrap_content属性时,view的内容不确定,可以根据内容改变宽高值,但是最多只能和父容器一样大。
至于这三种在自定义view中扮演什么样的角色,以及怎么用可以在网上搜搜相关文章,写的很详细,这里不做多介绍。