using System; using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; namespace AppUI.Util { [Serializable] public class UIBlinkTextItem { [Tooltip("留空则忽略")] public TMP_Text text; [Tooltip("勾选后使用下方颜色,否则用组件全局文字颜色")] public bool useCustomColors; public Color colorA = Color.white; public Color colorB = Color.red; } public enum UIBlinkImageMode { /// 在 spriteA / spriteB 之间切换。 Sprite, /// 在 colorA / colorB 之间切换(不改 Sprite)。 Color, } [Serializable] public class UIBlinkImageItem { [Tooltip("留空则忽略")] public Image image; public UIBlinkImageMode mode = UIBlinkImageMode.Sprite; [Tooltip("勾选后使用本项 Sprite,否则用组件全局图片")] public bool useCustomSprites; public Sprite spriteA; public Sprite spriteB; [Tooltip("Color 模式:勾选后用本项颜色,否则用组件全局图片颜色")] public bool useCustomColors; public Color colorA = Color.white; public Color colorB = new Color(1f, 1f, 1f, 0.35f); } /// /// 文字(TMP)闪烁:切换颜色;图片闪烁:切换 Sprite 或颜色。 /// 可选平滑渐变过渡(颜色插值;Sprite 模式在过渡中点切换贴图并插值 Tint)。 /// [DisallowMultipleComponent] public class UIBlinkEffect : MonoBehaviour { [Header("播放")] [Tooltip("每个状态保持时长(秒);开启渐变时指到达目标状态后的停留时间")] [SerializeField] [Min(0.02f)] float stateInterval = 0.5f; [SerializeField] bool loop = true; [SerializeField] bool playOnEnable = true; [Tooltip("停止时是否恢复到状态 A")] [SerializeField] bool restoreStateOnStop = true; [Tooltip("启用后延迟多久开始(秒)")] [SerializeField] [Min(0f)] float startDelay; [Header("渐变过渡(可选)")] [Tooltip("开启后 A↔B 之间颜色/透明度平滑插值,而非瞬间切换")] [SerializeField] bool useSmoothTransition; [Tooltip("单次 A→B 或 B→A 的过渡时长(秒)")] [SerializeField] [Min(0f)] float transitionDuration = 0.2f; [SerializeField] AnimationCurve transitionCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f); [Tooltip("勾选后过渡不受 Time.timeScale 影响")] [SerializeField] bool useUnscaledTime; [Header("文字(全局默认颜色)")] [SerializeField] Color textColorA = Color.white; [SerializeField] Color textColorB = Color.red; [SerializeField] List textItems = new List(); [Header("图片")] [SerializeField] UIBlinkImageMode defaultImageMode = UIBlinkImageMode.Sprite; [SerializeField] Sprite imageSpriteA; [SerializeField] Sprite imageSpriteB; [SerializeField] Color imageColorA = Color.white; [SerializeField] Color imageColorB = new Color(1f, 1f, 1f, 0.35f); [SerializeField] List imageItems = new List(); Coroutine _blinkRoutine; bool _showingStateA = true; bool _isPlaying; public bool IsPlaying => _isPlaying; public bool UseSmoothTransition { get => useSmoothTransition; set => useSmoothTransition = value; } public float TransitionDuration { get => transitionDuration; set => transitionDuration = Mathf.Max(0f, value); } public float StateInterval { get => stateInterval; set => stateInterval = Mathf.Max(0.02f, value); } public bool Loop { get => loop; set => loop = value; } void OnEnable() { if (playOnEnable) Play(); } void OnDisable() { Stop(restoreStateOnStop); } public void Play() { Stop(false); _showingStateA = true; ApplyStateLerp(0f); _blinkRoutine = StartCoroutine(BlinkRoutine()); _isPlaying = true; } public void Stop() => Stop(restoreStateOnStop); public void Stop(bool restore) { if (_blinkRoutine != null) { StopCoroutine(_blinkRoutine); _blinkRoutine = null; } _isPlaying = false; if (restore) ApplyStateLerp(0f); } public void SetState(bool stateA) { _showingStateA = stateA; ApplyStateLerp(stateA ? 0f : 1f); } IEnumerator BlinkRoutine() { if (startDelay > 0f) yield return WaitSeconds(startDelay); _showingStateA = true; ApplyStateLerp(0f); while (true) { yield return WaitSeconds(stateInterval); var targetA = !_showingStateA; yield return TransitionToState(targetA); _showingStateA = targetA; if (!loop) { if (!_showingStateA) { yield return WaitSeconds(stateInterval); yield return TransitionToState(true); _showingStateA = true; } _isPlaying = false; _blinkRoutine = null; yield break; } } } IEnumerator TransitionToState(bool targetStateA) { if (!useSmoothTransition || transitionDuration <= 0f) { ApplyStateLerp(targetStateA ? 0f : 1f); yield break; } var startT = _showingStateA ? 0f : 1f; var endT = targetStateA ? 0f : 1f; var elapsed = 0f; while (elapsed < transitionDuration) { elapsed += useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime; var normalized = Mathf.Clamp01(elapsed / transitionDuration); var curveT = transitionCurve != null && transitionCurve.length > 0 ? transitionCurve.Evaluate(normalized) : normalized; ApplyStateLerp(Mathf.Lerp(startT, endT, curveT)); yield return null; } ApplyStateLerp(endT); } IEnumerator WaitSeconds(float seconds) { if (seconds <= 0f) yield break; if (useUnscaledTime) { var end = Time.unscaledTime + seconds; while (Time.unscaledTime < end) yield return null; } else { yield return new WaitForSeconds(seconds); } } /// 0 = 状态 A,1 = 状态 B void ApplyStateLerp(float t) { t = Mathf.Clamp01(t); ApplyTextStateLerp(t); ApplyImageStateLerp(t); } void ApplyTextStateLerp(float t) { if (textItems == null) return; foreach (var item in textItems) { if (item?.text == null) continue; var a = item.useCustomColors ? item.colorA : textColorA; var b = item.useCustomColors ? item.colorB : textColorB; item.text.color = Color.Lerp(a, b, t); } } void ApplyImageStateLerp(float t) { if (imageItems == null) return; foreach (var item in imageItems) { if (item?.image == null) continue; var mode = item.mode; if (mode == UIBlinkImageMode.Sprite && item.image.sprite == null && defaultImageMode == UIBlinkImageMode.Color) mode = UIBlinkImageMode.Color; var ca = item.useCustomColors ? item.colorA : imageColorA; var cb = item.useCustomColors ? item.colorB : imageColorB; if (mode == UIBlinkImageMode.Color) { item.image.color = Color.Lerp(ca, cb, t); continue; } var spA = item.useCustomSprites ? item.spriteA : imageSpriteA; var spB = item.useCustomSprites ? item.spriteB : imageSpriteB; if (useSmoothTransition && transitionDuration > 0f) { var sp = t < 0.5f ? spA : spB; if (sp != null) item.image.sprite = sp; item.image.color = Color.Lerp(ca, cb, t); } else { var sp = t < 0.5f ? spA : spB; if (sp != null) item.image.sprite = sp; item.image.color = Color.Lerp(ca, cb, t); } } } #if UNITY_EDITOR [ContextMenu("Preview State A")] void EditorPreviewA() { ApplyStateLerp(0f); } [ContextMenu("Preview State B")] void EditorPreviewB() { ApplyStateLerp(1f); } [ContextMenu("Preview Transition 50%")] void EditorPreviewMid() { ApplyStateLerp(0.5f); } #endif } }