@@ -10,34 +10,41 @@ using SLSUtilities.WwiseAssistance;
|
||||
|
||||
namespace Ichni.Menu
|
||||
{
|
||||
// 一个完全自定义的列表控制器,实现了拖拽、惯性、边界和吸附
|
||||
// 一个完全自定义的列表控制器,实现了拖拽、惯性、边界和吸附
|
||||
public partial class SongListControllerUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
|
||||
{
|
||||
[Title("核心组件")]
|
||||
[Title("核心组件")]
|
||||
[SerializeField] public RectTransform content;
|
||||
[SerializeField] public RectTransform viewport;
|
||||
|
||||
[Title("预制体")]
|
||||
[SerializeField]
|
||||
[Title("预制体")]
|
||||
[SerializeField]
|
||||
private GameObject songItemPrefab;
|
||||
|
||||
[Title("对齐与动画")]
|
||||
[Title("对齐与动画")]
|
||||
[SerializeField] public RectTransform centerPoint;
|
||||
[SerializeField] private float snapSpeed = 10f;
|
||||
[SerializeField] private float decelerationRate = 0.15f;
|
||||
|
||||
[Title("平滑度优化")]
|
||||
[SerializeField] [Range(1f, 20f)]
|
||||
[Title("平滑度优化")]
|
||||
[SerializeField]
|
||||
[Range(1f, 20f)]
|
||||
private float dragSmoothing = 16f; // 拖拽平滑度的阻尼值,可在Inspector中调节
|
||||
[SerializeField][Range(1f, 20f)]
|
||||
[SerializeField]
|
||||
[Range(1f, 20f)]
|
||||
private float releaseSmoothing = 4f; // 松手后平滑度的阻尼值,可在Inspector中调节
|
||||
|
||||
|
||||
[Title("甩动判定")]
|
||||
[Tooltip("当松手时的速度大于此值,才会被判定为一次“甩动”并产生惯性")]
|
||||
[SerializeField] private float flickThreshold = 50f;
|
||||
|
||||
[Title("吸附判定")]
|
||||
[Tooltip("当甩动惯性低于这玩意的时候就吸附")]
|
||||
[SerializeField] private float SnapThreshold = 10f;
|
||||
[Tooltip("吸附选项时按当前速度预判的时间,值越大越容易顺着滑动方向吸附到下一首")]
|
||||
[SerializeField] [Min(0f)] private float snapVelocityProjectionTime = 0.12f;
|
||||
public SongSelectionTab selectedTab;
|
||||
|
||||
|
||||
// 内部变量
|
||||
public List<RectTransform> songItems = new List<RectTransform>();
|
||||
private Vector2 velocity;
|
||||
@@ -47,6 +54,7 @@ namespace Ichni.Menu
|
||||
private Vector2 targetPosition; // 【新增】内容的目标位置
|
||||
private float targetX = 1500f;
|
||||
public bool isDuringSnap = false;
|
||||
private bool isSnapRequested = false;
|
||||
public RectTransform closestTab;
|
||||
|
||||
// 仅供 Story SongBlock 在“下一次初始化列表”时指定初始歌曲;不写入章节缓存,
|
||||
@@ -82,7 +90,7 @@ namespace Ichni.Menu
|
||||
_initialSongSelectionOverride = null;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
topBound = (songItems.Count * 144f + (songItems.Count - 1) * 60f) - 72f; //topBound中,144为tab高度,60为tab间距,72为tab高度的一半
|
||||
bottomBound = 72f; //bottomBound中,72为tab高度的一半
|
||||
@@ -146,14 +154,14 @@ namespace Ichni.Menu
|
||||
|
||||
_initialSongSelectionOverride = null;
|
||||
}
|
||||
|
||||
|
||||
StartCoroutine(SnapToItem(songItems[songIndex], true));
|
||||
closestTab = songItems[songIndex];
|
||||
|
||||
|
||||
targetPosition = content.anchoredPosition;
|
||||
targetX = -1500f;
|
||||
}
|
||||
|
||||
|
||||
private void Update()
|
||||
{
|
||||
RectTransform closestItem = closestTab;
|
||||
@@ -167,7 +175,7 @@ namespace Ichni.Menu
|
||||
closestItem = item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (closestItem != null && closestTab != closestItem)
|
||||
{
|
||||
closestTab = closestItem;
|
||||
@@ -181,21 +189,20 @@ namespace Ichni.Menu
|
||||
// 如果不在拖拽,则处理惯性
|
||||
// 注意:我们只在 isDragging 为 false 时处理惯性,
|
||||
// OnEndDrag中已经对速度进行了判断,所以这里的逻辑依然适用
|
||||
if (!isDragging && Mathf.Abs(velocity.y) > 0.1f)
|
||||
if (!isDragging && !isDuringSnap && Mathf.Abs(velocity.y) > 0.1f)
|
||||
{
|
||||
// 将速度施加于目标位置
|
||||
targetPosition.y += velocity.y * Time.deltaTime;
|
||||
// 速度衰减
|
||||
velocity *= (1 - decelerationRate);
|
||||
|
||||
// 当速度足够小时,停止惯性并触发吸附
|
||||
if (Mathf.Abs(velocity.y) <= 0.1f)
|
||||
// 惯性速度回落到吸附范围后,就请求吸附到最近歌曲。
|
||||
if (!isSnapRequested && Mathf.Abs(velocity.y) <= SnapThreshold)
|
||||
{
|
||||
velocity = Vector2.zero;
|
||||
StartCoroutine(SnapToClosest());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 无论何时,都将Content的实际位置平滑地Lerp到目标位置
|
||||
float damping = isDragging ? dragSmoothing : releaseSmoothing;
|
||||
Vector2 finalPosition = Vector2.Lerp(content.anchoredPosition, targetPosition, Time.deltaTime * damping);
|
||||
@@ -204,15 +211,17 @@ namespace Ichni.Menu
|
||||
// 每次更新后都施加边界限制
|
||||
ClampContentPosition();
|
||||
}
|
||||
|
||||
|
||||
public void OnBeginDrag(PointerEventData eventData)
|
||||
{
|
||||
isDragging = true;
|
||||
velocity = Vector2.zero;
|
||||
StopAllCoroutines();
|
||||
isSnapRequested = false;
|
||||
isDuringSnap = false;
|
||||
// 开始拖拽时,将目标位置与当前位置同步
|
||||
targetPosition = content.anchoredPosition;
|
||||
|
||||
|
||||
selectedTab?.SetSelection(false);
|
||||
selectedTab = null; // 清除当前选中的Tab
|
||||
|
||||
@@ -228,7 +237,7 @@ namespace Ichni.Menu
|
||||
// 【核心修正 #1】计算速度时,只使用Y轴分量
|
||||
velocity = new Vector2(0, eventData.delta.y / Time.deltaTime);
|
||||
}
|
||||
|
||||
|
||||
public void OnEndDrag(PointerEventData eventData)
|
||||
{
|
||||
isDragging = false;
|
||||
@@ -244,7 +253,6 @@ namespace Ichni.Menu
|
||||
else
|
||||
{
|
||||
// 速度很小,是“慢速拖拽”,立即开始吸附
|
||||
velocity = Vector2.zero; // 清除残余速度
|
||||
StartCoroutine(SnapToClosest());
|
||||
}
|
||||
}
|
||||
@@ -255,7 +263,7 @@ namespace Ichni.Menu
|
||||
targetPosition.y = Mathf.Clamp(targetPosition.y, bottomBound, topBound);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public partial class SongListControllerUI
|
||||
{
|
||||
public void ClearSongTabs()
|
||||
@@ -276,8 +284,9 @@ namespace Ichni.Menu
|
||||
SnapCoroutine = null;
|
||||
targetPosition = content.anchoredPosition;
|
||||
isDuringSnap = false;
|
||||
isSnapRequested = false;
|
||||
}
|
||||
|
||||
|
||||
public void GenerateSongTabs()
|
||||
{
|
||||
ChapterSelectionUnit chapterUnit = ChapterSelectionManager.instance.currentChapter;
|
||||
@@ -315,7 +324,7 @@ namespace Ichni.Menu
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void GoToNextTab()
|
||||
{
|
||||
if (closestTab != null)
|
||||
@@ -332,18 +341,79 @@ namespace Ichni.Menu
|
||||
|
||||
public partial class SongListControllerUI
|
||||
{
|
||||
/// <summary>
|
||||
/// 列表滚动进度,0 = 第一首,1 = 最后一首。
|
||||
/// 基于 content.anchoredPosition.y 在 bottomBound ~ topBound 区间归一化。
|
||||
/// </summary>
|
||||
public float ScrollProgress =>
|
||||
songItems.Count <= 1
|
||||
? 0f
|
||||
: Mathf.InverseLerp(bottomBound, topBound, content.anchoredPosition.y);
|
||||
|
||||
/// <summary>
|
||||
/// 当前列表的连续歌曲索引,0 = 第一首,1 = 第二首。
|
||||
/// 拖动或吸附动画中会返回小数,供外部 UI 跟随歌曲滑动。
|
||||
/// </summary>
|
||||
public float ContinuousSongIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
if (songItems.Count <= 1)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return Mathf.Clamp(ScrollProgress * (songItems.Count - 1), 0f, songItems.Count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前列表中的歌曲总数。
|
||||
/// </summary>
|
||||
public int SongCount => songItems.Count;
|
||||
|
||||
private IEnumerator SnapCoroutine;
|
||||
|
||||
|
||||
// SnapToClosest 和 SnapToItem 这两个协程也需要微调,以确保它们能正确地与新的平滑系统协作
|
||||
private IEnumerator SnapToClosest()
|
||||
{
|
||||
if (isSnapRequested)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
isSnapRequested = true;
|
||||
|
||||
// ... (寻找最近项的逻辑不变) ...
|
||||
yield return new WaitForEndOfFrame();
|
||||
|
||||
float projectedContentY = Mathf.Clamp(
|
||||
targetPosition.y + velocity.y * snapVelocityProjectionTime,
|
||||
bottomBound,
|
||||
topBound);
|
||||
RectTransform closestItem = FindClosestItemToContentY(projectedContentY);
|
||||
|
||||
velocity = Vector2.zero;
|
||||
|
||||
if (closestItem != null)
|
||||
{
|
||||
yield return SnapToItem(closestItem, false);
|
||||
}
|
||||
|
||||
isSnapRequested = false;
|
||||
}
|
||||
|
||||
private RectTransform FindClosestItemToContentY(float contentY)
|
||||
{
|
||||
float minDistance = float.MaxValue;
|
||||
RectTransform closestItem = null;
|
||||
foreach (RectTransform item in songItems)
|
||||
{
|
||||
float distance = Mathf.Abs(item.position.y - centerPoint.position.y);
|
||||
Vector3 itemLocalPos = viewport.InverseTransformPoint(item.position);
|
||||
Vector3 centerLocalPos = viewport.InverseTransformPoint(centerPoint.position);
|
||||
float localOffsetY = centerLocalPos.y - itemLocalPos.y;
|
||||
float itemTargetY = Mathf.Clamp(content.anchoredPosition.y + localOffsetY, bottomBound, topBound);
|
||||
float distance = Mathf.Abs(itemTargetY - contentY);
|
||||
if (distance < minDistance)
|
||||
{
|
||||
minDistance = distance;
|
||||
@@ -351,24 +421,22 @@ namespace Ichni.Menu
|
||||
}
|
||||
}
|
||||
|
||||
if (closestItem != null)
|
||||
{
|
||||
yield return SnapToItem(closestItem, false);
|
||||
}
|
||||
return closestItem;
|
||||
}
|
||||
|
||||
|
||||
public IEnumerator SnapToTab(SongSelectionTab tab)
|
||||
{
|
||||
selectedTab?.SetSelection(false);
|
||||
selectedTab = null; // 清除当前选中的Tab
|
||||
isSnapRequested = false;
|
||||
|
||||
if (isDuringSnap && SnapCoroutine != null)
|
||||
{
|
||||
StopCoroutine(SnapCoroutine);
|
||||
}
|
||||
|
||||
|
||||
SnapCoroutine = SnapToItem(tab.GetComponent<RectTransform>(), false);
|
||||
|
||||
|
||||
yield return StartCoroutine(SnapCoroutine);
|
||||
}
|
||||
|
||||
@@ -378,20 +446,20 @@ namespace Ichni.Menu
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -419,11 +487,11 @@ namespace Ichni.Menu
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user