6. 自适应布局(断点)
收益:一个 .sharq 界面即可同时交付桌面与手机,无需单独构建。把界面接到下面的断点轴,令牌就会 按窄面板重排布局——而不是手工为移动端重建界面。
屏幕尺寸适配只用一个轴:SusBreakpointService。
没有 High/Low 分辨率服务(SusResolutionService 已被移除),也没有随显示器尺寸自动缩放的 面板比例。密度(.density-compact / .density-comfortable)是手动的产品预设,而不是 屏幕尺寸适配。
示例用的 PanelSettings 使用 ConstantPixelSize,因此断点宽度会跟随面板 / Game 视图的宽度 (不使用 Unity 的 ScaleWithScreenSize 自动缩放)。
// Inside SusComponent (injected automatically):
BreakpointService.Current.Value // Prop<Breakpoint>
BreakpointService.IsMobile // Computed<bool> — width ≤ 1024
BreakpointService.IsTablet // Computed<bool> — Md | Lg
BreakpointService.IsDesktop // Computed<bool> — width ≥ 1920宽度是如何测量的
Class(.breakpoint-*)始终位于token 级联根上 (SusBootstrap.TokenCascadeRoot——与承载主题/密度的元素相同)。
宽度来源(与旧的分辨率服务所用路径相同):
- 首选:
cascadeRoot.resolvedStyle.width(必要时也用layout.width) - 回退: 面板
visualTree尺寸 - 最后手段: Editor 的 Game 视图尺寸 /
Screen.width(仅当根尚未完成布局时)
更新由以下几处驱动:
- 来自
LoadTokenCascade/Mount的SusBreakpointService.Attach(root) - 每个
SusComponent的GeometryChangedEvent→ 推送级联根宽度 (显式调用Attach(cascadeRoot).Update(width),与已删除的SusResolutionService.Update(cascadeRoot, width)模式相同) - 轻量轮询 + 面板
visualTree几何钩子(在 Editor 的 Game 视图中拖拽时, 仅靠 content root 常常会漏掉几何变化事件)
不要用子组件自身的宽度来选取根 class——那会错误地翻转整棵树的 token。
Editor 小贴士
如果 Game 视图被锁定为固定分辨率(例如 1920×1080),仅调整已停靠窗口的大小时面板宽度可能 不会变化。请使用 Free Aspect,并把宽度拖过下面的阈值,或者在 Storybook 中强制指定断点 (Breakpoint 下拉框 → sm / md / …)。
断点
| 名称 | 宽度 | 根 class |
|---|---|---|
Sm | ≤ 640px | .breakpoint-sm |
Md | ≤ 1024px | .breakpoint-md |
Lg | ≤ 1440px | .breakpoint-lg |
Xl | ≤ 1920px | .breakpoint-xl |
Xxl | > 1920px | .breakpoint-2xl |
Token(--sk-*)
构建在 core 上的 UI 包(kit、game)通过自己的 token 样式表在 .breakpoint-* 下覆盖间距 / 高度 / 字体。 组件已经在使用 var(--sk-button-height)、var(--sk-space-16)、var(--sk-font-body) 等—— 它们会在根 class 变化时自动响应。
并非所有 --sk-* token 都随断点移动。尺寸分成若干族群——间距、控制高度、字体大小和其他一些随断点调整; border width、radius(pill 除外,它跟随自己的高度)、letter spacing、9-slice 边框和细微的光学对齐 调整在每个断点上都保持固定,这是有意为之的。完整的族群列表和每个不变量的理由见 设计令牌 §6.1。
断点和密度(.density-compact / .density-comfortable)都回答"这里有多少空间"这个问题, 因此项目应该能够组合使用它们并获得一个可预测的结果,而不是一个对另一个的无声覆盖。
状态: 以可预测的方式组合断点和密度,并把所有断点 × 密度组合收集到一个生成的 token 样式表中, 是即将发布的 kit 版本的一部分——见 kit changelog。现在这两个轴仍然可能在某些断点上竞争; 一旦该版本发布,此说明将被一个新行为的普通声明所取代。
在组件中使用
public class ResponsivePanel : SusComponent
{
public Prop<float> PanelWidth = new(300f);
protected override void Created()
{
Watch(BreakpointService.Current, (old, bp) =>
{
PanelWidth.Value = bp >= Breakpoint.Xl ? 400f : 300f;
});
}
}USS:通过 token 而非断点选择器来改变
优先用 token 而非手工为项目 class 编写 .breakpoint-* 选择器——token 已经承载每个断点的正确值, 而且在断点和密度一起被解析后将继续工作(见上面的状态说明):
/* 首选——token 已经随断点变化 */
.responsive-panel { width: var(--sk-tooltip-max-width, 300px); }/* 仍能解析,但这种形式的项目 class 覆盖可能在断点和密度以更高的特异度组合后不再生效——
改为覆盖 token 代替 */
.responsive-panel { width: 300px; }
.breakpoint-xl .responsive-panel { width: 400px; }API
public class SusBreakpointService
{
public Prop<Breakpoint> Current { get; }
public Computed<bool> IsMobile { get; } // ≤ 1024
public Computed<bool> IsTablet { get; } // Md | Lg
public Computed<bool> IsDesktop { get; } // ≥ 1920
/// <summary>When set, width polling is ignored (Storybook / QA).</summary>
public Breakpoint? Override { get; }
public static SusBreakpointService Attach(VisualElement root);
public static SusBreakpointService For(VisualElement root);
public static SusBreakpointService For(SusComponent component);
public void Update(float logicalWidth);
public void UpdateFromElement(VisualElement el);
public void SetOverride(Breakpoint? breakpoint); // null = resume auto
}Attach 是幂等的,也是正确的入口点(不存在 SusBreakpointService.Instance)。