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-*)
下游 UI 包会在 .breakpoint-* 下覆盖间距 / 高度 / 字体(例如下游的 *-tokens.uss 样式表)。组件已经在使用 var(--sk-button-height)、var(--sk-space-16)、 var(--sk-font-body) 等——它们会在根 class 变化时自动响应。
在 kit 的 token 样式表中,断点 token 块排在密度之后,因此当两个 class 同时存在时, 重叠的键(高度、间距)会优先采用当前生效的断点。
在组件中使用
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;
});
}
}通过断点 class 编写 USS
.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)。