跳转到内容

6. 自适应布局(断点) ​

收益:一个 .sharq 界面即可同时交付桌面与手机,无需单独构建。把界面接到下面的断点轴,令牌就会 按窄面板重排布局——而不是手工为移动端重建界面。

屏幕尺寸适配只用一个轴:SusBreakpointService。

没有 High/Low 分辨率服务(SusResolutionService 已被移除),也没有随显示器尺寸自动缩放的 面板比例。密度(.density-compact / .density-comfortable)是手动的产品预设,而不是 屏幕尺寸适配。

示例用的 PanelSettings 使用 ConstantPixelSize,因此断点宽度会跟随面板 / Game 视图的宽度 (不使用 Unity 的 ScaleWithScreenSize 自动缩放)。

csharp
// 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——与承载主题/密度的元素相同)。

宽度来源(与旧的分辨率服务所用路径相同):

  1. 首选: cascadeRoot.resolvedStyle.width(必要时也用 layout.width)
  2. 回退: 面板 visualTree 尺寸
  3. 最后手段: 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。现在这两个轴仍然可能在某些断点上竞争; 一旦该版本发布,此说明将被一个新行为的普通声明所取代。

在组件中使用 ​

csharp
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 已经承载每个断点的正确值, 而且在断点和密度一起被解析后将继续工作(见上面的状态说明):

css
/* 首选——token 已经随断点变化 */
.responsive-panel { width: var(--sk-tooltip-max-width, 300px); }
css
/* 仍能解析,但这种形式的项目 class 覆盖可能在断点和密度以更高的特异度组合后不再生效——
   改为覆盖 token 代替 */
.responsive-panel { width: 300px; }
.breakpoint-xl .responsive-panel { width: 400px; }

API ​

csharp
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)。