506 lines
18 KiB
C#
506 lines
18 KiB
C#
using System;
|
|
using System.Collections;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using Febucci.UI;
|
|
using Febucci.UI.Core;
|
|
|
|
namespace AibisDream.UI
|
|
{
|
|
/// <summary>
|
|
/// Terminal风格的TMP_Text组件,自动滚动到底部并遮罩超出内容
|
|
/// </summary>
|
|
[RequireComponent(typeof(RectTransform))]
|
|
public class TerminalText : MonoBehaviour
|
|
{
|
|
#region 字段
|
|
|
|
[SerializeField] private TMP_Text tmpText;
|
|
[SerializeField] private RectTransform viewportRectTransform;
|
|
[SerializeField] private TextAnimator_TMP textAnimator;
|
|
[SerializeField] private TypewriterCore typewriter;
|
|
|
|
private RectTransform rectTransform;
|
|
private RectTransform tmpTextRectTransform;
|
|
private Coroutine autoScrollCoroutine;
|
|
private float lastContentHeight = 0f;
|
|
|
|
public event Action OnTextShown;
|
|
|
|
#endregion
|
|
|
|
#region Unity生命周期
|
|
|
|
private void Awake()
|
|
{
|
|
InitializeComponentRefs();
|
|
ValidateMaskComponent();
|
|
}
|
|
|
|
private void OnRectTransformDimensionsChange()
|
|
{
|
|
if (tmpText != null && rectTransform != null)
|
|
{
|
|
// Viewport尺寸变化时,重新调整位置
|
|
UpdateBounds();
|
|
}
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
// 取消事件注册
|
|
if (typewriter != null)
|
|
{
|
|
typewriter.onTextShowed.RemoveListener(OnTextAnimationComplete);
|
|
typewriter.onCharacterVisible.RemoveListener(OnCharacterVisible);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 初始化
|
|
|
|
private void InitializeComponentRefs()
|
|
{
|
|
// 获取Viewport的RectTransform
|
|
rectTransform = viewportRectTransform != null ? viewportRectTransform : transform as RectTransform;
|
|
|
|
// 如果没有指定TMP_Text,尝试从子对象获取
|
|
if (tmpText == null)
|
|
{
|
|
tmpText = GetComponentInChildren<TMP_Text>();
|
|
}
|
|
|
|
if (tmpText != null)
|
|
{
|
|
tmpTextRectTransform = tmpText.rectTransform;
|
|
// 初始化内容高度(使用GetWorldCorners方法)
|
|
tmpText.ForceMeshUpdate();
|
|
Vector3[] contentCorners = new Vector3[4];
|
|
tmpTextRectTransform.GetWorldCorners(contentCorners);
|
|
Matrix4x4 viewWorldToLocalMatrix = rectTransform.worldToLocalMatrix;
|
|
Vector3 vMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
|
|
Vector3 vMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
|
|
|
|
for (int j = 0; j < 4; j++)
|
|
{
|
|
Vector3 v = viewWorldToLocalMatrix.MultiplyPoint3x4(contentCorners[j]);
|
|
vMin = Vector3.Min(v, vMin);
|
|
vMax = Vector3.Max(v, vMax);
|
|
}
|
|
|
|
Bounds contentBounds = new Bounds(vMin, Vector3.zero);
|
|
contentBounds.Encapsulate(vMax);
|
|
lastContentHeight = contentBounds.size.y;
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("TerminalText: 未找到TMP_Text组件!请在Inspector中指定或确保子对象中有TMP_Text组件。");
|
|
}
|
|
|
|
// 如果没有指定TextAnimator,尝试从TMP_Text的GameObject获取
|
|
if (textAnimator == null && tmpText != null)
|
|
{
|
|
textAnimator = tmpText.GetComponent<TextAnimator_TMP>();
|
|
}
|
|
|
|
// 如果没有指定Typewriter,尝试从TMP_Text的GameObject获取
|
|
if (typewriter == null && tmpText != null)
|
|
{
|
|
typewriter = tmpText.GetComponent<TypewriterCore>();
|
|
}
|
|
|
|
// 如果找到了Typewriter,注册事件
|
|
if (typewriter != null)
|
|
{
|
|
typewriter.onTextShowed.AddListener(OnTextAnimationComplete);
|
|
// 监听字符显示事件,在每次显示新字符时更新滚动位置
|
|
typewriter.onCharacterVisible.AddListener(OnCharacterVisible);
|
|
}
|
|
}
|
|
|
|
private void ValidateMaskComponent()
|
|
{
|
|
Mask mask = GetComponent<Mask>();
|
|
RectMask2D rectMask = GetComponent<RectMask2D>();
|
|
|
|
if (mask == null && rectMask == null)
|
|
{
|
|
Debug.LogWarning("TerminalText: Viewport上未找到Mask或RectMask2D组件,超出内容不会被遮罩!");
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (Input.GetKeyDown(KeyCode.K))
|
|
{
|
|
AddText("Hello, World!");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 公共方法
|
|
|
|
/// <summary>
|
|
/// 添加新文本(追加到现有文本)
|
|
/// </summary>
|
|
public void AddText(string text)
|
|
{
|
|
if (string.IsNullOrEmpty(text))
|
|
return;
|
|
|
|
// 如果使用Typewriter和TextAnimator,使用ShowText方法追加文本并播放动画
|
|
if (typewriter != null && textAnimator != null)
|
|
{
|
|
// 获取当前完整文本
|
|
string currentText = textAnimator.textFull;
|
|
|
|
// 添加换行符(如果不是第一行)
|
|
string textToAppend = string.IsNullOrEmpty(currentText)
|
|
? text
|
|
: "\n" + text;
|
|
|
|
// 记录当前已显示的字符数
|
|
int previousVisibleCount = textAnimator.maxVisibleCharacters;
|
|
|
|
if (previousVisibleCount > 0)
|
|
{
|
|
// 已经有文本显示,使用AppendText追加新文本
|
|
// AppendText会保留已显示文本的可见性,并正确初始化新字符
|
|
textAnimator.AppendText(textToAppend, hideText: true);
|
|
|
|
// AppendText最后会设置maxVisibleCharacters = CharactersCount(所有字符可见)
|
|
// 我们需要重新设置maxVisibleCharacters,保留之前已显示的字符,新追加的文本暂时隐藏
|
|
// 这样Typewriter就可以继续显示新文本了
|
|
textAnimator.firstVisibleCharacter = 0;
|
|
textAnimator.maxVisibleCharacters = previousVisibleCount;
|
|
|
|
// 等待一帧,确保AppendText的更新完成,然后再启动Typewriter
|
|
StartCoroutine(DelayedStartTypewriter());
|
|
}
|
|
else
|
|
{
|
|
// 如果是第一次添加文本,使用ShowText自动启动Typewriter
|
|
typewriter.ShowText(text);
|
|
}
|
|
}
|
|
else if (textAnimator != null)
|
|
{
|
|
// 只有TextAnimator,没有Typewriter
|
|
// 添加换行符(如果不是第一行)
|
|
string textToAppend = string.IsNullOrEmpty(textAnimator.textWithoutTextAnimTags)
|
|
? text
|
|
: "\n" + text;
|
|
|
|
// 使用TextAnimator的AppendText方法追加文本
|
|
textAnimator.AppendText(textToAppend, hideText: false);
|
|
|
|
// 立即更新布局和滚动
|
|
tmpText.ForceMeshUpdate();
|
|
StartCoroutine(DelayedAutoScroll());
|
|
}
|
|
else
|
|
{
|
|
// 没有TextAnimator时,使用原来的方式
|
|
if (tmpText == null)
|
|
return;
|
|
|
|
if (string.IsNullOrEmpty(tmpText.text))
|
|
{
|
|
tmpText.text = text;
|
|
}
|
|
else
|
|
{
|
|
tmpText.text += "\n" + text; // 添加换行符分隔
|
|
}
|
|
|
|
// 强制更新布局以获取最新边界
|
|
tmpText.ForceMeshUpdate();
|
|
|
|
// 自动滚动到底部(显示最新内容)
|
|
AutoScrollToBottom();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置Typewriter速度
|
|
/// </summary>
|
|
public void SetTypewriterSpeed(float speed)
|
|
{
|
|
if (typewriter != null)
|
|
{
|
|
typewriter.SetTypewriterSpeed(speed);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 文本动画完成回调
|
|
/// </summary>
|
|
private void OnTextAnimationComplete()
|
|
{
|
|
// 文本动画完成后,更新布局并滚动到底部
|
|
if (tmpText != null)
|
|
{
|
|
tmpText.ForceMeshUpdate();
|
|
StartCoroutine(DelayedAutoScroll());
|
|
// 并触发文本显示事件
|
|
OnTextShown?.Invoke();
|
|
OnTextShown = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 字符显示事件回调(每次显示新字符时调用)
|
|
/// </summary>
|
|
private void OnCharacterVisible(char character)
|
|
{
|
|
// 每次显示新字符时,延迟更新滚动位置
|
|
// 这样可以确保在文本增长过程中持续滚动到底部
|
|
// 使用协程延迟一帧,确保布局已更新
|
|
// 如果已经有协程在运行,先停止它
|
|
if (autoScrollCoroutine != null)
|
|
{
|
|
StopCoroutine(autoScrollCoroutine);
|
|
}
|
|
autoScrollCoroutine = StartCoroutine(DelayedAutoScroll());
|
|
}
|
|
|
|
/// <summary>
|
|
/// 在LateUpdate中持续检查文本高度变化并更新滚动位置
|
|
/// </summary>
|
|
private void LateUpdate()
|
|
{
|
|
if (tmpText == null || rectTransform == null || tmpTextRectTransform == null)
|
|
return;
|
|
|
|
// 如果使用Typewriter,在动画过程中持续检查
|
|
if (typewriter != null && typewriter.isShowingText)
|
|
{
|
|
tmpText.ForceMeshUpdate();
|
|
|
|
// 使用GetWorldCorners计算实际内容高度
|
|
Vector3[] contentCorners = new Vector3[4];
|
|
tmpTextRectTransform.GetWorldCorners(contentCorners);
|
|
Matrix4x4 viewWorldToLocalMatrix = rectTransform.worldToLocalMatrix;
|
|
Vector3 vMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
|
|
Vector3 vMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
|
|
|
|
for (int j = 0; j < 4; j++)
|
|
{
|
|
Vector3 v = viewWorldToLocalMatrix.MultiplyPoint3x4(contentCorners[j]);
|
|
vMin = Vector3.Min(v, vMin);
|
|
vMax = Vector3.Max(v, vMax);
|
|
}
|
|
|
|
Bounds contentBounds = new Bounds(vMin, Vector3.zero);
|
|
contentBounds.Encapsulate(vMax);
|
|
float currentContentHeight = contentBounds.size.y;
|
|
|
|
// 如果内容高度发生变化,更新滚动位置
|
|
if (Mathf.Abs(currentContentHeight - lastContentHeight) > 0.01f)
|
|
{
|
|
lastContentHeight = currentContentHeight;
|
|
AutoScrollToBottom();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 延迟一帧后自动滚动(确保布局已更新)
|
|
/// </summary>
|
|
private IEnumerator DelayedAutoScroll()
|
|
{
|
|
yield return null;
|
|
AutoScrollToBottom();
|
|
autoScrollCoroutine = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 延迟启动Typewriter(确保AppendText的更新完成)
|
|
/// </summary>
|
|
private IEnumerator DelayedStartTypewriter()
|
|
{
|
|
yield return null;
|
|
if (typewriter != null && textAnimator != null)
|
|
{
|
|
// 临时设置maxVisibleCharacters为CharactersCount,让Typewriter能显示所有字符
|
|
// Typewriter的ShowTextRoutine会跳过已可见的字符,所以只会显示新追加的部分
|
|
int savedMaxVisible = textAnimator.maxVisibleCharacters;
|
|
textAnimator.maxVisibleCharacters = textAnimator.CharactersCount;
|
|
|
|
// 手动启动Typewriter继续显示新文本
|
|
typewriter.StartShowingText(false);
|
|
|
|
// 注意:Typewriter会在显示过程中更新maxVisibleCharacters,所以不需要恢复
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 清空所有文本并重置位置
|
|
/// </summary>
|
|
public void Clear()
|
|
{
|
|
// 如果使用TextAnimator,使用SetText方法
|
|
if (textAnimator != null)
|
|
{
|
|
textAnimator.SetText(string.Empty);
|
|
}
|
|
else if (tmpText != null)
|
|
{
|
|
tmpText.text = string.Empty;
|
|
}
|
|
|
|
// 重置位置到顶部
|
|
if (tmpTextRectTransform != null)
|
|
{
|
|
Vector2 pos = tmpTextRectTransform.anchoredPosition;
|
|
pos.y = 0;
|
|
tmpTextRectTransform.anchoredPosition = pos;
|
|
}
|
|
|
|
if (tmpText != null)
|
|
{
|
|
tmpText.ForceMeshUpdate();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 内部方法
|
|
|
|
/// <summary>
|
|
/// 更新边界信息(类似ScrollRect)
|
|
/// </summary>
|
|
private void UpdateBounds()
|
|
{
|
|
if (tmpText == null || rectTransform == null)
|
|
return;
|
|
|
|
// 强制更新布局以获取最新边界
|
|
tmpText.ForceMeshUpdate();
|
|
|
|
// 获取Viewport和Content的实际大小
|
|
float viewportHeight = rectTransform.rect.height;
|
|
float contentHeight = tmpText.bounds.size.y;
|
|
|
|
// 如果内容超出Viewport,调整位置显示底部
|
|
if (contentHeight > viewportHeight)
|
|
{
|
|
AutoScrollToBottom();
|
|
}
|
|
else
|
|
{
|
|
// 内容未满,重置到顶部
|
|
if (tmpTextRectTransform != null)
|
|
{
|
|
Vector2 pos = tmpTextRectTransform.anchoredPosition;
|
|
pos.y = 0;
|
|
tmpTextRectTransform.anchoredPosition = pos;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 自动滚动到底部(核心实现)
|
|
/// 使用类似ScrollRect的方法计算bounds和位置
|
|
/// </summary>
|
|
private void AutoScrollToBottom()
|
|
{
|
|
if (tmpText == null || rectTransform == null || tmpTextRectTransform == null)
|
|
return;
|
|
|
|
// 确保布局已更新
|
|
tmpText.ForceMeshUpdate();
|
|
|
|
// 使用GetWorldCorners获取content的实际bounds(类似ScrollRect的GetBounds方法)
|
|
Vector3[] contentCorners = new Vector3[4];
|
|
tmpTextRectTransform.GetWorldCorners(contentCorners);
|
|
|
|
// 将content的world corners转换到viewport的本地空间
|
|
Matrix4x4 viewWorldToLocalMatrix = rectTransform.worldToLocalMatrix;
|
|
Vector3 vMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
|
|
Vector3 vMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
|
|
|
|
for (int j = 0; j < 4; j++)
|
|
{
|
|
Vector3 v = viewWorldToLocalMatrix.MultiplyPoint3x4(contentCorners[j]);
|
|
vMin = Vector3.Min(v, vMin);
|
|
vMax = Vector3.Max(v, vMax);
|
|
}
|
|
|
|
Bounds contentBounds = new Bounds(vMin, Vector3.zero);
|
|
contentBounds.Encapsulate(vMax);
|
|
|
|
// 获取Viewport的bounds
|
|
Bounds viewBounds = new Bounds(rectTransform.rect.center, rectTransform.rect.size);
|
|
|
|
// 计算内容高度和Viewport高度
|
|
float contentHeight = contentBounds.size.y;
|
|
float viewportHeight = viewBounds.size.y;
|
|
|
|
// 如果内容高度超过Viewport,需要调整位置
|
|
if (contentHeight > viewportHeight)
|
|
{
|
|
// 计算偏移量:让内容底部对齐到Viewport底部
|
|
// contentBounds.min.y是内容底部在viewport本地空间的位置
|
|
// viewBounds.min.y是viewport底部的位置(通常是负值)
|
|
// 我们需要让contentBounds.min.y = viewBounds.min.y
|
|
float currentBottom = contentBounds.min.y;
|
|
float targetBottom = viewBounds.min.y;
|
|
float offsetY = tmpTextRectTransform.anchoredPosition.y + (targetBottom - currentBottom);
|
|
|
|
Vector2 pos = tmpTextRectTransform.anchoredPosition;
|
|
pos.y = offsetY;
|
|
tmpTextRectTransform.anchoredPosition = pos;
|
|
}
|
|
else
|
|
{
|
|
// 内容未满,重置到顶部
|
|
// 让内容顶部对齐到viewport顶部
|
|
float currentTop = contentBounds.max.y;
|
|
float targetTop = viewBounds.max.y;
|
|
float offsetY = tmpTextRectTransform.anchoredPosition.y + (targetTop - currentTop);
|
|
|
|
Vector2 pos = tmpTextRectTransform.anchoredPosition;
|
|
pos.y = offsetY;
|
|
tmpTextRectTransform.anchoredPosition = pos;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Editor支持
|
|
|
|
#if UNITY_EDITOR
|
|
private void OnValidate()
|
|
{
|
|
// 在编辑器中验证组件引用
|
|
if (tmpText == null)
|
|
{
|
|
tmpText = GetComponentInChildren<TMP_Text>();
|
|
}
|
|
|
|
if (viewportRectTransform == null)
|
|
{
|
|
viewportRectTransform = transform as RectTransform;
|
|
}
|
|
|
|
// 自动查找TextAnimator和Typewriter
|
|
if (textAnimator == null && tmpText != null)
|
|
{
|
|
textAnimator = tmpText.GetComponent<TextAnimator_TMP>();
|
|
}
|
|
|
|
if (typewriter == null && tmpText != null)
|
|
{
|
|
typewriter = tmpText.GetComponent<TypewriterCore>();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#endregion
|
|
}
|
|
}
|