一、传统 View 中的九宫格控件
在 Jetpack Compose 还没问世的年代,我们用传统 View 来写九宫格控件。虽然有点繁琐,但也不算太难。大致步骤如下:
- 确定行列规则:九宫格一般是最多三列,图片数量决定行数。
- 测量与布局:根据父布局的宽度,计算每张图片的尺寸和位置。
- 绘制:将图片按规则摆放到对应位置。
从 NineGridView 里找到对应代码大概展示:
ini
// 代码都是从 NineGridView 拷贝而来的,点击👆前往出处。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = 0;
int totalWidth = width - getPaddingLeft() - getPaddingRight();
if (mImageInfo != null && mImageInfo.size() > 0) {
if (mImageInfo.size() == 1) {
gridWidth = singleImageSize > totalWidth ? totalWidth : singleImageSize;
gridHeight = (int) (gridWidth / singleImageRatio);
//矫正图片显示区域大小,不允许超过最大显示范围
if (gridHeight > singleImageSize) {
float ratio = singleImageSize * 1.0f / gridHeight;
gridWidth = (int) (gridWidth * ratio);
gridHeight = singleImageSize;
}
} else {
// gridWidth = gridHeight = (totalWidth - gridSpacing * (columnCount - 1)) / columnCount;
//这里无论是几张图片,宽高都按总宽度的 1/3
gridWidth = gridHeight = (totalWidth - gridSpacing * 2) / 3;
}
width = gridWidth * columnCount + gridSpacing * (columnCount - 1) + getPaddingLeft() + getPaddingRight();
height = gridHeight * rowCount + gridSpacing * (rowCount - 1) + getPaddingTop() + getPaddingBottom();
}
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mImageInfo == null) return;
int childrenCount = mImageInfo.size();
for (int i = 0; i < childrenCount; i++) {
ImageView childrenView = (ImageView) getChildAt(i);
int rowNum = i / columnCount;
int columnNum = i % columnCount;
int left = (gridWidth + gridSpacing) * columnNum + getPaddingLeft();
int top = (gridHeight + gridSpacing) * rowNum + getPaddingTop();
int right = left + gridWidth;
int bottom = top + gridHeight;
childrenView.layout(left, top, right, bottom);
if (mImageLoader != null) {
mImageLoader.onDisplayImage(getContext(), childrenView, mImageInfo.get(i).thumbnailUrl);
}
}
}
额....反正我看到这些代码就莫名其妙的觉得太「南」了!!!但...如果你使用 Compose 这一切都变得简单!
二、Compose 代码需要经过几个阶段?
View 系统会经过三个阶段呈现 UI 到手机上,分别是:onMeasure()、onLayout()、onDraw()
。
而 Compose 是:Compoasition、Layout、Drawing
一、组合(Composition)
组合是指把带有 @Composable
注解的代码,形成一个树的结构。
二、布局(Layout)
布局是个两步走的过程:
- 测量(Measure) :确定每个子组件的尺寸。
- 放置(Place) :把子组件放到指定的位置上。
三、绘制(Drawing)
最后一步是把布局好的 UI 渲染到屏幕上。
可以这么理解:组合阶段是「画草图」,布局阶段是「摆家具」,绘制阶段是「刷油漆」。
参考 Compose官网对应的文章。这不是本文的重点,所以简单的讲下。
三、Modifier.layout {} 和 Layout() 函数。
一、Modifier.layout {} 的作用。
Modifier.layout {}
是一个 LayoutModifier ;对于它的解释看源码:
它的作用:允许调用处的组件修改自己的尺寸和放置的位置。
首先看一个什么都不做的写法:
less
@Preview @Composable private fun TestModifierLayout() {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Blue)
) {
Text(text = "TestModifierLayout",
color = Color.White,
modifier = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}.background(Color.Red))
}
}
它的效果:
如果想让Text()
增加一个对于Top
的间距,除了Modifier.padding()
之外就是可以使用Modifier.layout {}
实现。
具体做法:
less
@Preview @Composable private fun TestModifierLayout() {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Gray)
) {
Text(text = "TestModifierLayout",
color = Color.White,
modifier = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
// 不同处 👇
placeable.placeRelative(0, 100)
}
}.background(Color.Red))
}
}
它的效果:
有人就会说:那我用Modifier.padding()
就好了,何必使用复杂得多的Modifier.layout {}
?
事实也是如此,但我描述的是Modifier.layout {}
具体作用做的一个案例而已,并不比直接使用Modifier.padding()
有多出其他的作用和好处。
细节讲解:
-
Modifier.layout {}
函数:-
这是一个高阶函数,用于定义测量和放置逻辑。
-
它有两个参数:
measurable: Measurable
:表示需要测量的子组件。constraints: Constraints
:表示父布局传递下来的约束条件(比如最小宽度、最大宽度等)。
-
它返回一个
MeasureResult
,其中包含:- 组件的宽高尺寸。
- 子组件的放置逻辑。
-
-
measurable.measure(constraints)
:- 测量子组件,返回一个
Placeable
对象。 Placeable
包含子组件的宽高信息,并提供放置方法。
- 测量子组件,返回一个
-
layout(width, height)
:- 定义组件的最终宽高,并且返回一个
MeasureResult
。
- 定义组件的最终宽高,并且返回一个
-
placeable.place(x, y)/placeRelative(x, y)
:- 定义组件在父组件中的位置,也就是放置。
placeRelative(x, y)
是针对从右到左布局(阿拉伯语)的特殊函数。
带入到案例代码中:
less
@Preview @Composable private fun TestModifierLayout() {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Gray)
) {
Text(text = "TestModifierLayout",
color = Color.White,
modifier = Modifier
.background(Color.Red)
.layout { measurable, constraints ->
// 这个案例中:measurable.measure(constraints) 就是 Text 的测量过程
val placeable = measurable.measure(constraints)
// 这里的 placeable 就是 Text 的测量结果
// 这里的 placeable.width 和 placeable.height 就是 Text 的宽高
// 这里的 placeable.placeRelative(0, 0) 就是 Text 的布局过程
// layout 的返回值就是这个自定义布局的宽高
// 👇这里加了 100
layout(placeable.width, placeable.height + 100) {
// 对从右到左的布局
// 👇这里加了 100
placeable.placeRelative(0, 100)
// 对从左到右的布局
//placeable.place(0, 0)
}
}
)
}
}
它的效果:
二、Layout() 函数的作用。
它的源代码:
其他的代码先不管,都不是本文的重点。注意 MeasurePolicy,点进去查看代码:
没有看错,Layout()
里面的参数和Modifier.layout {}
的参数不能说一模一样,只能说完全一致。
那它们两个在使用上有什么不同处呢?或者说使用场景有什么不同?
Modifier.layout {}
是一种 修饰符(Modifier) ,用于修改现有组件的布局行为。它不能创建新的布局,而是基于现有组件,调整它的测量和放置逻辑。Layout()
是一种 Composable 函数,用于创建一个全新的布局。它可以包含多个子组件,并完全控制这些子组件的测量、布局和放置。
如何选择?
- 如果你需要 调整单个组件的布局行为 ,比如改变宽高、位置、对齐方式等,使用
Modifier.layout {}
。 - 如果你需要 创建一个新的布局容器 ,并控制多个子组件的排列方式,使用
Layout()
。
四、Compose 实战九宫格控件。
前面铺垫了 Modifier.layout {}
和Layout()
,是因为仿微信朋友圈九宫格控件的实现原理就是它们两个,要讲清楚它们的机制才可以实现九宫格控件。
对应代码:
less
// 使用
@Preview
@Composable private fun NineGridLayoutPreview() {
NineGridLayout(modifier = Modifier.background(Color.Gray), itemSize = 100.dp, padding = 10.dp) {
repeat(9) {
Image(
painter = painterResource(id = R.mipmap.ic_test_image), contentDescription = null,
)
}
}
}
/**
* 九宫格布局
*
* @param singleItemSize 单个元素的大小
* @param itemSize 其他元素的大小
* @param padding 元素之间的间距
* @param content 元素内容
*/
@Composable
fun NineGridLayout(
modifier: Modifier = Modifier,
singleItemSize: Dp = 200.dp,
itemSize: Dp = 100.dp,
padding: Dp = 8.dp,
content: @Composable @UiComposable () -> Unit
) {
// 使用 Layout 进行自定义布局
Layout(content = content, modifier = modifier) { measurables, constraints ->
// 在 Layout 内部获取 Density 并计算 px 值
val paddingPx = padding.roundToPx()
val itemSizePx = itemSize.roundToPx()
val singleSizePx = singleItemSize.roundToPx()
// 子组件的数量
when (measurables.size) {
1 -> {
// 只有一个元素时,使用固定大小进行布局
// 测量出结果并且放置
val placeable = measurables.first().measure(constraints)
layout(singleSizePx, singleSizePx) {
placeable.placeRelative(0, 0)
}
}
else -> {
// 不止一个组件时,要计算数量来决定高度和宽度,这部分是通用代码就不过多解释了。
// 计算行数和总宽高,避免重复计算
val rowCount = (measurables.size + 2) / 3
val totalWidth = (itemSizePx + paddingPx) * 3 - paddingPx
val totalHeight = (itemSizePx + paddingPx) * rowCount - paddingPx
// 测量每个元素
val placeables = measurables.map { it.measure(Constraints.fixed(itemSizePx, itemSizePx)) }
// 布局逻辑:按 3 列排列
layout(totalWidth, totalHeight) {
// 遍历所有元素,计算位置并放置
placeables.forEachIndexed { index, placeable ->
val xPosition = (index % 3) * (itemSizePx + paddingPx)
val yPosition = (index / 3) * (itemSizePx + paddingPx)
placeable.placeRelative(xPosition, yPosition)
}
}
}
}
}
}
最终效果图:
其实细心的朋友们就会发现、使用 Compose 开发遇到微信朋友圈九宫格需求的时候根本是不需要NineGridLayout 也能实现需求,事实还是这么回事。
但我想的是借助**「微信朋友圈九宫格」的噱头来讲解Layout()
和Modifier.layout {}
的机制,学习效果会不会翻倍呢?「🐶」
End。
作者其他文章: