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().blocksRaycasts = false; targetClone.transform.SetSiblingIndex(targetItem.GetSiblingIndex()); // 确保在原始Item上面 RankItemUI rankItemUI = targetClone.GetComponent(); rankItemUI.SetOtherSprite(); // 设置为其他人的图标 // Step 1. 脱离LayoutGroup控制,移动到Viewport上 targetItem.SetParent(viewportParent, true); // true保持世界位置不变 //targetItem.gameObject.GetComponent().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().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(); 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()); } } public void StopAnimation() { if (animationSequence != null && animationSequence.IsPlaying()) { animationSequence.Kill(); ResetItem(); isAnimating = false; } } /// /// 平滑地将排名数字从旧值变更为新值 /// /// 包含 Text 或 TMP_Text 的 GameObject /// 起始名次(通常是旧排名 +1) /// 目标名次(通常是当前排名 +1) /// 变化时间 /// 插值曲线,默认线性 public void AnimateRankChange(GameObject targetGO, int fromRank, int toRank, float duration, Ease ease = Ease.Linear) { var tmp = targetGO.GetComponentInChildren(); var text = targetGO.GetComponentInChildren(); 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); } } }