Files
XericUIActionVessel/Runtime/FlipPage/XericFlipPageIndexVesselBase.cs
T
2025-09-19 20:08:27 +08:00

820 lines
25 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 System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.Serialization;
using UnityEngine.UI;
using XericLibrary.Runtime.MacroLibrary;
using XericUI.Core.Base;
using XericUI.Core.Vessel;
namespace XericUI.FlipPage
{
/// <summary>
/// 翻页管理器(逻辑层)
/// </summary>
/// <code>
/// 翻页逻辑层脚本,约束基本用法:
///
/// XericFlipPageIndexVesselBase 逻辑层管理器
/// |-PageInfo 页面数据结构
/// | |-ItemInfo 项目数据结构
///
/// 基本用法应该是,添加数据项目,这会自动分配到有效的页面中;
/// 如果页面项目已满,则新建一个页面继续填充项目。
/// 或者直接创建页面,将需要的数据项目直接写在页面数据中。
///
/// 使用FlipPage()翻页,这会立刻执行,并触发OnFlipPage函数,
/// currentPageIndex可以获取当前页数,Currentpage获取当前页面数据;
/// 在页面数据中pageNumber可以获取自己所在的页数,itemInfos获取页面中的所有数据项目。
/// </code>
[AddComponentMenu("Xeric UI Vessel/FlipPage/Basic FilpPage Database")]
public class XericFlipPageIndexVesselBase : XericUIBehaciour
{
#region 设定参数
/// <summary>
/// 候选页数量
/// </summary>
#if ODIN_INSPECTOR
[LabelText("候选页数量")]
[BoxGroup("设定", centerLabel: true)]
#endif
[Tooltip("最大同时呈现的页面选择器数量")]
[SerializeField]
protected float candidatesNumber = 1;
/// <summary>
/// 翻页可以跨越索引边接(循环索引)
/// </summary>
#if ODIN_INSPECTOR
[LabelText("翻页时允许跨越索引循环")]
[BoxGroup("设定")]
#endif
[SerializeField]
protected bool canPassIndexBoundary;
/// <summary>
/// 页面默认最大项目数量
/// </summary>
#if ODIN_INSPECTOR
[LabelText("单个页面内默认最多项目数量")]
[BoxGroup("设定")]
#endif
[SerializeField]
private int defaultItemCountPerPage = -1;
#endregion
#region 字段属性
/// <summary>
/// 当前选中的页面索引
/// </summary>
#if ODIN_INSPECTOR
[LabelText("当前选中页面索引")]
#endif
[SerializeField]
protected int currentPageIndex = 0;
#if UNITY_EDITOR && ODIN_INSPECTOR
/// <summary>
/// 显示调试列表
/// </summary>
private List<PageInfo> PageInfoLists => _pageInfos.ToList();
#endif
/// <summary>
/// 所有页面
/// </summary>
private readonly LinkedList<PageInfo> _pageInfos = new LinkedList<PageInfo>();
/// <summary>
/// 所有列表项目
/// </summary>
private readonly HashSet<ItemInfo> _itemInfos = HashSetPool<ItemInfo>.Get();
/// <summary>
/// 当前选中页面节点
/// </summary>
protected LinkedListNode<PageInfo> CurrentPageNode = null;
/// <summary>
/// 所有页面
/// </summary>
public LinkedList<PageInfo> PageInfos => _pageInfos;
/// <summary>
/// 当前选中页面内容
/// </summary>
public PageInfo Currentpage => CurrentPageNode?.Value;
/// <summary>
/// 分布于所有页面上的所有数据项目
/// </summary>
public IEnumerable<ItemInfo> AllItemInfos => _pageInfos.SelectMany(page => page.itemInfos);
/// <summary>
/// 页面默认最大项目数量
/// </summary>
protected int DefaultItemCountPerPage
=> defaultItemCountPerPage < 0 ? int.MaxValue : defaultItemCountPerPage;
private List<ItemInfo> _backupItemInfos;
public List<ItemInfo> BackItemInfos => _backupItemInfos;
/// <summary>
/// 分布所有页面上的所有项目数量
/// </summary>
public int TotalItemCount => AllItemInfos.Count();
/// <summary>
/// 所有页面数量
/// </summary>
public int TotalPageCount => PageInfos.Count();
#endregion
#region 生命周期
protected override void Start()
{
base.Start();
FlipPage(currentPageIndex);
}
protected override void OnDestroy()
{
base.OnDestroy();
HashSetPool<ItemInfo>.Release(_itemInfos);
}
#endregion
#region 翻页管理
/// <summary>
/// 当发生翻页时(这会在整个翻页动作流程的最后执行)
/// </summary>
protected virtual void OnFlipPage()
{
}
/// <summary>
/// 通过指令筛选页面
/// </summary>
/// <param name="pageNumber"></param>
/// <param name="forceRefresh"></param>
public virtual void FlipPage(string pageNumber, bool forceRefresh = false)
{
if (float.TryParse(pageNumber, out var index))
{
FlipPage((int)index - 1, forceRefresh);
}
}
/// <summary>
/// 翻到指定页
/// </summary>
/// <param name="pageIndex"></param>
/// <param name="quickSwitchPage">快速翻页,</param>
/// <param name="forceRefresh"></param>
public virtual void FlipPage(int pageIndex, bool forceRefresh = false)
{
// 检查翻页索引有效
if (!canPassIndexBoundary &&
(pageIndex < 0 || pageIndex >= _pageInfos.Count))
pageIndex = Mathf.Clamp(pageIndex, 0, _pageInfos.Count - 1);
else
pageIndex = MacroMath.PMod(pageIndex, _pageInfos.Count);
// 检查翻页数量有效
if (!forceRefresh && currentPageIndex == pageIndex)
return;
// 翻页计算
var pageIndexOffset = pageIndex - currentPageIndex;
var lastPageIndex = currentPageIndex;
// 如果节点不存在,通过索引寻找
if (CurrentPageNode == null)
CurrentPageNode = _pageInfos.GetOrderNode().LandedIndex(currentPageIndex);
// 如果索引偏移,逐个移动
else if (pageIndexOffset > 0)
{
// 先关闭页面元素
CurrentPageNode.Value.CallChildItemInvisible();
for (var i = 0; i < pageIndexOffset; i++)
{
var next = CurrentPageNode!.Next;
SwitchPage(next, _pageInfos.Last, i + 1 < pageIndexOffset);
}
}
else if (pageIndexOffset < 0)
{
// 先关闭页面元素
CurrentPageNode.Value.CallChildItemInvisible();
for (var i = 0; i > pageIndexOffset; i--)
{
var previa = CurrentPageNode!.Previous;
SwitchPage(previa, _pageInfos.First, i - 1 > pageIndexOffset);
}
}
// 没有翻动有效页,直接退出
else
return;
// 可能时当前不存在任何页面
if (CurrentPageNode == null)
return;
// 页面元素激活
CurrentPageNode.Value.CallChildItemVisible();
currentPageIndex = pageIndex;
OnFlipPage();
// 翻页
void SwitchPage(LinkedListNode<PageInfo> newPageNode, LinkedListNode<PageInfo> defualtPageNode, bool quickPage)
{
if (newPageNode != null)
{
Currentpage.OnPageInvisible(quickPage);
CurrentPageNode = newPageNode;
Currentpage.OnPageVisible(quickPage);
}
if (canPassIndexBoundary)
{
Currentpage.OnPageInvisible(quickPage);
CurrentPageNode = defualtPageNode;
Currentpage.OnPageVisible(quickPage);
}
}
}
/// <summary>
/// 翻到开头
/// </summary>
#if ODIN_INSPECTOR
[HorizontalGroup("翻页"), Button("翻到开头")]
#endif
public void FlipPageToStart()
=> FlipPage(0);
/// <summary>
/// 翻到上一页
/// </summary>
#if ODIN_INSPECTOR
[HorizontalGroup("翻页"), Button("翻到上一页")]
#endif
public void FlipPageToPrevious()
=> FlipPage(currentPageIndex - 1);
/// <summary>
/// 翻到下一页
/// </summary>
#if ODIN_INSPECTOR
[HorizontalGroup("翻页"), Button("翻到下一页")]
#endif
public void FlipPageToNext()
=> FlipPage(currentPageIndex + 1);
/// <summary>
/// 翻到结尾
/// </summary>
#if ODIN_INSPECTOR
[HorizontalGroup("翻页"), Button("翻到结尾")]
#endif
public void FlipPageToEnd()
=> FlipPage(_pageInfos.Count - 1);
/// <summary>
/// 添加页面在开头
/// </summary>
public virtual PageInfo AddPageFirst(PageInfo pageInfo)
{
if (pageInfo == null)
{
Debug.LogError($"不能添加一个空的页面内容到管理器中!", this);
return null;
}
// 添加页面下标
pageInfo.pageNumber = 0;
// 添加页面
_pageInfos.AddFirst(pageInfo);
// 更新后面页面的索引
RefreshAllPageNumber();
// 执行页面生命周期
pageInfo.OnPageCreated(this);
// 刷新页面
FlipPage(currentPageIndex, true);
return pageInfo;
}
/// <summary>
/// 添加页面在结尾
/// </summary>
public virtual PageInfo AddPageLast(PageInfo pageInfo)
{
if (pageInfo == null)
{
Debug.LogError($"不能添加一个空的页面内容到管理器中!", this);
return null;
}
// 添加页面下标
pageInfo.pageNumber = _pageInfos.Count;
// 添加页面
_pageInfos.AddLast(pageInfo);
// 执行页面生命周期
pageInfo.OnPageCreated(this);
// 刷新页面
FlipPage(currentPageIndex, true);
return pageInfo;
}
/// <summary>
/// 按对象引用删除页面
/// </summary>
/// <param name="pageInfo"></param>
public virtual void RemovePage(PageInfo pageInfo)
{
// 如果移除的是当前页面,执行关闭操作
if (CurrentPageNode.Value == pageInfo)
{
pageInfo.OnPageInvisible(true);
CurrentPageNode = null;
}
// 执行页面生命周期
pageInfo.OnPageDestroy(this);
// 页面索引移除
_pageInfos.Remove(pageInfo);
// 刷新页面
FlipPage(currentPageIndex, true);
var index = PageInfos.IndexOf(a => a == pageInfo);
RemovePage(index);
}
/// <summary>
/// 移除页面(页面上的数据内容也会一同移除)
/// </summary>
public virtual void RemovePage(int pageIndex)
{
if (0 > pageIndex || pageIndex > _pageInfos.Count)
{
Debug.LogError($"不能移除不在范围内的索引{pageIndex}!", this);
return;
}
// 获取页面节点
var page = _pageInfos.GetOrderNode().LandedIndex(pageIndex);
// 更新后面页面的索引
var tempPage = page;
var tempPageNumber = page.Value.pageNumber;
while (tempPage.Next != null)
{
tempPage = tempPage.Next;
tempPage.Value.pageNumber = tempPageNumber++;
}
// 如果移除的是当前页面,执行关闭操作
if (CurrentPageNode == page)
{
page.Value.OnPageInvisible(true);
CurrentPageNode = null;
}
// 执行页面生命周期
page.Value.OnPageDestroy(this);
// 页面索引移除
_pageInfos.Remove(page);
// 刷新页面
FlipPage(currentPageIndex, true);
}
/// <summary>
/// 仅移除页面,不会移除数据
/// </summary>
/// <param name="pageIndex"></param>
public void RemovePageWithoutItems(int pageIndex)
{
BackupItemsFormPages();
RemovePage(pageIndex);
RestoreItemsToPages();
}
/// <summary>
/// 获取索引下的页面
/// </summary>
/// <param name="pageIndex"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T IndexOfPageInfo<T>(int pageIndex)
where T : PageInfo
=> IndexOfPageInfo(pageIndex) as T;
/// <summary>
/// 获取索引下的页面
/// </summary>
/// <param name="pageIndex"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public PageInfo IndexOfPageInfo(int pageIndex)
=> _pageInfos.LandedIndex(pageIndex);
/// <summary>
/// 清除所有页面 (所有页面数据也会一同移除)
/// </summary>
#if ODIN_INSPECTOR
[HorizontalGroup("数据"), Button("清除所有页面")]
#endif
public virtual void ClearPages()
{
_pageInfos.Clear();
CurrentPageNode = null;
currentPageIndex = 0;
}
/// <summary>
/// 交换AB页面
/// </summary>
public virtual void SwapPages(int pageIndexA, int pageIndexB)
{
var a = _pageInfos.GetOrderNode().LandedIndex(pageIndexA);
var b = _pageInfos.GetOrderNode().LandedIndex(pageIndexB);
// 交换节点
_pageInfos.SwapNodes(a, b);
// 交换页面索引
(a.Value.pageNumber, b.Value.pageNumber) = (b.Value.pageNumber, a.Value.pageNumber);
// 刷新页面
FlipPage(currentPageIndex, true);
}
/// <summary>
/// 刷新所有页面索引
/// </summary>
public void RefreshAllPageNumber()
{
var index = 0;
foreach (var page in _pageInfos)
{
page.pageNumber = index++;
}
}
#endregion
#region 数据项目
/// <summary>
/// 添加数据项目
/// </summary>
/// <param name="item"></param>
/// <exception cref="Exception"></exception>
public virtual void AddItem(ItemInfo item)
{
AddItem(item, () => throw new Exception("需要翻页管理器实现相应的页面创建逻辑,并重写添加默认数据方法后才可以使用。"));
}
/// <summary>
/// 添加数据项目
/// </summary>
public virtual void AddItem(ItemInfo item, Func<PageInfo> addPage)
{
// 向前寻找空缺(这在页面都被准确填充的情况下最忧)
var firstVacancy = _pageInfos
.GetReversedOrderNode()
.LastOrDefault(a
=> a.Value.itemInfos.Count < DefaultItemCountPerPage);
if (firstVacancy != null)
{
firstVacancy.Value.itemInfos.Add(item);
_itemInfos.Add(item);
return;
}
if (addPage == null)
{
Debug.LogError("当前页面已满,数据项目期望顺位添加到新页面中,但未指定页面创建函数,添加过程已退出", this);
return;
}
// 新建一个页面存放数据
addPage.Invoke().itemInfos.Add(item);
_itemInfos.Add(item);
}
/// <summary>
/// 移除所有包含给定项目的位置(这会在页面上留下空缺)
/// </summary>
/// <param name="item"></param>
public virtual void RemoveItem(ItemInfo item)
{
foreach (var a in AllItemInfos
.Adjoint()
.Where(a => a.obj == item)
.Reverse())
{
ItemIndex2PageIndex(a.index);
}
}
public virtual void RemoveItem(int indexItem)
{
}
/// <summary>
/// 保留页面的状态下清空所有数据
/// </summary>
#if ODIN_INSPECTOR
[HorizontalGroup("数据"), Button("清除所有数据")]
#endif
public void ClearItemsWithoutPages()
{
foreach (var page in _pageInfos)
{
page.itemInfos.Clear();
}
}
/// <summary>
/// 使用索引定位到数据
/// </summary>
/// <param name="itemIndex"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T IndexOfItemInfo<T>(int itemIndex)
where T : ItemInfo
=> AllItemInfos.LandedIndex(itemIndex) as T;
/// <summary>
/// 将项目索引转为页面索引
/// </summary>
/// <param name="itemIndex"></param>
/// <returns></returns>
public virtual int ItemIndex2PageIndex(int itemIndex)
{
return itemIndex / DefaultItemCountPerPage;
}
/// <summary>
/// 当前翻页管理器中包含给定数据项目
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public bool Contains(ItemInfo item)
=> _itemInfos.Contains(item);
/// <summary>
/// 获取备份所有数据项目
/// </summary>
/// <param name="backupAtLocal">是否在本地备份?(默认启用,启用后返回的枚举器内容来自备份数据,允许推迟处理过程,但也会覆盖上一次备份)</param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public IEnumerable<T> BackupItemsFormPages<T>(bool backupAtLocal = true)
where T : ItemInfo
{
if (backupAtLocal)
{
BackupItemsFormPages();
return _backupItemInfos.OfType<T>();
}
return AllItemInfos.OfType<T>();
}
/// <summary>
/// 备份所有数据项目
/// </summary>
public List<ItemInfo> BackupItemsFormPages()
{
_backupItemInfos = AllItemInfos.ToList();
return _backupItemInfos;
}
/// <summary>
/// 强制重排所有数据项目(从备份中恢复数据)
/// </summary>
public void RestoreItemsToPages()
{
RestoreItemsToPages(_backupItemInfos);
}
/// <summary>
/// 强制重排所有数据项目(从备份中通过过滤器恢复数据)
/// </summary>
public void RestoreItemsByFilterToPages()
{
RestoreItemsToPages(_backupItemInfos.Where(a => a.PassFilter(this)).ToList());
}
/// <summary>
/// 强制重排所有数据项目(从给定备份中恢复数据)
/// </summary>
public void RestoreItemsToPages<T>(List<T> overrideDatas)
where T : ItemInfo
{
if (overrideDatas == null || overrideDatas.Count <= 0)
{
Debug.LogError($"重排数据需要一个有效的数据对象列表,如果希望将数据重置为空,可以使用Clear方法,而不是重排操作。", this);
return;
}
if (ItemIndex2PageIndex(overrideDatas.Count - 1) > _pageInfos.Count)
{
Debug.LogError($"重排数据时,原有数据数量({overrideDatas.Count})超出现有面板数量({_pageInfos.Count}),已拒绝重排", this);
return;
}
// 执行重排
var page = _pageInfos.First;
page.Value.itemInfos.Clear();
var pageIndex = 0;
for (int i = 0; i < overrideDatas.Count; i++)
{
var newPageIndex = ItemIndex2PageIndex(i);
if (pageIndex != newPageIndex)
{
pageIndex = newPageIndex;
page = page.Next;
if (page == null)
{
Debug.LogError($"重排数据时,页面链接错误,重排被迫中断", this);
return;
}
page.Value.itemInfos.Clear();
}
page.Value.itemInfos.Add(overrideDatas[i]);
}
}
#endregion
#region 类内类型
/// <summary>
/// 页面信息
/// </summary>
[Serializable]
public abstract class PageInfo
{
#region 字段属性
/// <summary>
/// 页面名称
/// </summary>
#if ODIN_INSPECTOR
[LabelText("页面名称")]
#endif
public string name = DateTime.Now.Ticks.ToString();
/// <summary>
/// 页面内项目列表
/// </summary>
#if ODIN_INSPECTOR
[LabelText("页面项目信息")]
#endif
public List<ItemInfo> itemInfos;
/// <summary>
/// 从零开始计数的页面下标
/// </summary>
#if ODIN_INSPECTOR
[LabelText("页面索引[0, n]")]
#endif
public int pageNumber { get; internal set; }
#endregion
#region 生命周期
/// <summary>
/// 页面创建时
/// </summary>
protected internal virtual void OnPageCreated(XericFlipPageIndexVesselBase parent)
{
// 获取列表
itemInfos = ListPool<ItemInfo>.Get();
}
/// <summary>
/// 页面销毁时
/// </summary>
protected internal virtual void OnPageDestroy(XericFlipPageIndexVesselBase parent)
{
// 从哈希表中移除
foreach (var itemInfo in itemInfos)
parent._itemInfos.Remove(itemInfo);
// 释放列表
if (itemInfos != null)
ListPool<ItemInfo>.Release(itemInfos);
}
/// <summary>
/// 页面显示时
/// </summary>
/// <param name="quickVisit">快速浏览(用于区分动画和逻辑,为真时,不应当在动画事件后继续加载数据)</param>
/// <code>
/// 页面中的成员会自动在非快速浏览模式下自动执行相应的激活函数
/// </code>
protected internal abstract void OnPageVisible(bool quickVisit);
/// <summary>
/// 页面隐藏时
/// </summary>
/// <param name="quickVisit">快速浏览(用于区分动画和逻辑,为真时,不应当在动画事件后继续加载数据)</param>
/// <code>
/// 页面中的成员会自动在非快速浏览模式下自动执行相应的激活函数
/// </code>
protected internal abstract void OnPageInvisible(bool quickVisit);
#endregion
/// <summary>
/// 传递子项激活回调事件
/// </summary>
protected internal virtual void CallChildItemVisible()
{
itemInfos
.Where(a => a != null)
.ForEachDo(itemInfo => itemInfo.OnItemVisible());
}
/// <summary>
/// 传递子项非激活回调事件
/// </summary>
protected internal virtual void CallChildItemInvisible()
{
itemInfos
.Where(a => a != null)
.ForEachDo(itemInfo => itemInfo.OnItemInvisible());
}
public override string ToString()
{
return $"[{nameof(PageInfo)}] ({name})";
}
}
/// <summary>
/// 项目信息
/// </summary>
[Serializable]
public abstract class ItemInfo
{
#region 字段属性
#if ODIN_INSPECTOR
[LabelText("项目名称")]
#endif
public string name = DateTime.Now.Ticks.ToString();
/// <summary>
/// 从零开始计数的项目下标
/// </summary>
#if ODIN_INSPECTOR
[LabelText("项目索引[0, n]")]
#endif
public int itemNumber { get; internal set; }
#endregion
#region 生命周期
/// <summary>
/// 当项目显示时
/// </summary>
/// <code>
/// 这会在页面显示之后执行
/// </code>
protected internal abstract void OnItemVisible();
/// <summary>
/// 当项目隐藏时
/// </summary>
/// <code>
/// 这会在页面关闭之前执行
/// </code>
protected internal abstract void OnItemInvisible();
#endregion
/// <summary>
/// 是否通过过滤器
/// </summary>
/// <param name="manager">传入管理器,建议将过滤器放在管理器中,然后具体的通过规则写在这里,根据对象的不同,通过的规则都不一样。</param>
/// <returns></returns>
public virtual bool PassFilter(XericFlipPageIndexVesselBase manager)
{
return true;
}
public override string ToString()
{
return $"[{nameof(ItemInfo)}] ({name})";
}
}
#endregion
}
}