Files
ichni_Official/Assets/Scripts/UI/SongSelection/SongListControllerUI.cs
SoulliesOfficial 70b2a43824 update
2025-08-22 14:54:40 -04:00

331 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using Sirenix.OdinInspector;
namespace Ichni.Menu
{
// 一个完全自定义的列表控制器,实现了拖拽、惯性、边界和吸附
public partial class SongListControllerUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
[Title("核心组件")]
[SerializeField] public RectTransform content;
[SerializeField] public RectTransform viewport;
[Title("预制体")]
[SerializeField]
private GameObject songItemPrefab;
[Title("对齐与动画")]
[SerializeField] public RectTransform centerPoint;
[SerializeField] private float snapSpeed = 10f;
[SerializeField] private float decelerationRate = 0.15f;
[Title("平滑度优化")]
[SerializeField] [Range(1f, 20f)]
private float dragSmoothing = 16f; // 拖拽平滑度的阻尼值可在Inspector中调节
[SerializeField][Range(1f, 20f)]
private float releaseSmoothing = 4f; // 松手后平滑度的阻尼值可在Inspector中调节
[Title("甩动判定")]
[Tooltip("当松手时的速度大于此值,才会被判定为一次“甩动”并产生惯性")]
[SerializeField] private float flickThreshold = 50f;
public SongSelectionTab selectedTab;
// 内部变量
public List<RectTransform> songItems = new List<RectTransform>();
private Vector2 velocity;
private bool isDragging = false;
public float topBound;
public float bottomBound = 0f;
private Vector2 targetPosition; // 【新增】内容的目标位置
private float targetX = 1500f;
public bool isDuringSnap = false;
public RectTransform closestTab;
public void InitializeList()
{
GenerateSongTabs();
Canvas.ForceUpdateCanvases();
topBound = (songItems.Count * 144f + (songItems.Count - 1) * 60f) - 72f; //topBound中144为tab高度60为tab间距72为tab高度的一半
bottomBound = 72f; //bottomBound中72为tab高度的一半
int songIndex = 0;
if (MenuInformationRecorder.instance.songSelectionRecords.TryGetValue(ChapterSelectionManager.instance.currentChapter, out var record))
{
songIndex = ChapterSelectionManager.instance.currentChapter.songs.FindIndex(song => song.songName == record.song.songName);
}
if (songItems.Count > 0)
{
StartCoroutine(SnapToItem(songItems[songIndex], true));
}
closestTab = songItems[songIndex];
targetPosition = content.anchoredPosition;
targetX = -1500f;
}
private void Update()
{
RectTransform closestItem = closestTab;
float cloesestY = Mathf.Infinity;
foreach (RectTransform item in songItems)
{
float distance = Mathf.Abs(item.position.y - centerPoint.position.y);
if (distance < cloesestY)
{
cloesestY = distance;
closestItem = item;
}
}
if (closestItem != null && closestTab != closestItem)
{
closestTab = closestItem;
MenuAudioManager.instance.audioContainer.PlaySoundFX("SwitchTab");
}
}
// 使用LateUpdate来处理所有位置更新防止抖动
void LateUpdate()
{
// 如果不在拖拽,则处理惯性
// 注意:我们只在 isDragging 为 false 时处理惯性,
// OnEndDrag中已经对速度进行了判断所以这里的逻辑依然适用
if (!isDragging && Mathf.Abs(velocity.y) > 0.1f)
{
// 将速度施加于目标位置
targetPosition.y += velocity.y * Time.deltaTime;
// 速度衰减
velocity *= (1 - decelerationRate);
// 当速度足够小时,停止惯性并触发吸附
if (Mathf.Abs(velocity.y) <= 0.1f)
{
velocity = Vector2.zero;
StartCoroutine(SnapToClosest());
}
}
// 无论何时都将Content的实际位置平滑地Lerp到目标位置
float damping = isDragging ? dragSmoothing : releaseSmoothing;
Vector2 finalPosition = Vector2.Lerp(content.anchoredPosition, targetPosition, Time.deltaTime * damping);
finalPosition.x = targetX;
content.anchoredPosition = finalPosition;
// 每次更新后都施加边界限制
ClampContentPosition();
}
public void OnBeginDrag(PointerEventData eventData)
{
isDragging = true;
velocity = Vector2.zero;
StopAllCoroutines();
// 开始拖拽时,将目标位置与当前位置同步
targetPosition = content.anchoredPosition;
selectedTab?.SetSelection(false);
selectedTab = null; // 清除当前选中的Tab
//DOTween.To(x=>targetX = x, targetX, -1550f, 0.2f).SetEase(Ease.OutQuad).Play();
//songItems.ForEach(item => item.DOScale(1.1f,0.2f).SetEase(Ease.OutQuad).Play());
}
public void OnDrag(PointerEventData eventData)
{
// 拖拽时只更新目标位置而不是直接移动Content
targetPosition += new Vector2(0, eventData.delta.y);
// 【核心修正 #1】计算速度时只使用Y轴分量
velocity = new Vector2(0, eventData.delta.y / Time.deltaTime);
}
public void OnEndDrag(PointerEventData eventData)
{
isDragging = false;
//DOTween.To(x => targetX = x, targetX, -1500f, 0.2f).SetEase(Ease.OutQuad).Play();
//songItems.ForEach(item => item.DOScale(1,0.2f).SetEase(Ease.OutQuad).Play());
// 【核心修正】在这里根据速度决定下一步做什么
if (Mathf.Abs(velocity.y) > flickThreshold)
{
// 速度足够大,是“甩动”,让 LateUpdate 中的惯性逻辑接管
// 我们什么都不用做LateUpdate会自动处理
}
else
{
// 速度很小,是“慢速拖拽”,立即开始吸附
velocity = Vector2.zero; // 清除残余速度
StartCoroutine(SnapToClosest());
}
}
private void ClampContentPosition()
{
// 【核心修正 #2】现在我们限制的是目标位置让Lerp去处理实际位置
targetPosition.y = Mathf.Clamp(targetPosition.y, bottomBound, topBound);
}
}
public partial class SongListControllerUI
{
public void ClearSongTabs()
{
foreach (RectTransform item in songItems)
{
Destroy(item.gameObject);
}
songItems.Clear();
selectedTab = null;
closestTab = null;
targetPosition = content.anchoredPosition;
isDuringSnap = false;
}
public void GenerateSongTabs()
{
ChapterSelectionUnit chapterUnit = ChapterSelectionManager.instance.currentChapter;
foreach (SongItemData song in chapterUnit.songs)
{
SongSelectionTab tab = Instantiate(songItemPrefab, content).GetComponent<SongSelectionTab>();
songItems.Add(tab.GetComponent<RectTransform>());
tab.SetUpTab(song);
}
}
}
public partial class SongListControllerUI
{
public void GoToFormerTab()
{
if (closestTab != null)
{
int currentIndex = songItems.IndexOf(closestTab);
if (currentIndex > 0)
{
SongSelectionTab formerTab = songItems[currentIndex - 1].GetComponent<SongSelectionTab>();
StartCoroutine(SnapToTab(formerTab));
}
}
}
public void GoToNextTab()
{
if (closestTab != null)
{
int currentIndex = songItems.IndexOf(closestTab);
if (currentIndex < songItems.Count - 1)
{
SongSelectionTab nextTab = songItems[currentIndex + 1].GetComponent<SongSelectionTab>();
StartCoroutine(SnapToTab(nextTab));
}
}
}
}
public partial class SongListControllerUI
{
private IEnumerator SnapCoroutine;
// SnapToClosest 和 SnapToItem 这两个协程也需要微调,以确保它们能正确地与新的平滑系统协作
private IEnumerator SnapToClosest()
{
// ... (寻找最近项的逻辑不变) ...
yield return new WaitForEndOfFrame();
float minDistance = float.MaxValue;
RectTransform closestItem = null;
foreach (RectTransform item in songItems)
{
float distance = Mathf.Abs(item.position.y - centerPoint.position.y);
if (distance < minDistance)
{
minDistance = distance;
closestItem = item;
}
}
if (closestItem != null)
{
yield return SnapToItem(closestItem, false);
}
}
public IEnumerator SnapToTab(SongSelectionTab tab)
{
selectedTab?.SetSelection(false);
selectedTab = null; // 清除当前选中的Tab
if (isDuringSnap && SnapCoroutine != null)
{
StopCoroutine(SnapCoroutine);
}
SnapCoroutine = SnapToItem(tab.GetComponent<RectTransform>(), false);
yield return StartCoroutine(SnapCoroutine);
}
private IEnumerator SnapToItem(RectTransform targetItem, bool immediate)
{
if (!immediate)
{
yield return new WaitForEndOfFrame();
}
Debug.Log("开始对齐到: " + targetItem.name);
selectedTab = targetItem.GetComponent<SongSelectionTab>();
selectedTab.SetSelection(true);
SongItemData connectedSong = selectedTab.connectedSong;
MenuManager.instance.songSelectionUIPage.selectedSong = connectedSong;
MenuManager.instance.songSelectionUIPage.selectedSave = GameSaveManager.instance.SongSaveModule.GetSongStatusSave(connectedSong.songName);
Vector3 closestItemLocalPos = viewport.InverseTransformPoint(targetItem.position);
Vector3 centerPointLocalPos = viewport.InverseTransformPoint(centerPoint.position);
float localOffsetY = centerPointLocalPos.y - closestItemLocalPos.y;
Vector2 finalTargetPosition = content.anchoredPosition + new Vector2(0, localOffsetY);
finalTargetPosition.y = Mathf.Clamp(finalTargetPosition.y, bottomBound, topBound);
if (immediate)
{
// 立即模式直接设置Content和Target
targetPosition = finalTargetPosition;
content.anchoredPosition = finalTargetPosition;
}
else
{
isDuringSnap = true;
// 动画模式只更新Target让LateUpdate中的Lerp来完成动画
targetPosition = finalTargetPosition;
// 我们也可以在这里保留一个独立的Lerp循环以使用不同的snapSpeed
velocity = Vector2.zero;
while (Mathf.Abs(content.anchoredPosition.y - targetPosition.y) > 1f)
{
content.anchoredPosition = Vector2.Lerp(content.anchoredPosition, targetPosition, Time.deltaTime * snapSpeed);
yield return null;
}
content.anchoredPosition = targetPosition;
}
Debug.Log($"已对齐到: {targetItem.GetComponent<SongSelectionTab>().songNameText.text}");
SongSelectionManager.instance.SetPreview(connectedSong, selectedTab.isLocked);
MenuManager.instance.songSelectionUIPage.difficultySelectionContainer.SetUp(connectedSong.difficultyDataList);
MenuManager.instance.songSelectionUIPage.songInfoUI.SetIllustration(connectedSong.illustration, connectedSong.illustratorName);
isDuringSnap = false;
}
}
}