using UnityEngine; using UnityEngine.UI; /// /// 让 CanvasScaler 的 matchWidthOrHeight 随屏幕宽高比动态切换,实现“设计区域永不裁切”(contain): /// - 屏幕比设计更宽(>16:9):锁高度(match=1),左右由背景 cover 填满,UI 不被竖向裁。 /// - 屏幕比设计更窄(<16:9,如 16:10 / 4:3):锁宽度(match=0),上下由背景 cover 填满,UI 不被横向裁。 /// 这样 1920x1080 设计范围内的所有 UI 在任意宽高比下都完整可见,仅“更宽松”的一侧留出由背景填充的余量。 /// /// 满足三要求: /// 1) 不改业务逻辑、不改对象引用;只调 CanvasScaler 的一个表现参数。 /// 2) 16:9 屏下 match=1 与原状态一致,逐像素不变;只改缩放基准,不动任何锚点/位置/动画。 /// 3) 窄屏不再裁切、宽屏铺满,配合背景 cover 始终填满屏幕。 /// [RequireComponent(typeof(CanvasScaler))] [DisallowMultipleComponent] public sealed class UICanvasMatchFitter : MonoBehaviour { private const float DesignAspect = 1920f / 1080f; private CanvasScaler scaler; private float lastAspect = -1f; private void OnEnable() { scaler = GetComponent(); // 切换 UI 时 Canvas 激活会走到这里。若只是设 match 而不立即同步, // 序列化的旧 match(=1) 会在激活当帧先生效、下一帧才被本组件改成正确值, // 造成偏心元素(如偶像名字 anchoredX≈-201)"先偏后正"的一帧跳动。 // 故激活时同步应用并强制刷新 Canvas,消除这一帧时序。 Apply(forceCanvasUpdate: true); } private void Update() { // 分辨率/窗口变化时才重算。 float aspect = (float)Screen.width / Mathf.Max(1, Screen.height); if (Mathf.Abs(aspect - lastAspect) > 0.0001f) Apply(forceCanvasUpdate: false); } private void Apply(bool forceCanvasUpdate) { if (scaler == null) scaler = GetComponent(); if (scaler == null) return; if (scaler.uiScaleMode != CanvasScaler.ScaleMode.ScaleWithScreenSize) return; float aspect = (float)Screen.width / Mathf.Max(1, Screen.height); lastAspect = aspect; scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight; // 更宽→锁高度(1);更窄→锁宽度(0)。等于始终匹配“更受限的一侧”,设计区域完整可见。 float target = aspect >= DesignAspect ? 1f : 0f; bool changed = !Mathf.Approximately(scaler.matchWidthOrHeight, target); scaler.matchWidthOrHeight = target; if (forceCanvasUpdate && changed) { // 让 CanvasScaler 立即按新 match 重算 scaleFactor、并在同一帧完成布局, // 避免"旧 scale 定位一帧 → 下一帧修正"的可见跳动。 // ForceUpdateCanvases 会驱动 CanvasScaler 重算并刷新布局。 Canvas.ForceUpdateCanvases(); } } }