UMiniGame/Assets/Games/Scripts/ImageAspectFill.cs
2025-12-27 15:29:20 +08:00

209 lines
7.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
/// <summary>
/// 终极等比例版Image严格按原始比例填满父容器尺寸正常、无留白、不变形、无报错
/// </summary>
[RequireComponent(typeof(Image))]
[ExecuteInEditMode]
public class ImageAspectFill : MonoBehaviour
{
[Header("基础配置")]
[Tooltip("Canvas的参考分辨率必须和CanvasScaler一致如1920x1080")]
public Vector2 canvasReferenceSize = new Vector2(1920, 1080);
[Tooltip("最大缩放倍数(防止过度放大模糊)")]
public float maxScale = 10f;
[Header("调试开关")]
public bool showLog = false;
private Image targetImage;
private RectTransform imageRect;
private RectTransform parentRect;
private CanvasScaler canvasScaler;
private float originalAspectRatio; // Sprite原始宽高比全程锁定
private float canvasScaleFactor; // 统一的Canvas缩放因子宽高共用
private float baseUIWidth; // 基准UI宽度按比例推导高度
private float lastScreenSizeW = 0;
private float lastScreenSizeH = 0;
void Start()
{
lastScreenSizeW = Screen.width;
lastScreenSizeH = Screen.height;
StartCoroutine(DelayedInit());
}
void OnEnable()
{
if (targetImage != null)
{
FitToParentProportionally();
}
else
{
StartCoroutine(DelayedInit());
}
}
void OnRectTransformDimensionsChange()
{
if (targetImage != null)
{
FitToParentProportionally();
}
}
private void Update()
{
// 检测Game视图窗口尺寸变化编辑器下
if (lastScreenSizeW != Screen.width || lastScreenSizeH != Screen.height)
{
lastScreenSizeW = Screen.width;
lastScreenSizeH = Screen.height;
if (showLog)
{
Debug.Log($"[编辑器窗口变化] 尺寸变为 {lastScreenSizeW}x{lastScreenSizeH}重新适配Image", this);
}
FitToParentProportionally();
}
}
/// <summary>
/// 延迟初始化:锁定原始比例+统一缩放因子
/// </summary>
private IEnumerator DelayedInit()
{
yield return null; // 等待Canvas布局完成
// 初始化组件
targetImage = GetComponent<Image>();
imageRect = GetComponent<RectTransform>();
parentRect = transform.parent.GetComponent<RectTransform>();
canvasScaler = GetComponentInParent<CanvasScaler>();
// 强制锚点居中(等比例缩放的前提)
imageRect.anchorMin = imageRect.anchorMax = imageRect.pivot = new Vector2(0.5f, 0.5f);
//imageRect.anchoredPosition = Vector2.zero;
// 1. 锁定Sprite原始宽高比核心全程不变
if (targetImage.sprite != null && targetImage.sprite.texture != null)
{
float spriteW = targetImage.sprite.texture.width;
float spriteH = targetImage.sprite.texture.height;
originalAspectRatio = spriteW / spriteH;
// 基准UI宽度取Sprite像素转换为UI单位的宽度统一缩放因子
baseUIWidth = spriteW / GetUnifiedCanvasScaleFactor();
}
else
{
// 无Sprite时用16:9默认比例基准宽度设为参考分辨率的1/2
originalAspectRatio = 16f / 9f;
baseUIWidth = canvasReferenceSize.x / 2f;
}
// 兜底:防止比例异常(避免变成正方形)
originalAspectRatio = Mathf.Clamp(originalAspectRatio, 0.1f, 10f); // 限制比例在0.1~10之间
baseUIWidth = Mathf.Max(baseUIWidth, 50f); // 基准宽度最小50UI单位
// 2. 初始化Image尺寸严格按比例
float initHeight = baseUIWidth / originalAspectRatio;
imageRect.sizeDelta = new Vector2(baseUIWidth, initHeight);
if (showLog)
{
Debug.Log($"[初始化] 原始比例:{originalAspectRatio:0.00} | 初始UI尺寸{baseUIWidth}x{initHeight}", this);
}
// 执行适配
FitToParentProportionally();
}
/// <summary>
/// 核心逻辑:严格按原始比例缩放,填满父容器
/// </summary>
private void FitToParentProportionally()
{
if (parentRect == null || targetImage == null) return;
// 1. 获取父容器的UI尺寸纯UI单位无缩放
Vector2 parentUISize = parentRect.rect.size;
float parentW = Mathf.Max(parentUISize.x, 50f);
float parentH = Mathf.Max(parentUISize.y, 50f);
// 2. 计算两个方向的缩放系数(严格按原始比例)
// 按宽度填满需要的缩放:父宽 / 基准UI宽
float scaleForWidth = parentW / baseUIWidth;
// 按高度填满需要的缩放:父高 / (基准UI宽 / 原始比例) → 等价于 父高×原始比例 / 基准UI宽
float scaleForHeight = (parentH * originalAspectRatio) / baseUIWidth;
// 3. 取最大值(填满父容器)+ 限制最大缩放
float finalScale = Mathf.Min(Mathf.Max(scaleForWidth, scaleForHeight), maxScale);
// 安全校验
if (float.IsInfinity(finalScale) || float.IsNaN(finalScale))
{
finalScale = 1f;
Debug.LogWarning($"缩放系数异常兜底为1倍", this);
}
// 4. 应用缩放(仅改缩放,尺寸不变,比例绝对锁定)
imageRect.localScale = new Vector3(finalScale, finalScale, 1f);
// 调试:验证最终比例
float finalW = baseUIWidth * finalScale;
float finalH = finalW / originalAspectRatio;
if (showLog)
{
Debug.Log($"[适配] 缩放系数:{finalScale:0.00} | 最终尺寸:{finalW:0.00}x{finalH:0.00} | 最终比例:{finalW / finalH:0.00}(原始:{originalAspectRatio:0.00}", this);
}
}
/// <summary>
/// 获取统一的Canvas缩放因子宽高共用不破坏比例
/// </summary>
private float GetUnifiedCanvasScaleFactor()
{
if (canvasScaler == null) return 1f;
// 适配CanvasScaler的三种模式全程返回单一缩放因子
switch (canvasScaler.uiScaleMode)
{
case CanvasScaler.ScaleMode.ConstantPixelSize:
return canvasScaler.scaleFactor;
case CanvasScaler.ScaleMode.ScaleWithScreenSize:
// 统一缩放因子:按最短边匹配参考分辨率
float scaleX = (float)Screen.width / canvasReferenceSize.x;
float scaleY = (float)Screen.height / canvasReferenceSize.y;
return canvasScaler.screenMatchMode == CanvasScaler.ScreenMatchMode.MatchWidthOrHeight
? Mathf.Lerp(scaleX, scaleY, canvasScaler.matchWidthOrHeight)
: Mathf.Min(scaleX, scaleY); // 按最短边缩放,统一因子
case CanvasScaler.ScaleMode.ConstantPhysicalSize:
return canvasScaler.scaleFactor;
default:
return 1f;
}
}
/// <summary>
/// 手动刷新更换Sprite后调用
/// </summary>
public void RefreshFit()
{
StartCoroutine(DelayedInit());
}
/// <summary>
/// 编辑器预览在Scene视图中也能看到效果
/// </summary>
private void OnDrawGizmos()
{
if (imageRect != null && showLog)
{
Gizmos.color = Color.green;
Gizmos.DrawWireCube(imageRect.position, imageRect.sizeDelta * imageRect.localScale);
}
}
}