目录
一.场景
二.如何动态插入View (源码:SDK30)
三.代码实现
第一种实现方法
第一种,设置root==mainRoot,attchToRoot==false
第二种,设置root==null,attchToRoot==false
源码分析(root与attachToRoot区别)
四.总结
一.场景
例如现在我们需要在布局中添加一个如下面的一个跑步提示,我们有多少种方法实现?
二.如何动态插入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,false)
//创建LayoutParams
val layoutParams = ConstraintLayout.LayoutParams(
100,
100
).apply {
topMargin = 100
}
//把View添加到布局中
mainRoot?.addView(mRunTipView,layoutParams)
|