feat(fix-system): 打孔带收藏夹改为固定3槽位布局

This commit is contained in:
2026-04-15 22:22:30 +08:00
parent 2f1ee81f6c
commit 7290bbab1e
4 changed files with 122 additions and 97 deletions
@@ -1,11 +1,11 @@
using System.Collections.Generic;
using AibisDream.Kit;
using System.Collections.Generic;
using DG.Tweening;
using UnityEngine;
/// <summary>
/// 打孔带收藏夹视图。侧栏(<see cref="favoritesUI"/> 上的 RectTransform)做滑入滑出;
/// 可选的 <see cref="punchTapePanel"/> 仅显隐,与侧栏同开同关。数据来自 <see cref="PunchTapeManager"/>。
/// 固定 3 个槽位,打孔带按顺序放入槽位。
/// </summary>
namespace AibisDream.FixSystem
{
@@ -14,8 +14,8 @@ namespace AibisDream.FixSystem
private const float PanelSlideDuration = 0.5f;
[Header("收藏夹 UI")]
[Tooltip("列表项父节点(Vertical Layout 等)")]
[SerializeField] private Transform punchTapeContent;
[Tooltip("3 个固定槽位,按顺序放置打孔带")]
[SerializeField] private RectTransform[] punchTapeSlots = new RectTransform[3];
[SerializeField] private GameObject punchTapeItemPrefab;
@@ -26,19 +26,28 @@ namespace AibisDream.FixSystem
[SerializeField] private Vector2 panelHiddenAnchoredPos;
[SerializeField] private Vector2 panelVisibleAnchoredPos;
private SimpleObjectPool<GameObject> _rowPool;
private readonly Dictionary<string, GameObject> _rowByTapeKey = new();
private readonly Dictionary<string, int> _slotIndexByKey = new();
private PunchTapeManager PunchTapeManager => PunchTapeManager.Instance;
private void Start()
{
if (punchTapeContent == null)
if (punchTapeSlots == null || punchTapeSlots.Length != 3)
{
Debug.LogError("[PunchTapeFavoritesPanel] 未指定 punchTapeContent。", this);
Debug.LogError("[PunchTapeFavoritesPanel] 必须指定 3 个 punchTapeSlots。", this);
enabled = false;
return;
}
for (var i = 0; i < punchTapeSlots.Length; i++)
{
if (punchTapeSlots[i] == null)
{
Debug.LogError($"[PunchTapeFavoritesPanel] punchTapeSlots[{i}] 未指定。", this);
enabled = false;
return;
}
}
if (punchTapeItemPrefab == null)
{
Debug.LogError("[PunchTapeFavoritesPanel] 未指定 punchTapeItemPrefab。", this);
@@ -58,53 +67,31 @@ namespace AibisDream.FixSystem
punchTapePanel?.DOKill();
DisposeRowPool();
ClearAllRows();
_rowByTapeKey.Clear();
_slotIndexByKey.Clear();
}
private void HandleCollectionChanged() => SyncListToData();
private void DisposeRowPool()
private void ClearAllRows()
{
if (_rowPool == null)
return;
_rowPool.Clear(Destroy);
_rowPool = null;
}
private void EnsureRowPool()
{
if (_rowPool != null)
return;
_rowPool = new SimpleObjectPool<GameObject>(
() =>
{
var go = Instantiate(punchTapeItemPrefab, punchTapeContent);
go.SetActive(false);
return go;
},
go =>
{
go.SetActive(false);
go.transform.SetParent(punchTapeContent, false);
});
foreach (var kvp in _rowByTapeKey)
{
if (kvp.Value != null)
Destroy(kvp.Value);
}
_rowByTapeKey.Clear();
_slotIndexByKey.Clear();
}
private void SyncListToData()
{
EnsureRowPool();
var favorites = PunchTapeManager.GetFavorites();
RemoveRowsNotInFavorites(favorites);
EnsureRowsForFavorites(favorites);
}
private void RemoveRowsNotInFavorites(IReadOnlyList<PunchTape> favorites)
{
// 回收不在前 3 个的项
var keepKeys = new HashSet<string>();
for (var i = 0; i < favorites.Count; i++)
for (var i = 0; i < favorites.Count && i < punchTapeSlots.Length; i++)
keepKeys.Add(favorites[i].GetRowKey());
var staleKeys = new List<string>();
@@ -113,35 +100,85 @@ namespace AibisDream.FixSystem
if (!keepKeys.Contains(key))
staleKeys.Add(key);
}
foreach (var key in staleKeys)
{
if (_rowByTapeKey.TryGetValue(key, out var go) && go != null)
_rowPool.Recycle(go);
Destroy(go);
_rowByTapeKey.Remove(key);
}
// 确保前 3 个槽位有对应项
for (var i = 0; i < punchTapeSlots.Length; i++)
{
if (i < favorites.Count)
{
var tape = favorites[i];
var key = tape.GetRowKey();
_slotIndexByKey[key] = i;
// 如果该 key 已经在别的槽位,先移过来
if (_rowByTapeKey.TryGetValue(key, out var existingGo) && existingGo != null)
{
existingGo.transform.SetParent(punchTapeSlots[i], false);
ResetRowRectTransform(existingGo);
existingGo.transform.SetSiblingIndex(i);
}
else
{
var rowGo = Instantiate(punchTapeItemPrefab, punchTapeSlots[i], false);
ResetRowRectTransform(rowGo);
_rowByTapeKey[key] = rowGo;
BindRow(rowGo, tape);
rowGo.SetActive(true);
}
}
else
{
// 清空该槽位上不再需要的项
ClearSlot(i);
}
}
}
private void EnsureRowsForFavorites(IReadOnlyList<PunchTape> favorites)
private void ClearSlot(int slotIndex)
{
for (var i = 0; i < favorites.Count; i++)
{
var tape = favorites[i];
var key = tape.GetRowKey();
var slot = punchTapeSlots[slotIndex];
if (slot == null || slot.childCount == 0)
return;
if (!_rowByTapeKey.TryGetValue(key, out var rowGo) || rowGo == null)
// 槽位下应该只有一个打孔带子对象
for (var i = slot.childCount - 1; i >= 0; i--)
{
var child = slot.GetChild(i).gameObject;
if (child == null) continue;
var item = child.GetComponent<PunchTapeItem>();
if (item != null && item.GetPunchTapeData() != null)
{
rowGo = _rowPool.Allocate();
rowGo.transform.SetParent(punchTapeContent, false);
_rowByTapeKey[key] = rowGo;
var key = item.GetPunchTapeData().GetRowKey();
_rowByTapeKey.Remove(key);
_slotIndexByKey.Remove(key);
}
BindRow(rowGo, tape);
rowGo.SetActive(true);
rowGo.transform.SetSiblingIndex(i);
Destroy(child);
}
}
private static void ResetRowRectTransform(GameObject rowGo)
{
var rt = rowGo.GetComponent<RectTransform>();
if (rt == null) return;
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
rt.pivot = new Vector2(0.5f, 0.5f);
rt.anchoredPosition = Vector2.zero;
rt.localRotation = Quaternion.identity;
rt.localScale = Vector3.one;
}
private static void BindRow(GameObject rowGo, PunchTape tape)
{
var interaction = rowGo.GetComponent<PunchTapeInteraction>();
@@ -156,10 +193,30 @@ namespace AibisDream.FixSystem
item.Setup(tape);
}
/// <summary>
/// 将拖拽回弹的打孔带重新放置到它所属的槽位中。
/// </summary>
public void ReturnToSlot(PunchTapeInteraction interaction)
{
if (interaction == null) return;
var item = interaction.GetComponent<PunchTapeItem>();
if (item == null) return;
var tape = item.GetPunchTapeData();
if (tape == null) return;
var key = tape.GetRowKey();
if (_slotIndexByKey.TryGetValue(key, out var slotIndex) && slotIndex >= 0 && slotIndex < punchTapeSlots.Length)
{
interaction.transform.SetParent(punchTapeSlots[slotIndex], false);
ResetRowRectTransform(interaction.gameObject);
}
}
private void ApplyClosedLayout()
{
punchTapePanel.anchoredPosition = panelHiddenAnchoredPos;
punchTapePanel.gameObject.SetActive(false);
}
@@ -10,18 +10,11 @@ namespace AibisDream.FixSystem
public class PunchTapeInteraction : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler,
IBeginDragHandler, IDragHandler, IEndDragHandler
{
private Transform originalParent;
public Transform OriginalParent => originalParent;
private Canvas canvas;
[HideInInspector]
public bool isFavorited = false;
private RectTransform rectTransform;
private Vector2 originalAnchorMin;
private Vector2 originalAnchorMax;
private Vector2 originalOffsetMin;
private Vector2 originalOffsetMax;
private Vector2 originalPivot;
private CursorMode cursorMode = CursorMode.Auto;
@@ -38,7 +31,6 @@ namespace AibisDream.FixSystem
private void OnEnable()
{
canvas = GetComponentInParent<Canvas>();
StoreOriginalTransform();
}
private void OnDisable()
@@ -50,16 +42,6 @@ namespace AibisDream.FixSystem
}
}
private void StoreOriginalTransform()
{
originalParent = transform.parent;
originalAnchorMin = rectTransform.anchorMin;
originalAnchorMax = rectTransform.anchorMax;
originalOffsetMin = rectTransform.offsetMin;
originalOffsetMax = rectTransform.offsetMax;
originalPivot = rectTransform.pivot;
}
public void OnPointerEnter(PointerEventData eventData)
{
}
@@ -78,6 +60,7 @@ namespace AibisDream.FixSystem
if (!isFavorited || isLocked) return;
transform.SetParent(canvas.transform, true);
transform.localScale = Vector3.one;
transform.SetAsLastSibling();
var cg = GetComponent<CanvasGroup>();
if (cg != null) cg.blocksRaycasts = false;
@@ -120,21 +103,15 @@ namespace AibisDream.FixSystem
}
if (accepted)
{
Destroy(gameObject);
}
else
ReturnToOriginalPosition();
}
public void ReturnToOriginalPosition()
{
isLocked = false;
transform.rotation = Quaternion.Euler(0, 0, 0);
transform.SetParent(originalParent, false);
rectTransform.anchorMin = originalAnchorMin;
rectTransform.anchorMax = originalAnchorMax;
rectTransform.offsetMin = originalOffsetMin;
rectTransform.offsetMax = originalOffsetMax;
rectTransform.pivot = originalPivot;
{
var manager = PunchTapeManager.Instance;
if (manager != null && manager.FavoritesPanel != null)
manager.FavoritesPanel.ReturnToSlot(this);
}
}
public void SetIsFavorited(bool favorited)
@@ -1,5 +1,4 @@
using UnityEngine;
using TMPro;
using UnityEngine.UI;
using UnityEngine.Localization.Components;
using AibisDream.Framework;
@@ -12,17 +11,13 @@ namespace AibisDream.FixSystem
public class PunchTapeItem : MonoBehaviour
{
public Image clueImageUI;
public TMP_Text categoryText;
public LocalizeStringEvent nameText;
public GameObject stateText;
[HideInInspector]
public bool used = false;
private PunchTape punchTapeData;
public GameObject finished;
public void Setup(PunchTape punchTape)
{
punchTapeData = punchTape;
@@ -41,11 +36,6 @@ namespace AibisDream.FixSystem
if (clueImageUI != null && pt.used)
clueImageUI.color = Color.grey;
if (stateText != null)
stateText.SetActive(pt.used);
if (categoryText != null)
categoryText.text = "分类:" + pt.Category;
if (nameText != null)
{
nameText.StringReference.Arguments = new object[] { LocalizationKit.LocalizeParam(pt.ClueName) };
@@ -15,6 +15,7 @@ namespace AibisDream.FixSystem
public GameObject punchTapecluePrefab;
[SerializeField] private PunchTapeFavoritesPanel favoritesPanel;
public PunchTapeFavoritesPanel FavoritesPanel => favoritesPanel;
[SerializeField] private PunchTapePrinter punchTapePrinter;
public PunchTapePrinter PunchTapePrinter => punchTapePrinter;