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
}
}