主题
ScreenViewport 分辨率适配
何时使用
当用户要实现或排查以下问题时使用本 Skill:
- 窗口调整、横竖屏切换或移动端 Surface 重建后的 UI 适配
- 全屏 HUD、全面屏安全区、不同宽高比和设计分辨率
- Canvas / CanvasAnimated 的绘制分辨率和像素预算
- 用户反馈分辨率变化后布局没有刷新
- 同一版本只在部分 Android / iOS 设备出现 UI 缩放、错位、裁切或字号异常
推荐入口
优先订阅客户端 ScreenViewport.Primary.OnViewportChanged。回调触发时,尺寸、方向、设备像素比、安全区和裁切偏移都已经更新完成:
csharp
#if CLIENT
var viewport = GameUI.Device.ScreenViewport.Primary;
viewport.OnViewportChanged += RefreshLayout;
void RefreshLayout()
{
var size = viewport.Size;
var safe = viewport.SafeZonePadding;
var dpr = viewport.DevicePixelRatio;
var orientation = viewport.DisplayOrientations;
RebuildResponsiveLayout(size, safe, dpr, orientation);
}
#endifOnSizeChanged 只适合只关心实际宽高变化的逻辑;完整布局不要只依赖它,因为 Surface 重建可能保持相同 drawable 宽高。OnOrientationChanged 和 OnDevicePixelRatioChanged 可用于专门的局部优化。
坐标和安全区
Size、DesignResolution和普通控件布局使用逻辑坐标。FontSize同样使用设计坐标,随画布缩放;这里的DevicePixelRatio是设计画布到屏幕像素的比例,不等于操作系统密度。排查小字或制定字号时先读 UI 字号与可读性,不要照搬 App 的 12–16 DIP/sp 或重复缩放字体。WidthPx/HeightPx是设备像素,只用于像素级资源或诊断。- 交互控件和重要文字使用
SafeZonePadding内的区域;背景可以铺满屏幕。 - 使用
CoverCentered时,像素坐标换算还要考虑GlobalOffsetPx。 - 不要硬编码 1920x1080、1080x1920 或某个设备分辨率作为运行时尺寸。
Size 已经由当前 DesignResolution 和 ScaleMode 换算完成;例如 Contain 会让非标准宽高比设备的 Size 扩展到完整可见逻辑区域。普通 GameUI 不要再用 WidthPx / HeightPx 重建另一套逻辑视口。
设计分辨率所有权
优先在当前 GameDataGameUI 中配置 DesignResolutionWidth、DesignResolutionHeight 和 ScaleMode,把它作为游戏 UI 的单一配置来源。代码式 UI 若确实需要调用 SetDesignResolution(...),应使用稳定的项目设计值,而不是按设备物理分辨率、当前方向或启动阶段采样动态改写。
SetDesignResolution(...) 修改的是全局 GameUI 坐标系,会同时影响页面、文字、系统 UI 和 Canvas。不要在项目代码中通过以下方式“纠正”框架状态:
- 比较
DisplayOrientations与WidthPx / HeightPx后切换成正方形或临时设计分辨率。 - 根据
WidthPx / HeightPx反推Size,再因为反推值不同而重写设计分辨率。 - 在 viewport 回调、轮询和延迟任务中反复调用
SetDesignResolution(...),直到观察值看似稳定。
如果完整 viewport 快照内部自相矛盾,先记录证据并按客户端 / 框架问题隔离复现;不要把设备时序假设固化成项目级全局缩放状态机。
布局变换边界
选择一个布局坐标来源并保持到底:优先让 Page / FillParent()、对齐、流式布局和 ScreenViewport.Size 完成响应式排版。需要在宽屏中居中展示固定宽高比内容时,可以在逻辑坐标内计算一次 Contain 内容矩形,但不要让同一层同时接受引擎全局缩放和另一套不完整的手工缩放。
若项目手工缩放固定设计坐标,必须让尺寸、位置、间距、圆角、字体和命中区域使用同一个缩放因子。只缩放控件矩形、仍把 FontSize 恢复为固定设计值,会在安全区或窄屏使文字与框体比例失配。安全区也只应用一次:背景可延伸到全屏,内容容器再扣除 SafeZonePadding。
事件处理约束
把回调视为“状态失效通知”,在回调中读取最新属性并标记布局脏;昂贵重排、Canvas 重建和相机调整应合并到下一帧。窗口拖动和旋转可能连续产生多次通知,不要递归修改窗口尺寸,也不要假定回调只触发一次。
完整布局通常只订阅 OnViewportChanged。不要同时把 OnViewportChanged、OnSizeChanged、OnOrientationChanged、OnDevicePixelRatioChanged 和定时轮询都接到同一套重排 / 设计分辨率修改逻辑;这些兼容事件可能在统一快照前分别到达,重复入口会放大时序差异。布局缓存必须包含或失效其全部输入,例如 viewport、safe area、父容器设计尺寸和内容模式,不能只按上次写入的子控件矩形跳过刷新。
如果对象会重复创建和销毁,保存处理器并在结束时取消订阅,避免重复刷新。临时 UI 不再复用时调用 Destroy()。
设备特异错位审计
不要只比较两张截图后猜设备 DPI。请让正常设备与异常设备在同一页面、同一数据和同一客户端版本下同时记录:
WidthPx、HeightPx、Size、DevicePixelRatio、DesignResolution、ScaleMode、DisplayOrientations和SafeZonePadding。- 页面根、主要内容容器、一个文字控件的逻辑尺寸、像素矩形和字号。
- 是否有项目代码调用
SetDesignResolution(...),以及调用前后的完整快照。
先检查 Size.Width * DevicePixelRatio / Size.Height * DevicePixelRatio 是否近似物理像素,再区分根坐标系异常、布局缓存未刷新、只缩放几何未缩放文字,或单个页面使用固定绝对坐标。验证至少覆盖 16:9、超宽屏、较窄横屏、安全区非零和旋转 / Surface 重建;只改变物理像素但保持宽高比时,逻辑布局应保持一致。
客户端能力版本
同尺寸 Surface 重建通知需要客户端能力版本 4。项目一般直接订阅事件即可;框架会在旧客户端上保留普通实际尺寸变化行为。只有需要选择不同实现时才检查:
csharp
#if CLIENT
using GameCore.ClientEnvironment;
bool supportsSurfaceResize = ClientEnvironment.Runtime.Supports(
ClientRuntimeFeature.ScreenViewportSurfaceResizeEvents);
#endif不要通过 BinaryVersion 判断能力,也不要在每帧重复查询 Supports(...)。
Canvas
Canvas 默认自动根据控件尺寸更新绘制分辨率。需要按移动端像素预算降级时,在 viewport 通知中读取像素尺寸并合并更新 Canvas.Resolution;不要把逻辑坐标整体改成设备像素坐标。