| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- using UnityEngine;
- using UnityEngine.UI;
- using DG.Tweening;
- using System.Collections;
- using TMPro;
- namespace LocalRank
- {
- public class RankItemData
- {
- public int Rank;
- public string UserName;
- public int Score;
- public string AvatarUrl;
- public bool IsSelf;
- public int RankIndex; // 用于存储当前Item的索引
- public override string ToString()
- {
- return $"Rank: {Rank}, UserName: {UserName}, Score: {Score}, AvatarUrl: {AvatarUrl}, IsSelf: {IsSelf}, RankIndex: {RankIndex}";
- }
- }
- public class RankUpAnimator : MonoBehaviour
- {
- public ScrollRect scrollRect; // 排行榜 ScrollRect
- public RectTransform content; // 排行榜内容区域 (Item们的父节点)
- public RectTransform viewport; // ScrollRect的可视区域
- public RectTransform viewportParent; // targetItem的父节点
- public RectTransform targetItem; // 要冲榜的Item
- public int targetRankIndex; // 目标名次(比如第3名)
- [Header("滚动速度")]
- public float scrollSpeed = 2000f;
- [Header("滚动时间")]
- public float scrollDuration = 1.5f;
- [Header("冲榜上升缩放曲线 (时间轴: 0 ~ scrollDuration)")]
- public AnimationCurve scaleCurve = AnimationCurve.EaseInOut(0, 1.2f, 1.5f, 1.5f);
- [Header("冲榜结束时候滞空时间")]
- public float hangTime = 0.5f; // 滞空时间
- [Header("位移动画 Ease 类型")]
- public Ease moveEase = Ease.Linear;
- [Header("重置频率")]
- public float contentResetThreshold = 100f;
- public float stopDelay = 0.5f;
- private Transform originalParent; // 保存初始Parent
- private Vector3 originalPosition; // 保存初始局部位置
- private Vector3 originalScale; // 保存初始缩放
- private Sequence animationSequence; // DOTween动画序列
- private bool isAnimating = false; // 动画状态
- public System.Action onAnimationStart; // 动画开始回调
- public System.Action onAnimationComplete; // 动画结束回调
- public void StartRankUpAnimation(int fromRank, int toRank)
- {
- if (isAnimating) return;
- isAnimating = true;
- // 触发动画开始回调
- onAnimationStart?.Invoke();
- // 保存原始状态
- originalParent = targetItem.parent;
- originalPosition = targetItem.localPosition;
- //用于计算缩放曲线的初始值
- // 这里的 scaleCurve.Evaluate(0f) 是为了获取曲线在 t=0 时的值,通常是 1.0f
- // 设置统一缩放起始值(来自曲线)
- float startScale = scaleCurve.Evaluate(0f);
- originalScale = targetItem.localScale = Vector3.one * startScale;
- // 使用clone对象来避免原始对象被修改
- targetClone = Instantiate(targetItem.gameObject, targetItem.parent, true);
- targetClone.name = targetItem.name + "_Clone";
- targetClone.transform.localScale = originalScale;
- targetClone.GetComponent<CanvasGroup>().blocksRaycasts = false;
- targetClone.transform.SetSiblingIndex(targetItem.GetSiblingIndex()); // 确保在原始Item上面
- RankItemUI rankItemUI = targetClone.GetComponent<RankItemUI>();
- rankItemUI.SetOtherSprite(); // 设置为其他人的图标
- // Step 1. 脱离LayoutGroup控制,移动到Viewport上
- targetItem.SetParent(viewportParent, true); // true保持世界位置不变
- //targetItem.gameObject.GetComponent<Outline>().enabled = true; // 激活轮廓
- // Step 2. 准备目标位置(计算目标Item应该停在哪)
- Vector3 targetLocalPosition = CalculateTargetLocalPosition(targetRankIndex);
- // Step 3. 开始上升动画 + 底部滚动动画
- animationSequence = DOTween.Sequence();
- animationSequence.SetTarget(targetItem);
- animationSequence.SetUpdate(true); //整个序列使用 UnscaledTime
- // --- Animation Step 1: 快速循环滚动阶段 ---
- animationSequence.AppendCallback(() =>
- {
- StartCoroutine(ScrollAndStop(targetLocalPosition));
- });
- // --- Animation Step 2: 自身向上冲动画 ---
- float totalMoveDuration = scrollDuration - hangTime;
- float fastUpDuration = totalMoveDuration * 0.2f; // 前段快速线性
- float floatUpDuration = totalMoveDuration * 0.8f; // 后段缓慢升空
- Vector3 startPos = targetItem.localPosition;
- Vector3 endPos = new Vector3(startPos.x, targetLocalPosition.y, startPos.z);
- Vector3 midPos = Vector3.Lerp(startPos, endPos, 0.4f); // 中间位置点(快速上升到一半)
- // 快速线性上升阶段
- animationSequence.Append(DOTween.To(() => 0f, t =>
- {
- float progress = t / fastUpDuration;
- targetItem.localPosition = Vector3.Lerp(startPos, midPos, progress);
- }, fastUpDuration, fastUpDuration).SetEase(Ease.Linear));
- // 缓慢升空 + 缩放曲线阶段
- animationSequence.Append(DOTween.To(() => 0f, t =>
- {
- float progress = t / floatUpDuration;
- float easedT = moveEase != Ease.Unset ? DOVirtual.EasedValue(0, 1, progress, moveEase) : progress;
- targetItem.localPosition = Vector3.Lerp(midPos, endPos, easedT);
- //但要确保 t + fastUpDuration <= scrollDuration,否则 Evaluate 超出曲线长度可能导致异常值
- float curveTime = Mathf.Clamp(t + fastUpDuration, 0f, scrollDuration);
- float scaleFactor = scaleCurve.Evaluate(t + fastUpDuration); // 补正时间轴
- targetItem.localScale = originalScale * scaleFactor;
- }, floatUpDuration, floatUpDuration));
- // --- Animation Step 3: 滞空阶段 ---
- // 滞空 -> 落地缓冲效果
- animationSequence.AppendInterval(hangTime); // 滞空 x 秒
- // 落地阶段(快速砸下来)
- animationSequence.Append(targetItem.DOLocalMove(targetLocalPosition, 0.25f)
- .SetEase(Ease.InQuad)); // 快速落地
- // 缩放恢复阶段
- // 能避免 DOTween 残留的内部插值。
- animationSequence.Join(DOTween.To(() => targetItem.localScale, s => targetItem.localScale = s, originalScale, 0.25f)
- .SetEase(Ease.OutQuad));
- // --- Animation Step 4: 滚动到目标名次 ---
- // AnimateRankChange(
- // targetItem.gameObject,
- // fromRank,
- // toRank,
- // scrollDuration
- // );
- // Step 4. 动画完成,归位
- animationSequence.OnComplete(() =>
- {
- //targetItem.gameObject.GetComponent<Outline>().enabled = false; // 关闭轮廓
- ResetItem();
- isAnimating = false;
- onAnimationComplete?.Invoke();
- });
- }
- /**
- * 无限滚动逻辑
- * 1. 先让Content向上滚动
- * 2. 当Content滚动到一定高度时,重置Content位置
- * 3. 等待一段时间后,停止滚动
- */
- IEnumerator ScrollAndStop(Vector3 targetPos)
- {
- float elapsed = 0f;
- while (elapsed < scrollDuration)
- {
- content.anchoredPosition += Vector2.up * scrollSpeed * Time.unscaledDeltaTime;//* Time.deltaTime;
- // 无限滚动逻辑
- if (content.anchoredPosition.y >= contentResetThreshold)
- {
- content.anchoredPosition -= Vector2.up * contentResetThreshold;
- }
- elapsed += Time.unscaledDeltaTime;//Time.deltaTime;
- yield return null;
- }
- yield return new WaitForSecondsRealtime(stopDelay);
- }
- private Vector3 CalculateTargetLocalPosition(int targetRank)
- {
- float itemHeight = targetItem.rect.height;
- VerticalLayoutGroup layoutGroup = content.GetComponent<VerticalLayoutGroup>();
- float spacing = layoutGroup != null ? layoutGroup.spacing : 0f;
- float topPadding = layoutGroup != null ? layoutGroup.padding.top : 0f;
- float totalHeightPerItem = itemHeight + spacing;
- float contentY = -(targetRank - 1) * totalHeightPerItem - topPadding;
- float viewportHeight = viewport.rect.height;
- float contentHeight = content.rect.height;
- float offsetY = 0f; // 你可以自定义偏移
- float viewportY = contentY + contentHeight - viewportHeight + offsetY;
- return new Vector3(targetItem.localPosition.x, viewportY, 0f);
- }
- private GameObject targetClone;
- private void ResetItem()
- {
- if (targetClone != null)
- {
- Destroy(targetClone); // 删除动画用副本
- targetClone = null;
- }
- if (targetItem != null)
- {
- // 把item归回原来的Content下
- targetItem.SetParent(content, false);
- // 移动到正确的位置
- targetItem.SetSiblingIndex(targetRankIndex - 1);
- // 恢复初始缩放
- targetItem.localScale = originalScale;
- // 保证在Layout下重新排列
- LayoutRebuilder.ForceRebuildLayoutImmediate(content.GetComponent<RectTransform>());
- }
- }
- public void StopAnimation()
- {
- if (animationSequence != null && animationSequence.IsPlaying())
- {
- animationSequence.Kill();
- ResetItem();
- isAnimating = false;
- }
- }
- /// <summary>
- /// 平滑地将排名数字从旧值变更为新值
- /// </summary>
- /// <param name="targetGO">包含 Text 或 TMP_Text 的 GameObject</param>
- /// <param name="fromRank">起始名次(通常是旧排名 +1)</param>
- /// <param name="toRank">目标名次(通常是当前排名 +1)</param>
- /// <param name="duration">变化时间</param>
- /// <param name="ease">插值曲线,默认线性</param>
- public void AnimateRankChange(GameObject targetGO, int fromRank, int toRank, float duration, Ease ease = Ease.Linear)
- {
- var tmp = targetGO.GetComponentInChildren<TMP_Text>();
- var text = targetGO.GetComponentInChildren<Text>();
- if (tmp == null && text == null)
- {
- Debug.LogWarning("未找到 TMP_Text 或 Text 组件!");
- return;
- }
- DOTween.To(() => fromRank, value =>
- {
- if (tmp != null)
- tmp.text = value.ToString();
- else if (text != null)
- text.text = value.ToString();
- }, toRank, duration).SetEase(ease);
- }
- }
- }
|