加入ui反射界面生成

This commit is contained in:
2026-04-18 18:03:45 +08:00
parent 21a09edad2
commit eb7bdfccc3
19 changed files with 492 additions and 116 deletions
+172 -106
View File
@@ -6,6 +6,7 @@ namespace XericUI.Helper
{
public static class GridLayoutCalculator
{
public static Vector2 Grid;
public struct LayoutItem
{
public Vector2 position; // anchoredPosition
@@ -18,18 +19,8 @@ namespace XericUI.Helper
/// <summary>
/// 计算所有项目的布局
/// </summary>
/// <param name="childCount">子项数量</param>
/// <param name="containerSize">所需尺寸</param>
/// <param name="cellSize">单元尺寸</param>
/// <param name="spacing">单元空隙</param>
/// <param name="padding">外围空隙</param>
/// <param name="startCorner">起始角</param>
/// <param name="startAxis">起始轴</param>
/// <param name="constraint">容器尺寸</param>
/// <param name="constraintCount">需要的列数</param>
/// <param name="childAlignment">子项对齐</param>
/// <returns></returns>
public static List<LayoutItem> CalculateGridLayout(
List<LayoutItem> layoutItems,
int childCount,
Vector2 containerSize,
Vector2 cellSize,
@@ -41,7 +32,15 @@ namespace XericUI.Helper
int constraintCount,
TextAnchor childAlignment = TextAnchor.UpperLeft)
{
List<LayoutItem> layoutItems = new List<LayoutItem>(childCount);
if (layoutItems == null)
layoutItems = new List<LayoutItem>();
else
layoutItems.Clear();
// 边界保护
constraintCount = Mathf.Max(1, constraintCount);
cellSize = new Vector2(Mathf.Max(0.1f, cellSize.x), Mathf.Max(0.1f, cellSize.y));
spacing = new Vector2(Mathf.Max(0f, spacing.x), Mathf.Max(0f, spacing.y));
// 1. 计算行列数
int cellCountX, cellCountY;
@@ -53,45 +52,44 @@ namespace XericUI.Helper
CalculateActualGridDimensions(childCount, cellCountX, cellCountY, constraint, constraintCount,
startAxis, out actualCellCountX, out actualCellCountY);
// 3. 计算所需空间和起始偏移
Grid = new Vector2(actualCellCountX, actualCellCountY);
// 3. 计算所需空间
Vector2 requiredSpace = new Vector2(
actualCellCountX * cellSize.x + (actualCellCountX - 1) * spacing.x,
actualCellCountY * cellSize.y + (actualCellCountY - 1) * spacing.y
actualCellCountX * cellSize.x + Mathf.Max(0, actualCellCountX - 1) * spacing.x,
actualCellCountY * cellSize.y + Mathf.Max(0, actualCellCountY - 1) * spacing.y
);
Vector2 startOffset = new Vector2(
GetStartOffset(0, requiredSpace.x, containerSize.x, padding, childAlignment),
GetStartOffset(1, requiredSpace.y, containerSize.y, padding, childAlignment)
);
// 4. 计算起始偏移(关键修复:根据startCorner调整基准点)
Vector2 startOffset = CalculateStartOffset(
requiredSpace, containerSize, padding, childAlignment, startCorner);
// 4. 处理特殊情况(约束条件下的子项重新排列)
int childrenToMove = 0;
if (childCount > constraintCount && constraint != GridLayoutGroup.Constraint.Flexible)
{
int cellsPerMainAxis = startAxis == GridLayoutGroup.Axis.Horizontal ? cellCountX : cellCountY;
if (Mathf.CeilToInt((float)childCount / (float)cellsPerMainAxis) < constraintCount)
{
childrenToMove = constraintCount - Mathf.CeilToInt((float)childCount / (float)cellsPerMainAxis);
childrenToMove += Mathf.FloorToInt((float)childrenToMove / ((float)cellsPerMainAxis - 1));
if (childCount % cellsPerMainAxis == 1)
childrenToMove += 1;
}
}
// 5. 处理特殊情况
int childrenToMove = CalculateChildrenToMove(childCount, cellCountX, cellCountY,
constraint, constraintCount, startAxis);
// 5. 计算每个子项的位置
int cornerX = (int)startCorner % 2;
int cornerY = (int)startCorner / 2;
// 6. 计算每个子项的位置
int cornerX = (int)startCorner % 2; // 0=左, 1=右
int cornerY = (int)startCorner / 2; // 0=上, 1=下
for (int i = 0; i < childCount; i++)
{
int positionX, positionY;
CalculateItemPosition(i, childCount, cellCountX, cellCountY, startAxis, constraint,
constraintCount, childrenToMove, cornerX, cornerY,
actualCellCountX, actualCellCountY, out positionX, out positionY);
int gridX, gridY;
CalculateItemGridPosition(i, childCount, cellCountX, cellCountY, startAxis,
constraint, constraintCount, childrenToMove,
actualCellCountX, actualCellCountY, out gridX, out gridY);
// 应用corner翻转(关键:这决定了索引0的位置)
if (cornerX == 1)
gridX = actualCellCountX - 1 - gridX;
if (cornerY == 1)
gridY = actualCellCountY - 1 - gridY;
// 计算最终位置(固定方向:向右下)
Vector2 position = new Vector2(
startOffset.x + (cellSize.x + spacing.x) * positionX,
startOffset.y + (cellSize.y + spacing.y) * positionY
startOffset.x + (cellSize.x + spacing.x) * gridX,
startOffset.y + (cellSize.y + spacing.y) * gridY
);
layoutItems.Add(new LayoutItem
@@ -99,14 +97,142 @@ namespace XericUI.Helper
position = position,
size = cellSize,
indexInGrid = i,
rowIndex = positionY,
columnIndex = positionX
rowIndex = gridY,
columnIndex = gridX
});
}
return layoutItems;
}
// ===== 核心修复:根据startCorner计算起始偏移 =====
private static Vector2 CalculateStartOffset(
Vector2 requiredSpace,
Vector2 containerSize,
RectOffset padding,
TextAnchor childAlignment,
GridLayoutGroup.Corner startCorner)
{
// 计算每个轴的起始偏移
float startX = CalculateAxisStartOffset(
0, requiredSpace.x, containerSize.x, padding, childAlignment, startCorner);
float startY = CalculateAxisStartOffset(
1, requiredSpace.y, containerSize.y, padding, childAlignment, startCorner);
return new Vector2(startX, startY);
}
private static float CalculateAxisStartOffset(
int axis, // 0=X, 1=Y
float requiredSpace,
float availableSpace,
RectOffset padding,
TextAnchor childAlignment,
GridLayoutGroup.Corner startCorner)
{
// 计算包含padding的总需求空间
float requiredSpaceWithPadding = requiredSpace + (axis == 0 ? padding.horizontal : padding.vertical);
// 计算剩余空间
float surplusSpace = availableSpace - requiredSpaceWithPadding;
// 获取对齐值(0=左/上, 0.5=中, 1=右/下)
float alignmentValue = GetAlignmentOnAxis(axis, childAlignment);
// 关键:根据startCorner调整基准点
float baseOffset;
if (axis == 0) // X轴
{
// 如果startCorner在右侧,基准点应该是padding.right
int cornerX = (int)startCorner % 2;
baseOffset = (cornerX == 0) ? padding.left : (availableSpace - padding.right - requiredSpace);
}
else // Y轴
{
// 如果startCorner在底部,基准点应该是padding.bottom
int cornerY = (int)startCorner / 2;
baseOffset = (cornerY == 0) ? padding.top : (availableSpace - padding.bottom - requiredSpace);
}
// 应用对齐(在剩余空间内)
return baseOffset + surplusSpace * alignmentValue;
}
private static void CalculateItemGridPosition(
int index,
int childCount,
int cellCountX,
int cellCountY,
GridLayoutGroup.Axis startAxis,
GridLayoutGroup.Constraint constraint,
int constraintCount,
int childrenToMove,
int actualCellCountX,
int actualCellCountY,
out int gridX,
out int gridY)
{
// 计算基础位置(不考虑corner翻转)
if (startAxis == GridLayoutGroup.Axis.Horizontal)
{
if (constraint == GridLayoutGroup.Constraint.FixedRowCount &&
childCount - index <= childrenToMove)
{
gridX = 0;
gridY = constraintCount - (childCount - index);
}
else
{
gridX = index % cellCountX;
gridY = index / cellCountX;
}
}
else
{
if (constraint == GridLayoutGroup.Constraint.FixedColumnCount &&
childCount - index <= childrenToMove)
{
gridX = constraintCount - (childCount - index);
gridY = 0;
}
else
{
gridX = index / cellCountY;
gridY = index % cellCountY;
}
}
}
private static int CalculateChildrenToMove(
int childCount,
int cellCountX,
int cellCountY,
GridLayoutGroup.Constraint constraint,
int constraintCount,
GridLayoutGroup.Axis startAxis)
{
if (constraint == GridLayoutGroup.Constraint.Flexible || childCount <= constraintCount)
return 0;
int cellsPerMainAxis = startAxis == GridLayoutGroup.Axis.Horizontal ? cellCountX : cellCountY;
if (cellsPerMainAxis <= 0)
return 0;
float rowsNeeded = Mathf.CeilToInt((float)childCount / (float)cellsPerMainAxis);
if (rowsNeeded >= constraintCount)
return 0;
int childrenToMove = constraintCount - (int)rowsNeeded;
childrenToMove += Mathf.FloorToInt((float)childrenToMove / Mathf.Max(1f, (float)cellsPerMainAxis - 1));
if (childCount % cellsPerMainAxis == 1)
childrenToMove += 1;
return Mathf.Clamp(childrenToMove, 0, childCount);
}
private static void CalculateGridDimensions(
int childCount,
Vector2 containerSize,
@@ -136,7 +262,6 @@ namespace XericUI.Helper
}
else
{
// Flexible constraint - based on container size
float availableWidth = containerSize.x - padding.horizontal;
float availableHeight = containerSize.y - padding.vertical;
@@ -182,74 +307,15 @@ namespace XericUI.Helper
else
actualCellCountX = Mathf.Clamp(cellCountX, 1, Mathf.CeilToInt(childCount / (float)cellCountY));
}
}
private static void CalculateItemPosition(
int index,
int childCount,
int cellCountX,
int cellCountY,
GridLayoutGroup.Axis startAxis,
GridLayoutGroup.Constraint constraint,
int constraintCount,
int childrenToMove,
int cornerX,
int cornerY,
int actualCellCountX,
int actualCellCountY,
out int positionX,
out int positionY)
{
if (startAxis == GridLayoutGroup.Axis.Horizontal)
{
if (constraint == GridLayoutGroup.Constraint.FixedRowCount && childCount - index <= childrenToMove)
{
positionX = 0;
positionY = constraintCount - (childCount - index);
}
else
{
positionX = index % cellCountX;
positionY = index / cellCountX;
}
}
else
{
if (constraint == GridLayoutGroup.Constraint.FixedColumnCount && childCount - index <= childrenToMove)
{
positionX = constraintCount - (childCount - index);
positionY = 0;
}
else
{
positionX = index / cellCountY;
positionY = index % cellCountY;
}
}
// Apply corner flipping
if (cornerX == 1)
positionX = actualCellCountX - 1 - positionX;
if (cornerY == 1)
positionY = actualCellCountY - 1 - positionY;
}
private static float GetStartOffset(int axis, float requiredSpace, float availableSpace,
RectOffset padding, TextAnchor childAlignment)
{
float requiredSpaceWithPadding = requiredSpace + (axis == 0 ? padding.horizontal : padding.vertical);
float surplusSpace = availableSpace - requiredSpaceWithPadding;
float alignmentOnAxis = GetAlignmentOnAxis(axis, childAlignment);
return (axis == 0 ? padding.left : padding.top) + surplusSpace * alignmentOnAxis;
}
private static float GetAlignmentOnAxis(int axis, TextAnchor childAlignment)
{
if (axis == 0)
return ((int)childAlignment % 3) * 0.5f;
return ((int)childAlignment % 3) * (1f / 3f); // 0, 0.333, 0.666
else
return ((int)childAlignment / 3) * 0.5f;
return ((int)childAlignment / 3) * 0.5f; // 0, 0.5, 1
}
}
}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8f0aa8590dd561c489521639bd71d57c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,14 @@
# 界面反射生成器
这部分内容允许用户通过传入一个数据结构来生成界面,这个界面会通过生成对应类型的组件并绑定输入输出来与传入数据对象进行交互。
要使用界面反射生成器,你需要准备对应的组件,并在管理器中提供名称来注册组件。
然后在脚本中使用特性标记要使用的组件。
在程序中将数据结构对象传入到生成器上,生成器会将与之对应的成员生成在界面上。
## 关于组件结构
为了规范项目结构,建议将属性对应的ui交互部分(如单选项,输入框等)单独制作成预制体,ui属性的名称结构如果有变动的话,另外制作成一个单独的外部组件,将属性交互部分嵌入到这个外部组件中。
这两个或多个组件都可以池化,你可以使用池组件定位器(XuiavPooledLocator)放置在组件对应的关键节点处,程序会自动根据定位器属性将两个组件拼装在一起,且自动调整布局。
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 905b18fa6d394d0db2a80e932ca20aa9
timeCreated: 1776497351
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 33c0f3e646823ac488fff9d41fbcb8ed
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f363e91ca63eef6458d809926e94daa5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6e3f549d8ff4b0c4ea26ef2953228199
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

@@ -0,0 +1,172 @@
fileFormatVersion: 2
guid: 641d3554e6d3112478ebaf3c3ddcfce6
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMasterTextureLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 2
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 6, y: 6, z: 6, w: 6}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 8192
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 8192
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: iPhone
maxTextureSize: 8192
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 8192
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Windows Store Apps
maxTextureSize: 8192
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 8192
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: WebGL
maxTextureSize: 8192
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 25e5ca2e9376f5d47ae568f5d8d9e3de
internalID: -255972835
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
spritePackingTag:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1fed1a8d6f3176a41a7fd19ce328d2bc
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,7 @@
namespace XericUI.ReflectionUIGenerator
{
public class XuiavrgAttribute
{
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 10e270c86cab4817a0b8ce7877b2ab88
timeCreated: 1776505683
@@ -0,0 +1,30 @@
using Deconstruction.Type.Peculiarity;
using UnityEngine;
namespace XericUI.ReflectionUIGenerator
{
/// <summary>
/// 节点下生成反射属性管理器
/// </summary>
public class XuiavrgManager
{
/// <summary>
/// 装填数据负载
/// </summary>
/// <param name="payload"></param>
public void PerformSchemaDrivenHydration(object payload)
{
var type = payload.GetType();
var attributes = type.GetCustomAttributes(true);
foreach (var attribute in attributes)
{
if (attribute is not XuiavrgAttribute xuiavrgAttribute)
continue;
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8643f83a494345edaba7e524c63dfd24
timeCreated: 1776500141
@@ -0,0 +1,14 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace XericUI.ReflectionUIGenerator
{
/// <summary>
/// ui反射属性定位器,放在池对象的根节点上,设定好接下来的目标即可
/// </summary>
public class XuiavrgPooledLocator : MonoBehaviour
{
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ba7420972b684874dbab8713369d6f1b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,7 @@
namespace XericUI.ReflectionUIGenerator
{
public static class XuiavrgUtilities
{
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 967542fae9744ef89747df28c1c2196e
timeCreated: 1776501926
+13 -10
View File
@@ -79,8 +79,10 @@ namespace XericUI.OverrideLayout
private Vector2 _lastContextSize;
private Vector2 _lastContextAnchoredPosition;
private List<GridLayoutCalculator.LayoutItem> _layoutItems;
private Queue<RectTransform> _activeQueue = new Queue<RectTransform>();
#endregion
[Serializable]
@@ -151,7 +153,6 @@ namespace XericUI.OverrideLayout
MacroGizmosDraw.Label(vertex3[1], "preload Rect");
MacroGizmosDraw.Label(vertex1[1], "display Rect");
}
#endif
protected override void OnValidate()
{
@@ -165,11 +166,12 @@ namespace XericUI.OverrideLayout
var trans = content.transform.GetChild(i) as RectTransform;
UpdateChildPivot(layouts[i], trans);
trans.anchoredPosition = layouts[i].position;
trans.SetDeltaSize(m_cellSize);
}
RefreshContentSizeByChildrenLayouts(layouts);
}
#endif
protected override void Awake()
{
base.Awake();
@@ -332,16 +334,16 @@ namespace XericUI.OverrideLayout
var size = first.position - last.position;
size = size.Abs();
var spacing = new Vector2(first.columnIndex - last.columnIndex * m_spacing.x, first.rowIndex - last.rowIndex * m_spacing.y);
var spacing = new Vector2(GridLayoutCalculator.Grid.x * m_spacing.x, GridLayoutCalculator.Grid.y * m_spacing.y);
switch (m_startAxis)
{
case GridLayoutGroup.Axis.Horizontal:
var ySpacing = first.rowIndex - last.rowIndex * m_spacing.y;
var ySpacing = GridLayoutCalculator.Grid.y * m_spacing.y;
size.x = Mathf.Max(size.x, viewport.rect.width);
size.y = Mathf.Max(size.y + spacing.y, m_cellSize.y);
break;
case GridLayoutGroup.Axis.Vertical:
var xSpacing = last.columnIndex * m_spacing.x;
var xSpacing = GridLayoutCalculator.Grid.x * m_spacing.x;
size.x = Mathf.Max(size.x + spacing.x, m_cellSize.x);
size.y = Mathf.Max(size.y, viewport.rect.height);
break;
@@ -385,6 +387,7 @@ namespace XericUI.OverrideLayout
private List<GridLayoutCalculator.LayoutItem> GetChildrenLayout(int childrenCount)
=> GridLayoutCalculator.CalculateGridLayout(
_layoutItems,
childCount: childrenCount,
containerSize: ContextSize,
cellSize: m_cellSize,
@@ -399,12 +402,12 @@ namespace XericUI.OverrideLayout
public void RefreshChildrenLayout()
{
var layoutItems = GetChildrenLayout(_childrenDatas.Count);
RefreshContentSizeByChildrenLayouts(layoutItems);
GetChildrenLayout(_childrenDatas.Count);
RefreshContentSizeByChildrenLayouts(_layoutItems);
// 应用计算结果
for (int i = 0; i < layoutItems.Count; i++)
for (int i = 0; i < _layoutItems.Count; i++)
{
var item = layoutItems[i];
var item = _layoutItems[i];
_childrenDatas[i].UpdateLayout(item);
Debug.Log($"Item {i}: Position{item.position}, Row{item.rowIndex}, Column{item.columnIndex}");
}