View的动态添加-Android

android11.png
Android 11 (Image credit: Shutterstock / TechRadar)

在安卓小列表展示的情况下,我们应该使用什么样的UI结构?使用写死的xml?很容易缺乏灵活性,如果列表变动就会需要更改多个模块。使用recycleview?虽然拥有灵活性但是太重,有杀鸡用牛刀之感。在实习的过程中我就碰到了这样的问题,通过同事的指点学习了为视图动态添加view的方式。在学习的过程中,我发现相关的文章相对较少,而且很多文章是在kotlin/java文件中添加布局,而不是xml文件,灵活性相对较差。所以在此与大家分享这个问题上我的实践方式。

布局文件

在动态生成布局的时候,我们应当使用一个诸如LinearLayout或是ConstraintLayout作为我们动态视图的容器。这样能够很方便定位动态view的位置。同时,这给我们kotlin代码中确定父视图提供了方便。注意不要使用EditText或是TextView,当然我觉得大部分人不会无聊到想测试安卓代码的鲁棒性。

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
<!--activity_main.xml-->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/test_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/test_title" />

<!--The Container-->
<LinearLayout
android:id="@+id/test_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

</LinearLayout>

</LinearLayout>

在上述的布局文件中,test_container就是我们的父容器。接下来就是动态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
<!--view_item.xml-->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="260dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:background="@color/grey">

<ImageView
android:id="@+id/item_avatar"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center_vertical"
android:src="@drawable/samira"
android:contentDescription="@string/test_title" />

<TextView
android:id="@+id/item_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:text="@string/item_test_content"/>
</LinearLayout>

上述的很多地方都可以根据需求灵活地改变,但是有两点是确定的:我们需要有一个父容器,以及一个子容器的xml布局。

在Kotlin文件中结合父子布局

既然布局文件很简单,那么很多的工作就是在Kotlin中完成了。下面首先是完整代码:

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
class MainActivity : AppCompatActivity() {

private lateinit var championConfig: MutableList<ChampionConfig>

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initChampionConfig()
// 获取父容器
val container = findViewById<LinearLayout>(R.id.test_container)
for(config in championConfig){
// 动态加载子项,并挂载到父容器
val v: View = layoutInflater.inflate(R.layout.view_item,container, false)
val avatar = v.findViewById<ImageView>(R.id.item_avatar)
avatar.setImageResource(config.avatarId)
val championName = v.findViewById<TextView>(R.id.item_name)
championName.text = config.championName
v.setOnClickListener{
config.clickFunction()
}
// 如果inflate第三个参数是false,这里需要手动addView
container.addView(v)
}
}

// 数据初始化
private fun initChampionConfig(){
championConfig = mutableListOf()
championConfig.add(ChampionConfig(R.drawable.kayn,"Kayn"){
Log.d("MainActivity","This is kayn")
})
championConfig.add(ChampionConfig(R.drawable.samira,"samira"){
Log.d("MainActivity","This is samira")
})
championConfig.add(ChampionConfig(R.drawable.khazix,"khazix"){
Log.d("MainActivity","This is khazix")
})
}
}

// 数据配置类
data class ChampionConfig(
var avatarId: Int,
var championName: String,
var clickFunction: ()->Unit
)

这里的核心部分就是两句话:inflate和addview。获取父容器非常简单我们就不再赘述了。在获取到父容器后,我们使用inflater动态加载子项布局。inflate的三个参数分配代表子项布局id,要挂载的父布局,是否立刻挂载。如果第三个参数我们选择false,我们就需要使用container.addview手动添加子项的view。这里有一个小坑,细心的同学可能发现,这个addview,似乎就能确定父子容器的关系啊?那inflate的第二个参数我们可以传null吗?这是可以的,但是这样会导致子项根节点(这里是view_item.xml中的LinearLayout)属性失效。通过下图我们能够发现,如果设定为null则view_item的宽高margin全部失效了,这是非常不符合预期的。

root-view-compare.png

另外一个值得一提的点是如果你需要绑定点击事件,那么最好在添加子视图的时候直接添加。实际上我们在父容器中添加了很多个带有相同id的view,如果添加完动态view后再修改我们就不能使用id获取到控件了,处理起来会比较麻烦。

感受

在感受了Vue的美好之后,安卓麻烦的布局文件确实震惊到我。为什么不能够像vue的v-for一样动态添加节点,动态设定id?查阅了资料后我发现这个问题在安卓最大的阻碍是xml文件的编译。因为xml的编译占了大量的CPU处理时间,我们不能够在运行时动态修改xml。导致动态布局的实现及其麻烦。能否开发一种新的布局方式优化布局的问题?我认为这或许会是一个非常有趣的研究方向。

参考资料

Android两种方式实现动态添加View 因为他将root设定为null了,导致我第一次调试样式一直不对

android 动态添加控件并实现每个子控件的点击事件