PAYATHON 2026

Flutter GridView 如何实现蜂窝状六边形布局?

数据小陈

结论

HexagonClipper 只裁剪每个项目的绘制区域,不会改变项目在 GridView 中的位置。SliverGridDelegateWithFixedCrossAxisCount 会将项目排列在规则的网格槽位中,所以六边形默认仍会整齐对齐。

要让相邻行左右错开,可以根据项目所在的行,为奇数行增加水平偏移。下面的写法保留了现有的 GridView.builder 和六边形裁剪逻辑:

LayoutBuilder(
  builder: (context, constraints) {
    const columns = 3;
    const spacing = 2.0;
    const padding = 8.0;

    final tileWidth =
        (constraints.maxWidth - padding * 2 - spacing * (columns - 1)) /
            columns;

    return GridView.builder(
      primary: true,
      scrollDirection: Axis.vertical,
      clipBehavior: Clip.none,
      padding: const EdgeInsets.all(padding),
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: columns,
        mainAxisSpacing: spacing,
        crossAxisSpacing: spacing,
        childAspectRatio: 0.866,
      ),
      itemCount: _exploreScreenController.posts.length,
      itemBuilder: (context, index) {
        final post = _exploreScreenController.posts[index];
        if (post.gallery.isEmpty) {
          return const SizedBox.shrink();
        }

        final galleryItem = post.gallery.first;
        final isVideo = galleryItem.mediaType == 2;
        final row = index ~/ columns;

        return Transform.translate(
          offset: Offset(row.isOdd ? tileWidth / 2 : 0, 0),
          child: GestureDetector(
            onTap: () {
              Get.to(() => ReelScrollView(
                    posts: _exploreScreenController.posts,
                    initialIndex: index,
                  ));
            },
            child: ClipPath(
              clipper: HexagonClipper(),
              child: Stack(
                fit: StackFit.expand,
                children: [
                  CachedNetworkImage(
                    imageUrl: galleryItem.thumbnail,
                    fit: BoxFit.cover,
                    placeholder: (context, url) => const Center(
                      child: CircularProgressIndicator(),
                    ),
                    errorWidget: (context, url, error) =>
                        const Icon(Icons.error),
                  ),
                  Container(color: Colors.black.withOpacity(0.1)),
                  if (isVideo)
                    const Positioned(
                      top: 6,
                      right: 6,
                      child: ThemeIconWidget(
                        ThemeIcon.goLive,
                        color: Colors.white,
                        size: 16,
                      ),
                    ),
                ],
              ),
            ),
          ),
        );
      },
    );
  },
)

row = index ~/ columns 用来计算项目所在的行。奇数行通过 Transform.translate 向右移动半个项目宽度。childAspectRatio 控制网格槽位的宽高比,可以按实际设计调整。clipBehavior: Clip.none 允许偏移后的项目绘制到网格槽位边界之外。如果边缘项目仍被裁掉,可以检查外层容器的裁剪设置,或为布局预留水平空间。

如果需要更紧密的蜂窝效果

这种做法只会错开各行,行与行之间仍保留 mainAxisSpacing,六边形不会自动嵌合。如果设计要求相邻行垂直重叠、边缘紧密贴合,可以用 Stack 配合自定义 CustomScrollView/Sliver 布局,根据每个六边形的宽高计算 left 和 top,也可以实现自定义 SliverGridDelegate。具体间距要根据六边形尺寸和目标效果确定。

现有代码的注意事项

示例中第一个 return GestureDetector(...) 后还有第二个 GestureDetector(...)。第一个 return 已结束当前 itemBuilder 调用,后面的代码不会执行。删除第二段,避免误以为两种布局都会参与渲染。

此外,当前 HexagonClipper.shouldReclip 固定返回 false,适用于裁剪形状不会随状态变化的情况。如果裁剪路径会动态改变,需要根据实际变化判断是否重绘。

备注:内容仅供参考。