• 作者:老汪软件技巧
  • 发表时间:2024-12-21 07:05
  • 浏览量:

一、传统 View 中的九宫格控件

在 Jetpack Compose 还没问世的年代,我们用传统 View 来写九宫格控件。虽然有点繁琐,但也不算太难。大致步骤如下:

确定行列规则:九宫格一般是最多三列,图片数量决定行数。测量与布局:根据父布局的宽度,计算每张图片的尺寸和位置。绘制:将图片按规则摆放到对应位置。

从 NineGridView 里找到对应代码大概展示:

// 代码都是从 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)

布局是个两步走的过程:

三、绘制(Drawing)

最后一步是把布局好的 UI 渲染到屏幕上。

可以这么理解:组合阶段是「画草图」,布局阶段是「摆家具」,绘制阶段是「刷油漆」。

参考 Compose官网对应的文章。这不是本文的重点,所以简单的讲下。

三、Modifier.layout {} 和 Layout() 函数。一、Modifier.layout {} 的作用。

Modifier.layout {} 是一个 LayoutModifier ;对于它的解释看源码:

它的作用:允许调用处的组件修改自己的尺寸和放置的位置。

首先看一个什么都不做的写法:

@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 {}实现。

具体做法:

@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))
    }
}

它的效果:

Compose 仿微信朋友圈九宫格控件以及背后的原理__Compose 仿微信朋友圈九宫格控件以及背后的原理

有人就会说:那我用Modifier.padding()就好了,何必使用复杂得多的Modifier.layout {}?

事实也是如此,但我描述的是Modifier.layout {}具体作用做的一个案例而已,并不比直接使用Modifier.padding()有多出其他的作用和好处。

细节讲解:

它返回一个 MeasureResult,其中包含:

带入到案例代码中:

@Preview @Composable private fun TestModifierLayout() {
   Box(

Compose 仿微信朋友圈九宫格控件以及背后的原理__Compose 仿微信朋友圈九宫格控件以及背后的原理

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 {}的参数不能说一模一样,只能说完全一致。

那它们两个在使用上有什么不同处呢?或者说使用场景有什么不同?

如何选择?

四、Compose 实战九宫格控件。

前面铺垫了 Modifier.layout {} 和Layout() ,是因为仿微信朋友圈九宫格控件的实现原理就是它们两个,要讲清楚它们的机制才可以实现九宫格控件。

对应代码:

// 使用
@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。

作者其他文章:


上一条查看详情 +关于写周报的思考
下一条 查看详情 +没有了