1 Preface
在这篇文章中,我们将实现一个自定义控件,类似水平方向的 LinearLayout,区别是:当水平方向上空间不足时,子 View 自动从下一行开始放置。
这种控件有个统称:流式布局(FlowLayout)。
2 Situation
先来看一个微信朋友圈详情页的照片墙效果:
我们通过 View Hierarchy 来看下这些头像的布局:
可以看到,每一行头像都是一个水平方向的 LinearLayout。外面也是一个 LinearLayout。
当然,这是最容易想到的实现方式。
好处是思路简单,代码容易维护,即使是新手也能维护。
坏处是出现层级嵌套,影响性能。
3 Target
从减少层级的角度,对这个布局做下优化。
这种布局的本质是:有一个 ViewGroup,对其添加子 View 的时候,从左至右水平摆放,当第 n 行的水平空间不足时从第 n+1 行最左侧开始摆放,即一个会自动换行的 ViewGroup。
这种方式的好处很明显:布局层级明显减少。
由于子 View 从上一行末尾换到下一行首时的轨迹像 Z,,我们姑且称之为 ZLayout。
4 Action
继承 ViewGroup,重写 onMeasure()、onLayout()方法。
5 Result
5.1 ZLayout
该控件已经开源到 Github 上,具体代码见链接。
5.2 使用场景
ZLayout 除了可以实现 Situation 中的照片墙,还有下面一种用法。
截取大众点评 app 个人空间的一个效果:
注意昵称后面 lv5 和 vip 图标
如果用户昵称后面除了 lv5 和 vip 之外,还有其他头衔,很可能因为空间不够而导致一行放不下,我们假设空间不足是不显示剩余的头衔图标。这种情况该怎么办呢?
在昵称后面放一个 LinearLayout 行吗?我们试验下。
用 200px x 100px 的飞机图来代表头衔,分别使用不同的 LayoutParams 参数,看能否达到上述目标。
ico_aereo.png
实验代码:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout container = new LinearLayout(getApplicationContext());
container.setOrientation(LinearLayout.VERTICAL);
container.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
// 第1行
LinearLayout line1 = new LinearLayout(getApplicationContext());
line1.setOrientation(LinearLayout.HORIZONTAL);
LinearLayout.LayoutParams lpWrapContent = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
// 第2行
LinearLayout line2 = new LinearLayout(getApplicationContext());
line2.setOrientation(LinearLayout.HORIZONTAL);
LinearLayout.LayoutParams lpExactDimension = new LinearLayout.LayoutParams(200, 100);
for (int i = 0; i < 200; i++) {
ImageView img1 = new ImageView(getApplicationContext());
img1.setImageDrawable(getResources().getDrawable(R.mipmap.ico_aereo));
line1.addView(img1, lpWrapContent);
ImageView img2 = new ImageView(getApplicationContext());
img2.setImageDrawable(getResources().getDrawable(R.mipmap.ico_aereo));
line2.addView(img2, lpExactDimension);
}
container.addView(line1, lpWrapContent);
container.addView(line2, lpWrapContent);
setContentView(container);
}
}
运行结果如下:
从图可以看出,将 ImageView 的 LayoutParams 设置为 WRAP_CONTENT 和精确值,效果是不同的。具体来说,当剩余水平空间不足以放下一个原始尺寸的 ImageView 时,前者等比例缩小显示,后者则截断显示。
注意,上述结果仅仅是对于 ImageView 而言的,如果是其他类型,如 TextView,则不一定成立。换做 TextView 的实验结果如下:
第1行的 TextView 的 LayoutParams 全部是 WRAP_CONTENT;第2行的 TextView 的 width=127px,height=WRAP_CONTENT。
试验代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout container = new LinearLayout(getApplicationContext());
container.setOrientation(LinearLayout.VERTICAL);
container.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
// 第1行
LinearLayout line1 = new LinearLayout(getApplicationContext());
line1.setOrientation(LinearLayout.HORIZONTAL);
LinearLayout.LayoutParams lpWrapContent = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
// 第2行
LinearLayout line2 = new LinearLayout(getApplicationContext());
line2.setOrientation(LinearLayout.HORIZONTAL);
LinearLayout.LayoutParams lpExactDimension = new LinearLayout.LayoutParams(127, ViewGroup.LayoutParams.WRAP_CONTENT);
for (int i = 0; i < 200; i++) {
TextView view1 = new TextView(getApplicationContext());
view1.setText("love u...");
view1.setTextColor(getResources().getColor(R.color.colorAccent));
line1.addView(view1, lpWrapContent);
TextView view2 = new TextView(getApplicationContext());
view2.setText("love u...");
view2.setTextColor(getResources().getColor(R.color.colorAccent));
line2.addView(view2, lpExactDimension);
}
container.addView(line1, lpWrapContent);
container.addView(line2, lpWrapContent);
setContentView(container);
}
显然,LinearLayout 对 ImageView 的 layout 策略是无法满足我们的需求的。
ZLayout 可以做到。只需将设置其一个属性即可:
z:maxLines = "1"
或
z.setMaxLines(1);
即,ZLayout 最多只有1行子控件,其余的不做处理。
6 References
- ZLayout
- 安卓自定义控件原理及实践