UIBlinkEffect.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using TMPro;
  5. using UnityEngine;
  6. using UnityEngine.UI;
  7. namespace AppUI.Util
  8. {
  9. [Serializable]
  10. public class UIBlinkTextItem
  11. {
  12. [Tooltip("留空则忽略")]
  13. public TMP_Text text;
  14. [Tooltip("勾选后使用下方颜色,否则用组件全局文字颜色")]
  15. public bool useCustomColors;
  16. public Color colorA = Color.white;
  17. public Color colorB = Color.red;
  18. }
  19. public enum UIBlinkImageMode
  20. {
  21. /// <summary>在 spriteA / spriteB 之间切换。</summary>
  22. Sprite,
  23. /// <summary>在 colorA / colorB 之间切换(不改 Sprite)。</summary>
  24. Color,
  25. }
  26. [Serializable]
  27. public class UIBlinkImageItem
  28. {
  29. [Tooltip("留空则忽略")]
  30. public Image image;
  31. public UIBlinkImageMode mode = UIBlinkImageMode.Sprite;
  32. [Tooltip("勾选后使用本项 Sprite,否则用组件全局图片")]
  33. public bool useCustomSprites;
  34. public Sprite spriteA;
  35. public Sprite spriteB;
  36. [Tooltip("Color 模式:勾选后用本项颜色,否则用组件全局图片颜色")]
  37. public bool useCustomColors;
  38. public Color colorA = Color.white;
  39. public Color colorB = new Color(1f, 1f, 1f, 0.35f);
  40. }
  41. /// <summary>
  42. /// 文字(TMP)闪烁:切换颜色;图片闪烁:切换 Sprite 或颜色。
  43. /// 可选平滑渐变过渡(颜色插值;Sprite 模式在过渡中点切换贴图并插值 Tint)。
  44. /// </summary>
  45. [DisallowMultipleComponent]
  46. public class UIBlinkEffect : MonoBehaviour
  47. {
  48. [Header("播放")]
  49. [Tooltip("每个状态保持时长(秒);开启渐变时指到达目标状态后的停留时间")]
  50. [SerializeField]
  51. [Min(0.02f)]
  52. float stateInterval = 0.5f;
  53. [SerializeField]
  54. bool loop = true;
  55. [SerializeField]
  56. bool playOnEnable = true;
  57. [Tooltip("停止时是否恢复到状态 A")]
  58. [SerializeField]
  59. bool restoreStateOnStop = true;
  60. [Tooltip("启用后延迟多久开始(秒)")]
  61. [SerializeField]
  62. [Min(0f)]
  63. float startDelay;
  64. [Header("渐变过渡(可选)")]
  65. [Tooltip("开启后 A↔B 之间颜色/透明度平滑插值,而非瞬间切换")]
  66. [SerializeField]
  67. bool useSmoothTransition;
  68. [Tooltip("单次 A→B 或 B→A 的过渡时长(秒)")]
  69. [SerializeField]
  70. [Min(0f)]
  71. float transitionDuration = 0.2f;
  72. [SerializeField]
  73. AnimationCurve transitionCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
  74. [Tooltip("勾选后过渡不受 Time.timeScale 影响")]
  75. [SerializeField]
  76. bool useUnscaledTime;
  77. [Header("文字(全局默认颜色)")]
  78. [SerializeField]
  79. Color textColorA = Color.white;
  80. [SerializeField]
  81. Color textColorB = Color.red;
  82. [SerializeField]
  83. List<UIBlinkTextItem> textItems = new List<UIBlinkTextItem>();
  84. [Header("图片")]
  85. [SerializeField]
  86. UIBlinkImageMode defaultImageMode = UIBlinkImageMode.Sprite;
  87. [SerializeField]
  88. Sprite imageSpriteA;
  89. [SerializeField]
  90. Sprite imageSpriteB;
  91. [SerializeField]
  92. Color imageColorA = Color.white;
  93. [SerializeField]
  94. Color imageColorB = new Color(1f, 1f, 1f, 0.35f);
  95. [SerializeField]
  96. List<UIBlinkImageItem> imageItems = new List<UIBlinkImageItem>();
  97. Coroutine _blinkRoutine;
  98. bool _showingStateA = true;
  99. bool _isPlaying;
  100. public bool IsPlaying => _isPlaying;
  101. public bool UseSmoothTransition
  102. {
  103. get => useSmoothTransition;
  104. set => useSmoothTransition = value;
  105. }
  106. public float TransitionDuration
  107. {
  108. get => transitionDuration;
  109. set => transitionDuration = Mathf.Max(0f, value);
  110. }
  111. public float StateInterval
  112. {
  113. get => stateInterval;
  114. set => stateInterval = Mathf.Max(0.02f, value);
  115. }
  116. public bool Loop
  117. {
  118. get => loop;
  119. set => loop = value;
  120. }
  121. void OnEnable()
  122. {
  123. if (playOnEnable)
  124. Play();
  125. }
  126. void OnDisable()
  127. {
  128. Stop(restoreStateOnStop);
  129. }
  130. public void Play()
  131. {
  132. Stop(false);
  133. _showingStateA = true;
  134. ApplyStateLerp(0f);
  135. _blinkRoutine = StartCoroutine(BlinkRoutine());
  136. _isPlaying = true;
  137. }
  138. public void Stop() => Stop(restoreStateOnStop);
  139. public void Stop(bool restore)
  140. {
  141. if (_blinkRoutine != null)
  142. {
  143. StopCoroutine(_blinkRoutine);
  144. _blinkRoutine = null;
  145. }
  146. _isPlaying = false;
  147. if (restore)
  148. ApplyStateLerp(0f);
  149. }
  150. public void SetState(bool stateA)
  151. {
  152. _showingStateA = stateA;
  153. ApplyStateLerp(stateA ? 0f : 1f);
  154. }
  155. IEnumerator BlinkRoutine()
  156. {
  157. if (startDelay > 0f)
  158. yield return WaitSeconds(startDelay);
  159. _showingStateA = true;
  160. ApplyStateLerp(0f);
  161. while (true)
  162. {
  163. yield return WaitSeconds(stateInterval);
  164. var targetA = !_showingStateA;
  165. yield return TransitionToState(targetA);
  166. _showingStateA = targetA;
  167. if (!loop)
  168. {
  169. if (!_showingStateA)
  170. {
  171. yield return WaitSeconds(stateInterval);
  172. yield return TransitionToState(true);
  173. _showingStateA = true;
  174. }
  175. _isPlaying = false;
  176. _blinkRoutine = null;
  177. yield break;
  178. }
  179. }
  180. }
  181. IEnumerator TransitionToState(bool targetStateA)
  182. {
  183. if (!useSmoothTransition || transitionDuration <= 0f)
  184. {
  185. ApplyStateLerp(targetStateA ? 0f : 1f);
  186. yield break;
  187. }
  188. var startT = _showingStateA ? 0f : 1f;
  189. var endT = targetStateA ? 0f : 1f;
  190. var elapsed = 0f;
  191. while (elapsed < transitionDuration)
  192. {
  193. elapsed += useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime;
  194. var normalized = Mathf.Clamp01(elapsed / transitionDuration);
  195. var curveT = transitionCurve != null && transitionCurve.length > 0
  196. ? transitionCurve.Evaluate(normalized)
  197. : normalized;
  198. ApplyStateLerp(Mathf.Lerp(startT, endT, curveT));
  199. yield return null;
  200. }
  201. ApplyStateLerp(endT);
  202. }
  203. IEnumerator WaitSeconds(float seconds)
  204. {
  205. if (seconds <= 0f)
  206. yield break;
  207. if (useUnscaledTime)
  208. {
  209. var end = Time.unscaledTime + seconds;
  210. while (Time.unscaledTime < end)
  211. yield return null;
  212. }
  213. else
  214. {
  215. yield return new WaitForSeconds(seconds);
  216. }
  217. }
  218. /// <param name="t">0 = 状态 A,1 = 状态 B</param>
  219. void ApplyStateLerp(float t)
  220. {
  221. t = Mathf.Clamp01(t);
  222. ApplyTextStateLerp(t);
  223. ApplyImageStateLerp(t);
  224. }
  225. void ApplyTextStateLerp(float t)
  226. {
  227. if (textItems == null)
  228. return;
  229. foreach (var item in textItems)
  230. {
  231. if (item?.text == null)
  232. continue;
  233. var a = item.useCustomColors ? item.colorA : textColorA;
  234. var b = item.useCustomColors ? item.colorB : textColorB;
  235. item.text.color = Color.Lerp(a, b, t);
  236. }
  237. }
  238. void ApplyImageStateLerp(float t)
  239. {
  240. if (imageItems == null)
  241. return;
  242. foreach (var item in imageItems)
  243. {
  244. if (item?.image == null)
  245. continue;
  246. var mode = item.mode;
  247. if (mode == UIBlinkImageMode.Sprite && item.image.sprite == null && defaultImageMode == UIBlinkImageMode.Color)
  248. mode = UIBlinkImageMode.Color;
  249. var ca = item.useCustomColors ? item.colorA : imageColorA;
  250. var cb = item.useCustomColors ? item.colorB : imageColorB;
  251. if (mode == UIBlinkImageMode.Color)
  252. {
  253. item.image.color = Color.Lerp(ca, cb, t);
  254. continue;
  255. }
  256. var spA = item.useCustomSprites ? item.spriteA : imageSpriteA;
  257. var spB = item.useCustomSprites ? item.spriteB : imageSpriteB;
  258. if (useSmoothTransition && transitionDuration > 0f)
  259. {
  260. var sp = t < 0.5f ? spA : spB;
  261. if (sp != null)
  262. item.image.sprite = sp;
  263. item.image.color = Color.Lerp(ca, cb, t);
  264. }
  265. else
  266. {
  267. var sp = t < 0.5f ? spA : spB;
  268. if (sp != null)
  269. item.image.sprite = sp;
  270. item.image.color = Color.Lerp(ca, cb, t);
  271. }
  272. }
  273. }
  274. #if UNITY_EDITOR
  275. [ContextMenu("Preview State A")]
  276. void EditorPreviewA()
  277. {
  278. ApplyStateLerp(0f);
  279. }
  280. [ContextMenu("Preview State B")]
  281. void EditorPreviewB()
  282. {
  283. ApplyStateLerp(1f);
  284. }
  285. [ContextMenu("Preview Transition 50%")]
  286. void EditorPreviewMid()
  287. {
  288. ApplyStateLerp(0.5f);
  289. }
  290. #endif
  291. }
  292. }