目录
第一种,设置root==mainRoot,attchToRoot==false
第二种,设置root==null,attchToRoot==false
一.场景
例如现在我们需要在布局中添加一个如下面的一个跑步提示,我们有多少种方法实现?
-
1.直接在想要插入的xml布局中写
-
2.直接用代码生成布局,在代码中动态的插入(addView)
-
3.单独写一个xml布局,在代码中动态的插入(addView)
最终,选择了第三种办法,-
一是避免页面的复杂度,
-
二是降低xml的加载显示速度。
-
三是相比与第二种,第二种的效率明显会比第三种高(因为xml转换成View需要解析),但是代码生成的布局较难维护和可观性差,还是选择第三种方式。
-
二.如何动态插入View (源码:SDK30)
在讲实现前,我们先看下如何动态插入View,因为这里是通过xml文件来写的布局,所以就涉及到一个xml转换成View的问题,可以通过LayoutInflater类来转换
主要看inflate方法,他有三个参数:
第一个(resource):布局代码,例如R.layout.xx
第二个(root):需要插入的布局的父布局
第三个(attachToRoot):是否需要附加到root上
`public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)`
关于参数的具体逻辑后面再做分析,这里先看下如何使用
三.代码实现
第一种实现方法
1.需要插入的根布局(注意是ConstraintLayout根布局)
//R.layout.activity_main
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_root"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:background="#ff0000"
android:layout_height="match_parent"
tools:context=".MainActivity">
</androidx.constraintlayout.widget.ConstraintLayout>
布局代码和UI效果图:
注意margin,上下左右各留出10dp的距离
核心插入代码:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//mainRoot就是activity_main的根布局
val mainRoot = findViewById<ConstraintLayout>(R.id.main_root)
//把R.layout.layout_run_view布局插入到mainRoot中
LayoutInflater.from(this).inflate(R.layout.layout_run_view, mainRoot,true)
}
看下运行的效果:
可以很明显的看到,左右间隔是有了,但上间隔却没有(下间隔不好表示就不说了,不过原理跟上间隔一样)
到这里,我们再把根布局换成FrameLayout试试(注意代码记得修改),再看下效果图
很明显,换成FrameLayout上间隔就出来,那这到底是什么导致的呢?
原因很简单,这是由于根布局自身导致的。因为ConstraintLayout
与其他布局间隔设置(FrameLayout
,RelativeLayout
,LinearLayout
)的区别就是,ConstraintLayout
需要与向上留出间隔,那么你就需要指定一个他处在的位置,例如` app:layout_constraintTop_toTopOf="parent"
`
解决方法也有,如下:
第一种,设置root==mainRoot,attchToRoot==false
//解析R.layout.layout_run_view,返回View对象,
//attachToRoot == true,返回的就是root
//attachToRoot == false,返回的就是设置LayoutParams的View
val mRunTipView = LayoutInflater.from(this).inflate(R.layout.layout_run_view, mainRoot, false)
val layoutParams = mRunTipView.layoutParams as ConstraintLayout.LayoutParams
layoutParams.apply {
topToTop = ConstraintLayout.LayoutParams.PARENT_ID
}
//把View添加到布局中
mainRoot?.addView(mRunTipView, layoutParams)
第二种,设置root==null,attchToRoot==false
与第一种的区别是,此时inflate
返回的是没有设置LayoutParams的View,就需要我们自己生成一个ConstraintLayout.LayoutParams传进去,如下代码:
val mRunTipView =
LayoutInflater.from(this).inflate(R.layout.layout_run_view1, null,false)
//创建LayoutParams
val layoutParams = ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.MATCH_PARENT,
ConstraintLayout.LayoutParams.WRAP_CONTENT
).apply {
topToTop = ConstraintLayout.LayoutParams.PARENT_ID
}
//把View添加到布局中
mainRoot?.addView(mRunTipView, layoutParams)
运行代码,看下效果。
是的,没有看错,什么都没有。关于原因是因为我们设置的LayoutParams的高是WRAP_CONTENT
,而他布局里又没有子控件自然就没有高度。
解决方法有两个:
1.修改LayoutParams,固定高度如下:
val layoutParams = ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.MATCH_PARENT,
100
)
2.在插入布局中增加子控件或者padding
修改下R.layout.layout_run_view
的布局,代码如下:
仅仅是把layout_height
变成wrap_content
,增加paddingBottom="50dp"
(你也可以在布局里放个50dp的View)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="50dp"
android:layout_margin="50dp"
android:background="#EE000000">
<!--中间代码省略-->
</RelativeLayout>
再看下效果。可以看到黑色块出来了,但是左右上下间距呢?你都没有设置当然就没有了
注意:
1.因为attachToRoot == false,所以inflate返回的是传入布局(R.layout.layout_run_view)的View,而不是mainRoot
2.如果attachToRoot == true,返回的就是mainRoot,也就是我们一开始的插入代码
源码分析(root与attachToRoot区别)
关于inflate
方法的参数传值导致不同的结果,我们可以深入源码看一下(代码经过精简,主要梳理重要逻辑):
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
//1.尝试使用预编译生成View,但是这个版本不支持预编译布局。
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
//通过xml解析resource布局
XmlResourceParser parser = res.getLayout(resource);
try {
//这里才是真正的创建View
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
可以看下tryInflatePrecompiled
的方法,很明显,因为mUseCompiledView
的值一直都是false,所以这个方法暂时没用到的。可以看一下mUseCompiledView
的赋值
private @Nullable View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root,
boolean attachToRoot) {
if (!mUseCompiledView) {
return null;
}
}
跟踪一下mUseCompiledView
的设置路径,可以看到源码的提示
“Precompiled layouts are not supported in this release.”此版本不支持预编译布局
private void initPrecompiledViews() {
// Precompiled layouts are not supported in this release.
boolean enabled = false;
initPrecompiledViews(enabled);
}
回到主流程继续查看inflate
方法
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
View result = root;
//判断布局是不是 merge
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");
}
} else {
// 创建布局(R.layout.layout_run_view)
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
//1.root != null 并且 attachToRoot==false,返回的就只是设置了params的View
if (root != null) {
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
temp.setLayoutParams(params);
}
}
//2.root != null 并且 attachToRoot==true,返回的是root,也就是根布局
if (root != null && attachToRoot) {
root.addView(temp, params);
}
//3.root == null 并且 attachToRoot==false,返回的就只是设置了params的View
if (root == null || !attachToRoot) {
result = temp;
}
}
return result;
}
看到这我们就知道的inflate
方法中ViewGroup root
和boolean attachToRoot
两个参数的区别。
总结一下:
-
root == null,返回的是一个没有设置LayoutParams的View
if (root == null || !attachToRoot) { result = temp; }
-
root != null,attachToRoot == true,返回的就是已经添加完布局的root
if (root != null && attachToRoot) { root.addView(temp, params); }
-
root != null,attachToRoot == false,返回的就是设置LayoutParams的View
if (root != null) { params = root.generateLayoutParams(attrs); if (!attachToRoot) { temp.setLayoutParams(params); } }
四.总结
-
在代码中,
ConstraintLayout
与其他布局的区别。在xml中很常见,但是在代码中却容易疏漏。 -
在需要在代码中插入布局时,可分为以下几种
2.1. 不需要返回生成的View,直接插入到对应的布局中LayoutInflater.from(this).inflate(R.layout.layout_run_view, mainRoot,true) //或者 LayoutInflater.from(this).inflate(R.layout.layout_run_view, mainRoot)
2.2. 需要返回生成的View,可能需要对View进行一些动画操作,比较方便
val mRunTipView = LayoutInflater.from(this).inflate(R.layout.layout_run_view, mainRoot,false) mainRoot?.addView(mRunTipView) //mRunTipView 旋转,平移。。。
2.3. 需要返回生成的View,自己对View的位置大小进行修改val mRunTipView = LayoutInflater.from(this).inflate(R.layout.layout_run_view, null) //等价于下面这种 val mRunTipView = LayoutInflater.from(this).inflate(R.layout.layout_run_view, null,false) //创建LayoutParams val layoutParams = ConstraintLayout.LayoutParams( 100, 100 ).apply { topMargin = 100 } //把View添加到布局中 mainRoot?.addView(mRunTipView,layoutParams)