You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity UI交错砖格(Running bond)布局:动态计算行列求助

Unity 动态交错砖格(Running Bond)布局实现方案

核心思路

放弃嵌套Horizontal/Vertical Layout Group的静态分组方案,改用自定义布局逻辑直接计算每个砖块的位置。这种方式可以灵活适配父容器尺寸变化,支持无限数量的砖块(结合对象池),完美解决多行列的交错对齐问题。

实现步骤

1. 基础配置定义

先明确布局的关键参数,方便后续调整:

  • 砖块尺寸:固定2:1比例,定义brickWidth,高度自动计算为brickWidth / 2
  • 间距:分别定义horizontalSpacing(砖块水平间距)和verticalSpacing(行之间的额外垂直间距)
  • 容器尺寸:获取ScrollRect Viewport的有效宽度,作为布局的最大可用宽度

2. 动态计算行列数

每行砖块数计算

  • 奇数行(第1、3、5...行):直接用可用宽度计算最大容纳数,公式:
    oddRowCount = Mathf.FloorToInt((availableWidth + horizontalSpacing) / (brickWidth + horizontalSpacing))
  • 偶数行(第2、4、6...行):需要偏移半个砖宽+半个间距,所以可用宽度先减去(brickWidth + horizontalSpacing)/2,再计算:
    evenRowCount = Mathf.FloorToInt((availableWidth + horizontalSpacing - (brickWidth + horizontalSpacing)/2) / (brickWidth + horizontalSpacing))

总行数计算

交替使用奇偶行的砖块数,从总砖块数中逐行扣除,直到剩余砖块数为0,记录每行的实际砖块数量。

3. 砖块位置计算

  • 垂直方向:从Content顶部开始布局,奇数行的Y坐标为currentY,偶数行需要偏移半个砖高(brickHeight/2),行之间的间隔为brickHeight/2 + verticalSpacing
  • 水平方向:奇数行从左 padding 开始,每个砖块的X坐标为startX + i*(brickWidth + horizontalSpacing);偶数行的起始X坐标偏移(brickWidth + horizontalSpacing)/2

4. 适配容器尺寸变化

重写OnRectTransformDimensionsChange()方法,在父容器(Viewport)尺寸变化时自动触发布局刷新。

5. 无限数量支持(对象池优化)

如果需要支持大量砖块,不要一次性实例化所有对象:

  • 维护一个砖块对象池,回收不可见的砖块
  • 监听ScrollRect的滚动事件,计算当前可见的行范围,只实例化/复用可见区域内的砖块

6. 悬停缩放适配

  • 将砖块的RectTransform锚点设为(0,0),Pivot设为(0.5,0.5),确保缩放时以砖块中心为原点,不会偏移位置
  • 阴影效果使用Unity自带的Shadow组件,调整阴影的Effect Distance和Color,避免影响布局层级

代码示例

using UnityEngine;
using System.Collections.Generic;

public class RunningBondLayout : MonoBehaviour
{
    [Header("布局配置")]
    public RectTransform brickPrefab;
    public float brickWidth = 200f;
    public float horizontalSpacing = 20f;
    public float verticalSpacing = 10f;
    public RectTransform viewport;

    [Header("数据配置")]
    public int totalBrickCount = 30;

    private List<RectTransform> activeBricks = new List<RectTransform>();

    private void Start()
    {
        RefreshLayout();
    }

    // 容器尺寸变化时自动刷新
    private void OnRectTransformDimensionsChange()
    {
        RefreshLayout();
    }

    // 手动触发布局刷新
    public void RefreshLayout()
    {
        // 回收现有砖块(对象池模式下改为放回池,此处简化为销毁)
        foreach (var brick in activeBricks)
        {
            Destroy(brick.gameObject);
        }
        activeBricks.Clear();

        float availableWidth = viewport.rect.width;
        float brickHeight = brickWidth / 2f;

        // 计算奇偶行的最大砖块数
        int oddRowMax = Mathf.FloorToInt((availableWidth + horizontalSpacing) / (brickWidth + horizontalSpacing));
        int evenRowMax = Mathf.FloorToInt((availableWidth + horizontalSpacing - (brickWidth + horizontalSpacing)/2f) / (brickWidth + horizontalSpacing));

        // 生成每行的实际砖块数列表
        List<int> rowCounts = new List<int>();
        int remaining = totalBrickCount;
        bool isOddRow = true;
        while (remaining > 0)
        {
            int take = isOddRow ? oddRowMax : evenRowMax;
            take = Mathf.Min(take, remaining);
            rowCounts.Add(take);
            remaining -= take;
            isOddRow = !isOddRow;
        }

        // 设置Content高度
        float contentHeight = verticalSpacing * (rowCounts.Count + 1) 
                            + brickHeight * rowCounts.Count 
                            + (rowCounts.Count - 1) * (brickHeight / 2f);
        GetComponent<RectTransform>().sizeDelta = new Vector2(0, contentHeight);

        // 逐个放置砖块
        float currentY = contentHeight - verticalSpacing - brickHeight;
        for (int rowIdx = 0; rowIdx < rowCounts.Count; rowIdx++)
        {
            bool currentIsOdd = rowIdx % 2 == 0;
            int brickCount = rowCounts[rowIdx];
            float startX = verticalSpacing;

            if (!currentIsOdd)
            {
                startX += (brickWidth + horizontalSpacing) / 2f;
            }

            for (int brickIdx = 0; brickIdx < brickCount; brickIdx++)
            {
                RectTransform brick = Instantiate(brickPrefab, transform);
                brick.sizeDelta = new Vector2(brickWidth, brickHeight);
                brick.anchorMin = Vector2.zero;
                brick.anchorMax = Vector2.zero;
                brick.pivot = new Vector2(0.5f, 0.5f);

                float xPos = startX + brickIdx * (brickWidth + horizontalSpacing);
                float yPos = currentY - (currentIsOdd ? 0 : brickHeight / 2f);
                brick.anchoredPosition = new Vector2(xPos, yPos);

                activeBricks.Add(brick);
            }

            // 更新下一行Y坐标
            currentY -= brickHeight / 2f + verticalSpacing;
        }
    }

    // 动态修改总砖块数
    public void UpdateTotalBricks(int count)
    {
        totalBrickCount = count;
        RefreshLayout();
    }
}

使用说明

  1. 将脚本挂载到ScrollRect的Content对象上
  2. 拖入砖块预制体、Viewport对象,设置布局参数
  3. 运行后自动生成交错布局,调整Viewport尺寸会自动适配
  4. 如需支持无限滚动,将销毁砖块的逻辑替换为对象池回收,添加滚动监听实现动态加载

内容的提问来源于stack exchange,提问作者KillerFrog11

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 08:09:58