2025-07-26 04:20:25 -04:00
|
|
|
|
using System;
|
2025-07-08 14:28:40 -04:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.UI;
|
|
|
|
|
|
using UnityEngine.EventSystems;
|
|
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using DG.Tweening;
|
|
|
|
|
|
using Sirenix.OdinInspector;
|
|
|
|
|
|
|
2025-08-11 14:04:06 -04:00
|
|
|
|
namespace Ichni.Menu
|
2025-07-08 14:28:40 -04:00
|
|
|
|
{
|
|
|
|
|
|
// 一个完全自定义的列表控制器,实现了拖拽、惯性、边界和吸附
|
2025-08-11 14:04:06 -04:00
|
|
|
|
public partial class SongListControllerUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
|
2025-07-08 14:28:40 -04:00
|
|
|
|
{
|
|
|
|
|
|
[Title("核心组件")]
|
2025-07-21 05:42:20 -04:00
|
|
|
|
[SerializeField] public RectTransform content;
|
|
|
|
|
|
[SerializeField] public RectTransform viewport;
|
2025-07-08 14:28:40 -04:00
|
|
|
|
|
|
|
|
|
|
[Title("预制体")]
|
|
|
|
|
|
[SerializeField]
|
|
|
|
|
|
private GameObject songItemPrefab;
|
|
|
|
|
|
|
|
|
|
|
|
[Title("对齐与动画")]
|
2025-07-21 05:42:20 -04:00
|
|
|
|
[SerializeField] public RectTransform centerPoint;
|
2025-08-22 14:54:40 -04:00
|
|
|
|
[SerializeField] private float snapSpeed = 10f;
|
|
|
|
|
|
[SerializeField] private float decelerationRate = 0.15f;
|
2025-07-08 14:28:40 -04:00
|
|
|
|
|
|
|
|
|
|
[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;
|
|
|
|
|
|
|
2025-07-21 05:42:20 -04:00
|
|
|
|
public SongSelectionTab selectedTab;
|
2025-07-08 14:28:40 -04:00
|
|
|
|
|
|
|
|
|
|
// 内部变量
|
2025-08-11 14:04:06 -04:00
|
|
|
|
public List<RectTransform> songItems = new List<RectTransform>();
|
2025-07-08 14:28:40 -04:00
|
|
|
|
private Vector2 velocity;
|
|
|
|
|
|
private bool isDragging = false;
|
|
|
|
|
|
public float topBound;
|
|
|
|
|
|
public float bottomBound = 0f;
|
|
|
|
|
|
private Vector2 targetPosition; // 【新增】内容的目标位置
|
|
|
|
|
|
private float targetX = 1500f;
|
2025-07-26 04:20:25 -04:00
|
|
|
|
public bool isDuringSnap = false;
|
|
|
|
|
|
public RectTransform closestTab;
|
2025-07-08 14:28:40 -04:00
|
|
|
|
|
2025-08-11 14:04:06 -04:00
|
|
|
|
public void InitializeList()
|
2025-07-08 14:28:40 -04:00
|
|
|
|
{
|
2025-08-11 14:04:06 -04:00
|
|
|
|
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;
|
2025-07-08 14:28:40 -04:00
|
|
|
|
targetX = -1500f;
|
|
|
|
|
|
}
|
2025-08-11 14:04:06 -04:00
|
|
|
|
|
2025-07-26 04:20:25 -04:00
|
|
|
|
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;
|
2025-08-11 14:04:06 -04:00
|
|
|
|
MenuAudioManager.instance.audioContainer.PlaySoundFX("SwitchTab");
|
2025-07-26 04:20:25 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-08 14:28:40 -04:00
|
|
|
|
// 使用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
|
|
|
|
|
|
|
2025-07-21 05:42:20 -04:00
|
|
|
|
//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());
|
2025-07-08 14:28:40 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
2025-07-21 05:42:20 -04:00
|
|
|
|
//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());
|
2025-07-08 14:28:40 -04:00
|
|
|
|
|
|
|
|
|
|
// 【核心修正】在这里根据速度决定下一步做什么
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
2025-08-11 14:04:06 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-07-08 14:28:40 -04:00
|
|
|
|
|
2025-08-11 14:04:06 -04:00
|
|
|
|
public partial class SongListControllerUI
|
|
|
|
|
|
{
|
|
|
|
|
|
private IEnumerator SnapCoroutine;
|
|
|
|
|
|
|
2025-07-08 14:28:40 -04:00
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-07-26 04:20:25 -04:00
|
|
|
|
|
2025-07-21 05:42:20 -04:00
|
|
|
|
public IEnumerator SnapToTab(SongSelectionTab tab)
|
2025-07-10 08:42:30 -04:00
|
|
|
|
{
|
|
|
|
|
|
selectedTab?.SetSelection(false);
|
|
|
|
|
|
selectedTab = null; // 清除当前选中的Tab
|
2025-07-26 04:20:25 -04:00
|
|
|
|
|
2025-08-22 14:54:40 -04:00
|
|
|
|
if (isDuringSnap && SnapCoroutine != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
StopCoroutine(SnapCoroutine);
|
|
|
|
|
|
}
|
2025-07-26 04:20:25 -04:00
|
|
|
|
|
|
|
|
|
|
SnapCoroutine = SnapToItem(tab.GetComponent<RectTransform>(), false);
|
2025-07-10 08:42:30 -04:00
|
|
|
|
|
2025-07-26 04:20:25 -04:00
|
|
|
|
yield return StartCoroutine(SnapCoroutine);
|
2025-07-10 08:42:30 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-08 14:28:40 -04:00
|
|
|
|
private IEnumerator SnapToItem(RectTransform targetItem, bool immediate)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!immediate)
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return new WaitForEndOfFrame();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Debug.Log("开始对齐到: " + targetItem.name);
|
2025-07-21 05:42:20 -04:00
|
|
|
|
|
|
|
|
|
|
selectedTab = targetItem.GetComponent<SongSelectionTab>();
|
|
|
|
|
|
selectedTab.SetSelection(true);
|
|
|
|
|
|
|
|
|
|
|
|
SongItemData connectedSong = selectedTab.connectedSong;
|
|
|
|
|
|
MenuManager.instance.songSelectionUIPage.selectedSong = connectedSong;
|
2025-07-26 04:20:25 -04:00
|
|
|
|
MenuManager.instance.songSelectionUIPage.selectedSave = GameSaveManager.instance.SongSaveModule.GetSongStatusSave(connectedSong.songName);
|
2025-07-21 05:42:20 -04:00
|
|
|
|
|
2025-07-08 14:28:40 -04:00
|
|
|
|
Vector3 closestItemLocalPos = viewport.InverseTransformPoint(targetItem.position);
|
|
|
|
|
|
Vector3 centerPointLocalPos = viewport.InverseTransformPoint(centerPoint.position);
|
|
|
|
|
|
float localOffsetY = centerPointLocalPos.y - closestItemLocalPos.y;
|
2025-08-22 14:54:40 -04:00
|
|
|
|
|
2025-07-08 14:28:40 -04:00
|
|
|
|
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
|
|
|
|
|
|
{
|
2025-07-26 04:20:25 -04:00
|
|
|
|
isDuringSnap = true;
|
2025-07-08 14:28:40 -04:00
|
|
|
|
// 动画模式:只更新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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-26 04:20:25 -04:00
|
|
|
|
Debug.Log($"已对齐到: {targetItem.GetComponent<SongSelectionTab>().songNameText.text}");
|
|
|
|
|
|
|
|
|
|
|
|
SongSelectionManager.instance.SetPreview(connectedSong, selectedTab.isLocked);
|
2025-07-21 05:42:20 -04:00
|
|
|
|
MenuManager.instance.songSelectionUIPage.difficultySelectionContainer.SetUp(connectedSong.difficultyDataList);
|
|
|
|
|
|
MenuManager.instance.songSelectionUIPage.songInfoUI.SetIllustration(connectedSong.illustration, connectedSong.illustratorName);
|
2025-08-11 14:04:06 -04:00
|
|
|
|
|
2025-07-26 04:20:25 -04:00
|
|
|
|
isDuringSnap = false;
|
2025-07-08 14:28:40 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|