优化一些动画效果

Signed-off-by: TRADER_FOER <lhf190@outlook.com>
This commit is contained in:
2026-07-26 12:12:45 +08:00
parent a34461d31f
commit a00046128d
84 changed files with 8241 additions and 1681 deletions

View File

@@ -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;
}
}