跳转到内容

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 / MountSusBreakpointService.Attach(root)
  • 每个 SusComponentGeometryChangedEvent → 推送级联根宽度 (显式调用 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 同时存在时, 重叠的键(高度、间距)会优先采用当前生效的断点。

在组件中使用

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;
        });
    }
}

通过断点 class 编写 USS

css
.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)。