Files
XericUIActionVessel/Runtime/FlipPage/XericFlipPageIndexVesselBase.cs

887 lines
27 KiB
C#
Raw Permalink 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;
using System.Collections.Generic;
using System.Linq;
#if ODIN_INSPECTOR
using Sirenix.OdinInspector;
#endif
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 : XericUIBehaviour
{
#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>
private ObjectPool<PageInfo> _pageInfoPool = null;
protected ObjectPool<PageInfo> PageInfoPool
{
get
{
if (_pageInfoPool != null)
return _pageInfoPool;
_pageInfoPool = new ObjectPool<PageInfo>(CreatPageInfo, GetPageInfo, ReleasePageInfo, DestroyPageInfo);
return _pageInfoPool;
}
}
protected virtual Func<PageInfo> HowToCreatPageInfo => () => throw new Exception("需要翻页管理器实现相应的页面创建逻辑,并重写添加默认数据方法后才可以使用。");
/// <summary>
/// 当前选中的页面索引
/// </summary>
#if ODIN_INSPECTOR
[LabelText("当前选中页面索引")]
#endif
[SerializeField]
protected int currentPageIndex = 0;
/// <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;
/// <summary>
/// 分布所有页面上的所有项目数量
/// </summary>
public int TotalItemCount => AllItemInfos.Count();
/// <summary>
/// 所有页面数量
/// </summary>
public int TotalPageCount => PageInfos.Count();
#if UNITY_EDITOR && ODIN_INSPECTOR
[LabelText("所有页面")]
[Obsolete("显示调试列表 属性仅编辑时有效,不要调用")]
private List<PageInfo> _pageInfoLists_ => _pageInfos.ToList();
#endif
#endregion
#region 生命周期
protected override void Awake()
{
base.Awake();
}
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>
/// <remarks>
/// 手动输入页数时执行此方法。
/// </remarks>
/// <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>
/// <remarks>
/// 翻页时,当前页会触发一次 OnPageInvisible,目标页会触发一次 OnPageVisible;
/// 翻到目标页数过程中除当前页和目标页之外的所有页面会触发一次OnPageVisible,OnPageInvisible的快速翻页;
/// 在重写相关事件时,利用快速翻页状态可以跳过动画加载,仅执行相关的触发器。
/// 如果压根不希望调用期间所有页面的翻页浏览事件,可以执行 JumpPage。
/// </remarks>
/// <param name="pageIndex">目标页数</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;
var quickPage = false; // 快速浏览页
bool visitOffsetDirIsNext;
var lastPage = CurrentPageNode;
// 如果节点不存在,通过索引直接命中
if (CurrentPageNode == null)
{
CurrentPageNode = _pageInfos.GetOrderNode().LandedIndex(currentPageIndex);
if (CurrentPageNode == null)
Debug.LogError($"索引{currentPageIndex}下的节点不存在(总数{_pageInfos.Count}页), 请检查后再试", this);
else
Currentpage.OnPageVisible(false);
}
// 如果索引偏移,逐个移动
else if (pageIndexOffset != 0)
{
// 判断方向
visitOffsetDirIsNext = pageIndexOffset > 0;
pageIndexOffset = Mathf.Abs(pageIndexOffset);
// 先关闭页面元素
CurrentPageNode.Value.CallChildItemInvisible();
for (var i = 1; i < pageIndexOffset; i++)
SwitchPage(false);
SwitchPage(true);
}
// 没有翻动有效页,直接退出
else
return;
// 可能时当前不存在任何页面
if (CurrentPageNode == null)
return;
// 页面元素激活
CurrentPageNode.Value.CallChildItemVisible();
currentPageIndex = pageIndex;
OnFlipPage();
return;
void SwitchPage(bool isEndPage)
{
Currentpage.OnPageInvisible(quickPage);
quickPage = !isEndPage;
// 先检查页面,再执行委托
var nextPage = visitOffsetDirIsNext ? CurrentPageNode.Next : CurrentPageNode.Previous;
// 如果都正常,执行生命周期
if (nextPage is { Value: not null })
{
nextPage.Value.OnPageVisible(quickPage);
}
// 如果跳转失败,说明到头了,检查是否允许循环索引
else if (canPassIndexBoundary)
{
CurrentPageNode = visitOffsetDirIsNext ? _pageInfos.First : _pageInfos.Last;
CurrentPageNode.Value.OnPageVisible(quickPage);
}
// 不允许循环索引就报个错然后什么都不做
else
{
Debug.LogWarning("翻页超出范围,由于页面不允许循环索引,所以现在翻到了空页面");
CurrentPageNode = null;
return;
}
CurrentPageNode = nextPage;
}
}
/// <summary>
/// 径直跳转到页 (skip quick Page)
/// </summary>
/// <remarks>
/// 相比 FlipPage 方法,JumpPage会直接空降到页,不会调用期间其他页的浏览事件。
/// </remarks>
public virtual void JumpPage(int pageIndex, bool forceRefresh = false)
{
// 子项隐藏
CurrentPageNode?.Value.CallChildItemInvisible();
// 计算索引对应的页面
var page = _pageInfos.First;
while (pageIndex-- > 0 && page.Next != null)
page = page.Next;
// 只执行非快速浏览的回调
Currentpage?.OnPageInvisible(false);
CurrentPageNode = page;
Currentpage?.OnPageVisible(false);
currentPageIndex = pageIndex;
// 子项显示
CurrentPageNode?.Value.CallChildItemVisible();
}
/// <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>
/// <remarks>
/// 注意统一页面类型,如果没有指定自定义的页面,那么
/// </remarks>
public virtual PageInfo AddPage(PageInfo pageInfo = null, bool isFirstOtherwiseLast = false)
{
if (pageInfo == null)
pageInfo = PageInfoPool.Get();
if (isFirstOtherwiseLast)
{
// 添加页面(页面下标默认0)
_pageInfos.AddFirst(pageInfo);
// 更新后面页面的索引
RefreshAllPageNumber();
}
else
{
// 添加页面下标
pageInfo.pageNumber = _pageInfos.Count;
// 添加页面
_pageInfos.AddLast(pageInfo);
}
// GetPageInfo(pageInfo);
// 执行页面生命周期
pageInfo.OnPageCreated(this);
// 刷新页面
FlipPage(currentPageIndex, true);
return pageInfo;
}
/// <summary>
/// 按对象引用删除页面
/// </summary>
/// <param name="pageInfo"></param>
public virtual void RemovePage(PageInfo pageInfo)
{
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++;
}
// 刷新页面
FlipPage(currentPageIndex, true);
}
/// <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>
/// <param name="resetPool">同时重置页面池</param>
/// <remarks>
/// 页面的引用会从管理器中释放,同时需要自行重写页面 OnPageDestroy 确保页面准确释放了各种东西。
/// 除非你确认你的页面数据对象类型是一致的,否则无论如何都建议清空页面时也清空对象池,不然会带来严重的类型污染。
/// </remarks>
#if ODIN_INSPECTOR
[HorizontalGroup("数据"), Button("清除所有页面")]
#endif
public virtual void ClearPages(bool resetPool = true)
{
if (CurrentPageNode != null)
Currentpage.OnPageInvisible(true);
var tempPageInfo = ListPool<PageInfo>.Get();
tempPageInfo.AddRange(_pageInfos);
foreach (var pageInfo in tempPageInfo)
{
pageInfo.ItemInfos.Clear();
PageInfoPool.Release(pageInfo);
}
ListPool<PageInfo>.Release(tempPageInfo);
// 清空池
if (resetPool)
PageInfoPool?.Clear();
// 重置当前页面
CurrentPageNode = null;
currentPageIndex = 0;
// 清空缓存
_pageInfos.Clear();
_itemInfos.Clear();
}
/// <summary>
/// 关闭所有页面
/// </summary>
public void InactivePages() => SetPagesActive(false);
/// <summary>
/// 重新激活所有页面
/// </summary>
public void ReactivePages() => SetPagesActive(true);
/// <summary>
/// 设置所有页面的激活状态
/// </summary>
/// <param name="active"></param>
public void SetPagesActive(bool active)
{
foreach (var pageInfo in _pageInfos)
pageInfo.pageActive = active;
FlipPage(0, true);
}
/// <summary>
/// 使用自定义的方法设置激活状态
/// </summary>
/// <param name="isActive"></param>
/// <typeparam name="T"></typeparam>
public void SetPagesActive<T>(Func<T, bool> isActive)
where T : PageInfo
{
foreach (var pageInfo in _pageInfos)
pageInfo.pageActive = isActive((T)pageInfo);
FlipPage(0, true);
}
/// <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>
/// <returns></returns>
protected virtual PageInfo CreatPageInfo() => HowToCreatPageInfo();
/// <summary>
/// 定义对象池如何执行获取页面后的事件
/// </summary>
/// <param name="info"></param>
protected virtual void GetPageInfo(PageInfo info)
{
// 检查代码中重复调用
if (!info.IsAlreadyRelease)
{
Debug.LogError($"流程错误,页面尝试重复获取,请检查代码流程({info.GetHashCode():X16})\n_>{info.name}");
return;
}
info.OnPageCreated(this);
return;
}
/// <summary>
/// 定义对象池如何执行释放页面后的事件
/// </summary>
/// <param name="info"></param>
protected virtual void ReleasePageInfo(PageInfo info)
{
// 检查代码中重复调用
if (info.IsAlreadyRelease)
{
Debug.LogError("流程错误,页面尝试重复释放,请检查代码流程");
return;
}
if (CurrentPageNode != null && Currentpage == info)
{
info.OnPageInvisible(true);
CurrentPageNode = null;
}
info.OnPageDestroy(this);
_pageInfos.Remove(info);
return;
}
/// <summary>
/// 定义对象池如何执行销毁页面后的事件
/// </summary>
/// <param name="info"></param>
protected virtual void DestroyPageInfo(PageInfo info)
{
}
#endregion
#region 数据项目
/// <summary>
/// 添加一组数据项目(方法待优化)
/// </summary>
/// <param name="items"></param>
/// <param name="customPage"></param>
public virtual void AddItems(IEnumerable<ItemInfo> items, PageInfo customPage = null)
{
foreach (var item in items)
AddItem(item, customPage);
}
/// <summary>
/// 添加数据项目
/// </summary>
/// <param name="item"></param>
/// <typeparam name="T">提供一个指定的数据类型,默认添加这个类型的页面</typeparam>
public virtual void AddItem<T>(ItemInfo item)
where T : PageInfo, new()
=> AddItem(item, new T());
/// <summary>
/// 添加数据项目
/// </summary>
/// <remarks>
/// 添加项目到页面中
/// </remarks>
public virtual void AddItem(ItemInfo item, PageInfo customPage = null)
{
var page = _pageInfos.Count <= 0 ? null : _pageInfos.Last.Value;
if (page != null &&
page.CheckPageAnyLack(item) &&
page.ItemInfos.Count < DefaultItemCountPerPage)
{
page.ItemInfos.Add(item);
_itemInfos.Add(item);
return;
}
// 新建一个页面存放数据
page = customPage ?? PageInfoPool.Get();
AddPage(page);
page.ItemInfos.Add(item);
_itemInfos.Add(item);
}
/// <summary>
/// 移除所有包含给定项目的位置(这会在页面上留下空缺)
/// </summary>
/// <param name="item"></param>
public virtual void RemoveItem(ItemInfo item)
{
_itemInfos.Remove(item);
foreach (var a in AllItemInfos
.Adjoint()
.Where(a => a.obj == item)
.Reverse())
{
ItemIndex2PageIndex(a.index);
}
}
/// <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)
=> itemIndex / DefaultItemCountPerPage;
/// <summary>
/// 当前翻页管理器中包含给定数据项目
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public bool Contains(ItemInfo item)
=> _itemInfos.Contains(item);
#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>
[SerializeField]
#if ODIN_INSPECTOR
[LabelText("页面项目信息")]
#endif
private List<ItemInfo> _itemInfos;
public List<ItemInfo> ItemInfos
{
get => _itemInfos ??= ListPool<ItemInfo>.Get();
set => _itemInfos = value;
}
/// <summary>
/// 从零开始计数的页面下标
/// </summary>
#if ODIN_INSPECTOR
[LabelText("页面索引[0, n]")]
#endif
public int pageNumber { get; internal set; }
public bool pageActive;
/// <summary>
/// 随生命周期标识当前是否已经被释放
/// </summary>
public bool IsAlreadyRelease { get; internal set; } = true;
#endregion
#region 生命周期
/// <summary>
/// 页面创建时
/// </summary>
protected internal virtual void OnPageCreated(XericFlipPageIndexVesselBase parent)
{
IsAlreadyRelease = false;
}
/// <summary>
/// 页面销毁时
/// </summary>
protected internal virtual void OnPageDestroy(XericFlipPageIndexVesselBase parent)
{
IsAlreadyRelease = true;
// 从哈希表中移除
foreach (var itemInfo in ItemInfos)
parent._itemInfos.Remove(itemInfo);
// 释放列表
if (ItemInfos != null)
{
ListPool<ItemInfo>.Release(ItemInfos);
ItemInfos = null;
}
}
/// <summary>
/// 页面显示时回调
/// </summary>
/// <param name="quickVisit">快速浏览(用于区分动画和逻辑,为真时,不应当在动画事件后继续加载数据)</param>
/// <remarks>
/// 页面中的成员会自动执行其相应的激活函数,不需要在这里手动调用。
/// 快速浏览主要为了区分当前执行的是动画事件还是纯逻辑。
/// </remarks>
protected internal abstract void OnPageVisible(bool quickVisit);
/// <summary>
/// 页面隐藏时回调
/// </summary>
/// <param name="quickVisit">快速浏览(用于区分动画和逻辑,为真时,不应当在动画事件后继续加载数据)</param>
/// <remarks>
/// 页面中的成员会自动执行其相应的激活函数,不需要在这里手动调用。
/// 快速浏览主要为了区分当前执行的是动画事件还是纯逻辑。
/// </remarks>
protected internal abstract void OnPageInvisible(bool quickVisit);
#endregion
/// <summary>
/// 子项激活回调(应呼叫子项激活)
/// </summary>
/// <remarks>
/// 仅页面触发快速浏览,子项没有这个功能
/// </remarks>
protected internal virtual void CallChildItemVisible()
{
ItemInfos
.Where(a => a != null)
.ForEachDo(itemInfo => itemInfo.OnItemVisible());
}
/// <summary>
/// 子项非激活回调(应呼叫子项隐藏)
/// </summary>
/// <remarks>
/// 仅页面触发快速浏览,子项没有这个功能
/// </remarks>
protected internal virtual void CallChildItemInvisible()
{
ItemInfos
.Where(a => a != null)
.ForEachDo(itemInfo => itemInfo.OnItemInvisible());
}
/// <summary>
/// 检查页面是否存在可以放下对象的空缺
/// </summary>
/// <returns></returns>
protected internal virtual bool CheckPageAnyLack(ItemInfo item = null)
{
return true;
}
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
public override string ToString()
{
return $"[{nameof(ItemInfo)}] ({name})";
}
}
#endregion
}
}