Files
Echo/Assets/_Project/Scripts/UI/MessageAligner.cs
T

340 lines
11 KiB
C#
Raw Normal View History

2026-07-08 19:53:15 +08:00
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace UI
{
[RequireComponent(typeof(VerticalLayoutGroup))]
public class MessageAligner : MonoBehaviour
{
[Header("Bubble")]
public Image bubbleBackground;
public Sprite playerBubbleSprite;
public Sprite aiBubbleSprite;
public Color playerColor = new Color(0.2f, 0.6f, 1f);
public Color aiColor = new Color(0.2f, 0.2f, 0.2f);
public Color playerTextColor = Color.white;
public Color aiTextColor = Color.white;
[Header("Avatar Row")]
public HorizontalLayoutGroup rowLayout;
public GameObject avatarObject;
public Image avatarImage;
public Sprite playerAvatarSprite;
public Sprite aiAvatarSprite;
public Color playerAvatarColor = Color.white;
public Color aiAvatarColor = Color.white;
public bool showPlayerAvatar = true;
public bool showAiAvatar = true;
public float rowHorizontalPadding = 20f;
public float rowSpacingWhenAvatarVisible = 14f;
public float rowSpacingWhenAvatarHidden = 0f;
public float rowMinHeight = 80f;
[Header("Layout")]
[Tooltip("Hard cap for bubble width before wrapping kicks in.")]
public float maxBubbleWidth = 1000f;
public bool clampToViewportWidth = true;
[Range(0.1f, 1f)] public float viewportWidthRatio = 0.7f;
private TextMeshProUGUI textComponent;
private Sprite fallbackBubbleSprite;
private Sprite fallbackAvatarSprite;
private bool lastIsPlayer;
private void Awake()
{
ResolveReferences();
CacheFallbacks();
}
private void OnValidate()
{
ResolveReferences();
CacheFallbacks();
}
public void SetAlignment(bool isPlayer)
{
lastIsPlayer = isPlayer;
ResolveReferences();
CacheFallbacks();
ApplyRootLayout();
ApplyAvatarStyle(isPlayer);
ApplyRowLayout(isPlayer);
ApplyBubbleStyle(isPlayer);
ApplyTextStyle(isPlayer);
ApplyWidthLimit();
RebuildLayout();
}
public void Reapply()
{
SetAlignment(lastIsPlayer);
}
private void ResolveReferences()
{
if (bubbleBackground == null)
{
TextMeshProUGUI tmp = GetComponentInChildren<TextMeshProUGUI>(true);
if (tmp != null)
{
bubbleBackground = tmp.GetComponentInParent<Image>();
}
}
if (bubbleBackground != null && textComponent == null)
{
textComponent = bubbleBackground.GetComponentInChildren<TextMeshProUGUI>(true);
}
if (rowLayout == null && bubbleBackground != null && bubbleBackground.transform.parent != null)
{
rowLayout = bubbleBackground.transform.parent.GetComponent<HorizontalLayoutGroup>();
}
if (avatarObject == null && rowLayout != null)
{
foreach (Transform child in rowLayout.transform)
{
if (bubbleBackground == null || child.gameObject != bubbleBackground.gameObject)
{
avatarObject = child.gameObject;
break;
}
}
}
if (avatarImage == null && avatarObject != null)
{
avatarImage = avatarObject.GetComponent<Image>();
}
}
private void CacheFallbacks()
{
if (bubbleBackground != null)
{
if (fallbackBubbleSprite == null)
{
fallbackBubbleSprite = bubbleBackground.sprite;
}
if (aiBubbleSprite == null) aiBubbleSprite = fallbackBubbleSprite;
if (playerBubbleSprite == null) playerBubbleSprite = fallbackBubbleSprite;
}
if (avatarImage != null)
{
if (fallbackAvatarSprite == null)
{
fallbackAvatarSprite = avatarImage.sprite;
}
if (aiAvatarSprite == null) aiAvatarSprite = fallbackAvatarSprite;
if (playerAvatarSprite == null) playerAvatarSprite = fallbackAvatarSprite;
}
}
private void ApplyRootLayout()
{
VerticalLayoutGroup layoutGroup = GetComponent<VerticalLayoutGroup>();
if (layoutGroup == null)
{
return;
}
int horizontalPadding = Mathf.RoundToInt(Mathf.Max(0f, rowHorizontalPadding));
layoutGroup.childControlWidth = true;
layoutGroup.childForceExpandWidth = true;
layoutGroup.childControlHeight = true;
layoutGroup.childForceExpandHeight = false;
layoutGroup.childAlignment = TextAnchor.UpperLeft;
layoutGroup.padding.left = horizontalPadding;
layoutGroup.padding.right = horizontalPadding;
}
private void ApplyRowLayout(bool isPlayer)
{
if (rowLayout == null)
{
return;
}
LayoutElement rowElement = rowLayout.GetComponent<LayoutElement>();
if (rowElement == null)
{
rowElement = rowLayout.gameObject.AddComponent<LayoutElement>();
}
rowElement.flexibleWidth = 1f;
if (rowMinHeight > 0f)
{
rowElement.minHeight = rowMinHeight;
}
bool avatarVisible = avatarObject != null && avatarObject.activeSelf;
rowLayout.childAlignment = isPlayer ? TextAnchor.LowerRight : TextAnchor.LowerLeft;
rowLayout.reverseArrangement = isPlayer && avatarVisible;
rowLayout.spacing = avatarVisible ? rowSpacingWhenAvatarVisible : rowSpacingWhenAvatarHidden;
rowLayout.childControlWidth = false;
rowLayout.childControlHeight = false;
rowLayout.childForceExpandWidth = false;
rowLayout.childForceExpandHeight = false;
}
private void ApplyAvatarStyle(bool isPlayer)
{
bool shouldShowAvatar = isPlayer ? showPlayerAvatar : showAiAvatar;
if (avatarObject != null)
{
avatarObject.SetActive(shouldShowAvatar);
}
if (!shouldShowAvatar || avatarImage == null)
{
return;
}
avatarImage.sprite = ResolveAvatarSprite(isPlayer);
avatarImage.color = isPlayer ? playerAvatarColor : aiAvatarColor;
avatarImage.preserveAspect = true;
}
private void ApplyBubbleStyle(bool isPlayer)
{
if (bubbleBackground == null)
{
return;
}
bubbleBackground.sprite = ResolveBubbleSprite(isPlayer);
bubbleBackground.color = isPlayer ? playerColor : aiColor;
}
private void ApplyTextStyle(bool isPlayer)
{
if (textComponent == null)
{
return;
}
textComponent.color = isPlayer ? playerTextColor : aiTextColor;
}
private void ApplyWidthLimit()
{
if (textComponent == null)
{
return;
}
float effectiveMaxWidth = maxBubbleWidth;
if (clampToViewportWidth)
{
float viewportWidth = GetViewportWidth();
if (viewportWidth > 0f)
{
float viewportMaxWidth = viewportWidth * viewportWidthRatio;
if (viewportMaxWidth > 0f)
{
effectiveMaxWidth = effectiveMaxWidth <= 0f
? viewportMaxWidth
: Mathf.Min(effectiveMaxWidth, viewportMaxWidth);
}
}
}
LayoutElement textLayout = textComponent.GetComponent<LayoutElement>();
if (textLayout == null)
{
textLayout = textComponent.gameObject.AddComponent<LayoutElement>();
}
Vector2 preferredSize =
textComponent.GetPreferredValues(textComponent.text, float.PositiveInfinity, float.PositiveInfinity);
if (effectiveMaxWidth > 0f && preferredSize.x > effectiveMaxWidth)
{
textLayout.enabled = true;
textLayout.preferredWidth = effectiveMaxWidth;
}
else
{
textLayout.enabled = false;
}
}
private Sprite ResolveBubbleSprite(bool isPlayer)
{
if (isPlayer)
{
if (playerBubbleSprite != null) return playerBubbleSprite;
if (aiBubbleSprite != null) return aiBubbleSprite;
}
else
{
if (aiBubbleSprite != null) return aiBubbleSprite;
if (playerBubbleSprite != null) return playerBubbleSprite;
}
return fallbackBubbleSprite;
}
private Sprite ResolveAvatarSprite(bool isPlayer)
{
if (isPlayer)
{
if (playerAvatarSprite != null) return playerAvatarSprite;
if (aiAvatarSprite != null) return aiAvatarSprite;
}
else
{
if (aiAvatarSprite != null) return aiAvatarSprite;
if (playerAvatarSprite != null) return playerAvatarSprite;
}
return fallbackAvatarSprite;
}
private void RebuildLayout()
{
if (rowLayout != null)
{
LayoutRebuilder.ForceRebuildLayoutImmediate(rowLayout.GetComponent<RectTransform>());
}
LayoutRebuilder.ForceRebuildLayoutImmediate(GetComponent<RectTransform>());
RectTransform parentRt = transform.parent as RectTransform;
if (parentRt != null)
{
LayoutRebuilder.ForceRebuildLayoutImmediate(parentRt);
}
}
private float GetViewportWidth()
{
ScrollRect scrollRect = GetComponentInParent<ScrollRect>();
if (scrollRect != null)
{
RectTransform viewport = scrollRect.viewport != null
? scrollRect.viewport
: scrollRect.GetComponent<RectTransform>();
if (viewport != null) return viewport.rect.width;
}
RectTransform parentRt = transform.parent as RectTransform;
if (parentRt != null) return parentRt.rect.width;
return 0f;
}
}
}