ConstraintLayout 的一个小技巧

需求背景

在做一些账号相关的功能时,我们经常有以下场景,需要展示一个人的头像、名称,名称后要跟一个小标签,比如勋章/成就图标、定位图标、等级图标等,大致如下:

用常规方式实现上面这种布局,如下:(为了简化代码,尽可能看到关键的约束条件和参数,我写了点 style 把不太相关的参数统一声明了,后面的代码示例也是这样,不做赘述)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_container_0"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">

<ImageView
android:id="@+id/iv_avatar_0"
style="@style/ConstraintLayoutTips.Avatar"
app:layout_constraintStart_toStartOf="parent" />

<TextView
android:id="@+id/tv_name_0"
style="@style/ConstraintLayoutTips.Name"
app:layout_constraintStart_toEndOf="@id/iv_avatar_0" />

<ImageView
android:id="@+id/iv_position_0"
style="@style/ConstraintLayoutTips.Position"
android:layout_marginStart="10dp"
app:layout_constraintStart_toEndOf="@id/tv_name_0" />

</androidx.constraintlayout.widget.ConstraintLayout>

但是名称是用户自己定义的,有长有短,遇到太长的名字时,name 控件会穿出屏幕边界,把右侧的 position 挤得不可见,如下:

实际上我们想要 position 控件跟着 name 控件的长度改变位置,但是最右只能去到屏幕边界,不能被挤出去,同理 name 控件既要根据文字内容来展示,又要在宽度即将超出时进行折叠,末尾显示为 … 的形式,效果如下:

要达到这种效果,直接沿用上面的三个控件的结构,暂时我还没有办法做到,这时需要加入第四个控件(如果需求里跟在名称后的控件有两个,就省了这个操作,如果没有,直接加一个 1dp 宽高的占位 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
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_container_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/cl_container_0">

<ImageView
android:id="@+id/iv_avatar_1"
style="@style/ConstraintLayoutTips.Avatar"
app:layout_constraintStart_toStartOf="parent" />

<TextView
android:id="@+id/tv_name_1"
style="@style/ConstraintLayoutTips.Name"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@id/placeholder_view_1"
app:layout_constraintStart_toEndOf="@id/iv_avatar_1" />

<!-- 宽高 1dp 的占位 View,仅用于实现约束效果,并没有其他作用 -->

<View
android:id="@+id/placeholder_view_1"
style="@style/ConstraintLayoutTips.Placeholder"
android:layout_marginStart="10dp"
app:layout_constraintStart_toEndOf="@+id/tv_name_1" />

<ImageView
android:id="@+id/iv_position_1"
style="@style/ConstraintLayoutTips.Position"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@id/placeholder_view_1" />

</androidx.constraintlayout.widget.ConstraintLayout>

与原始布局的不同点在于:

  1. name 控件多了 app:layout_constrainedWidth=”true” 属性,而且右侧约束 placeholder_view
  2. 多了一个 placeholder_view 占位控件,placeholder_view 左侧约束 name 控件
  3. position 控件多了 app:layout_constraintHorizontal_bias=”0.0” 属性,而且左侧约束 placeholder_view,右侧约束父布局

这个方案的基本原理在于 “推动”,name 控件推动 placeholder_view,placeholder_view 控件推动 position,推到边界时,position 控件右侧约束父布局,再也推不动了,理论上来说 position 会被限制在边界,但事实上还要再给 name 加上 app:layout_constrainedWidth=”true” 属性,对 name 控件进行强制约束,这时才能达到效果

同时还要注意,这里的 name、placeholder_view 和 position 不能形成链,形成链的话要方案二的一些处理才能达到最终效果,讲方案二时会再展开

第二种方案

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
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_container_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/cl_container_1">

<ImageView
android:id="@+id/iv_avatar_2"
style="@style/ConstraintLayoutTips.Avatar"
app:layout_constraintStart_toStartOf="parent" />

<TextView
android:id="@+id/tv_name_2"
style="@style/ConstraintLayoutTips.Name"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toStartOf="@id/placeholder_view_2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/iv_avatar_2"
app:layout_constraintWidth_default="wrap" />

<!-- 宽高 1dp 的占位 View,仅用于实现约束效果,并没有其他作用 -->

<View
android:id="@+id/placeholder_view_2"
style="@style/ConstraintLayoutTips.Placeholder"
android:layout_marginStart="10dp"
app:layout_constraintEnd_toStartOf="@id/iv_position_2"
app:layout_constraintStart_toEndOf="@+id/tv_name_2" />

<ImageView
android:id="@+id/iv_position_2"
style="@style/ConstraintLayoutTips.Position"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/placeholder_view_2" />

</androidx.constraintlayout.widget.ConstraintLayout>

与原始布局的不同点在于:

  1. name 控件 android:layout_width=”0dp”,多了 app:layout_constraintWidth_default=”wrap” 属性,设置了 app:layout_constraintHorizontal_bias=”0.0”、app:layout_constraintHorizontal_chainStyle=”packed”,而且右侧约束 placeholder_view
  2. 多了一个 placeholder_view 占位控件,placeholder_view 左侧约束 name 控件,右侧约束 position 控件
  3. position 控件左侧约束 placeholder_view,右侧约束父布局

与方案一的不同点在于:

  1. 三个控件构成了一条链
  2. 主要处理的控件是 name 控件

这个方案的基本原理在于 “链”,三个控件构成了一条链,且把它们 packed 打包到一起,用 app:layout_constraintHorizontal_bias=”0.0” 控制居左显示,最后用 android:layout_width=”0dp” 配合 app:layout_constraintWidth_default=”wrap”,处理了 name 控件字数不多时没有根据字数动态展示宽度的问题(不设置这个参数,它会把控件占满,因为 width=”0dp”)

需要注意

  1. layout_constraintHorizontal_bias 用于控制布局相对水平约束空间的位置百分比,在约束主体只有一个时才有效(不知道这样说是不是合理,大概是这个意思吧),也就是说分两块时,要把 layout_constraintHorizontal_chainStyle 设置为 packed,将其绑成一块才行,否则不会生效(vertical 同理)
  2. layout_constraintWidth_default 需要在 layout_width 为 0dp 时才能生效,因为它就是用来控制 layout_width 为 0dp 时的布局状态的(height 同理)

参考文章

看了这位哥们的文章学到的,加了些自己的理解,整理归纳


ConstraintLayout 的一个小技巧
https://enderhoshi.github.io/2024/09/05/ConstraintLayout 的一个小技巧/
作者
HoshIlIlI
发布于
2024年9月5日
许可协议