Compare commits
2 Commits
b3f3f268bf
...
2471317801
| Author | SHA1 | Date | |
|---|---|---|---|
| 2471317801 | |||
| 623edbeed6 |
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f9ad1ce05574ba886146f9982e01142
|
||||
timeCreated: 1764839428
|
||||
@ -1,245 +0,0 @@
|
||||
#if INPUTSYSTEM_SUPPORT
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
public static class InputDeviceWatcher
|
||||
{
|
||||
public enum InputDeviceCategory
|
||||
{
|
||||
Keyboard,
|
||||
Xbox,
|
||||
PlayStation,
|
||||
Other
|
||||
}
|
||||
|
||||
static readonly float DebounceWindow = 1f;
|
||||
public static InputDeviceCategory CurrentCategory = InputDeviceCategory.Keyboard;
|
||||
public static string CurrentDeviceName = "";
|
||||
|
||||
private static InputAction _anyInputAction;
|
||||
private static int _lastDeviceId = -1;
|
||||
private static float _lastInputTime = -Mathf.Infinity;
|
||||
|
||||
private static InputDeviceCategory _lastEmittedCategory = InputDeviceCategory.Keyboard;
|
||||
|
||||
public static event Action<InputDeviceCategory> OnDeviceChanged;
|
||||
|
||||
private static bool initialized = false;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
public static void Initialize()
|
||||
{
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
|
||||
CurrentCategory = InputDeviceCategory.Keyboard;
|
||||
CurrentDeviceName = "";
|
||||
_lastEmittedCategory = CurrentCategory; // 初始化同步
|
||||
|
||||
_anyInputAction = new InputAction("AnyDevice", InputActionType.PassThrough);
|
||||
_anyInputAction.AddBinding("<Keyboard>/anyKey");
|
||||
_anyInputAction.AddBinding("<Gamepad>/*");
|
||||
_anyInputAction.AddBinding("<Joystick>/*");
|
||||
|
||||
_anyInputAction.performed += OnAnyInputPerformed;
|
||||
_anyInputAction.Enable();
|
||||
|
||||
InputSystem.onDeviceChange += OnDeviceChange;
|
||||
#if UNITY_EDITOR
|
||||
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
static void OnPlayModeStateChanged(PlayModeStateChange state)
|
||||
{
|
||||
if (state == PlayModeStateChange.ExitingPlayMode)
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
||||
}
|
||||
#endif
|
||||
|
||||
public static void Dispose()
|
||||
{
|
||||
if (!initialized) return;
|
||||
CurrentCategory = InputDeviceCategory.Keyboard;
|
||||
_anyInputAction.performed -= OnAnyInputPerformed;
|
||||
_anyInputAction.Disable();
|
||||
_anyInputAction.Dispose();
|
||||
|
||||
InputSystem.onDeviceChange -= OnDeviceChange;
|
||||
|
||||
OnDeviceChanged = null;
|
||||
initialized = false;
|
||||
|
||||
_lastEmittedCategory = InputDeviceCategory.Keyboard;
|
||||
}
|
||||
|
||||
private static void OnAnyInputPerformed(InputAction.CallbackContext ctx)
|
||||
{
|
||||
if (ctx.control == null || ctx.control.device == null) return;
|
||||
|
||||
var device = ctx.control.device;
|
||||
|
||||
if (!IsRelevantDevice(device)) return;
|
||||
|
||||
int curId = device.deviceId;
|
||||
float now = Time.realtimeSinceStartup;
|
||||
|
||||
if (curId == _lastDeviceId) return;
|
||||
if (DebounceWindow > 0f && (now - _lastInputTime) < DebounceWindow) return;
|
||||
|
||||
_lastInputTime = now;
|
||||
_lastDeviceId = curId;
|
||||
|
||||
CurrentCategory = DetermineCategoryFromDevice(device);
|
||||
CurrentDeviceName = device.displayName ?? $"Device_{curId}";
|
||||
|
||||
EmitChange();
|
||||
}
|
||||
|
||||
private static void OnDeviceChange(InputDevice device, InputDeviceChange change)
|
||||
{
|
||||
if (change == InputDeviceChange.Removed || change == InputDeviceChange.Disconnected)
|
||||
{
|
||||
if (device.deviceId == _lastDeviceId)
|
||||
{
|
||||
_lastDeviceId = -1;
|
||||
_lastInputTime = -Mathf.Infinity;
|
||||
CurrentDeviceName = "";
|
||||
CurrentCategory = InputDeviceCategory.Keyboard;
|
||||
EmitChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------ 分类逻辑 --------------------
|
||||
private static InputDeviceCategory DetermineCategoryFromDevice(InputDevice device)
|
||||
{
|
||||
if (device == null) return InputDeviceCategory.Keyboard;
|
||||
// 重要:鼠标不再被视为键盘类(避免鼠标触发时回退到 Keyboard)
|
||||
if (device is Keyboard) return InputDeviceCategory.Keyboard;
|
||||
if (device is Mouse) return InputDeviceCategory.Other; // 明确忽略鼠标
|
||||
if (IsGamepadLike(device)) return GetGamepadCategory(device);
|
||||
|
||||
string combined = $"{device.description.interfaceName} {device.layout} {device.description.product} {device.description.manufacturer} {device.displayName}".ToLower();
|
||||
|
||||
if (combined.Contains("xbox") || combined.Contains("xinput")) return InputDeviceCategory.Xbox;
|
||||
if (combined.Contains("dualshock") || combined.Contains("dualsense") || combined.Contains("playstation")) return InputDeviceCategory.PlayStation;
|
||||
|
||||
return InputDeviceCategory.Other;
|
||||
}
|
||||
|
||||
private static bool IsGamepadLike(InputDevice device)
|
||||
{
|
||||
if (device is Gamepad) return true;
|
||||
if (device is Joystick) return true;
|
||||
|
||||
var layout = (device.layout ?? "").ToLower();
|
||||
// 这里保留 controller/gamepad/joystick 的识别,但忽略 mouse/touch 等
|
||||
if (layout.Contains("mouse") || layout.Contains("touch") || layout.Contains("pen")) return false;
|
||||
return layout.Contains("gamepad") || layout.Contains("controller") || layout.Contains("joystick");
|
||||
}
|
||||
|
||||
private static bool IsRelevantDevice(InputDevice device)
|
||||
{
|
||||
if (device == null) return false;
|
||||
if (device is Keyboard) return true;
|
||||
if (IsGamepadLike(device)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static InputDeviceCategory GetGamepadCategory(InputDevice device)
|
||||
{
|
||||
if (device == null) return InputDeviceCategory.Other;
|
||||
|
||||
var iface = (device.description.interfaceName ?? "").ToLower();
|
||||
if (iface.Contains("xinput")) return InputDeviceCategory.Xbox;
|
||||
|
||||
if (TryParseVidPidFromCapabilities(device.description.capabilities, out int vendorId, out int _))
|
||||
{
|
||||
if (vendorId == 0x045E || vendorId == 1118) return InputDeviceCategory.Xbox;
|
||||
if (vendorId == 0x054C || vendorId == 1356) return InputDeviceCategory.PlayStation;
|
||||
}
|
||||
|
||||
string combined = $"{device.description.interfaceName} {device.layout} {device.description.product} {device.description.manufacturer} {device.displayName}".ToLower();
|
||||
if (combined.Contains("xbox")) return InputDeviceCategory.Xbox;
|
||||
if (combined.Contains("dualshock") || combined.Contains("playstation")) return InputDeviceCategory.PlayStation;
|
||||
|
||||
return InputDeviceCategory.Other;
|
||||
}
|
||||
|
||||
// ------------------ VID/PID 解析 --------------------
|
||||
private static bool TryParseVidPidFromCapabilities(string capabilities, out int vendorId, out int productId)
|
||||
{
|
||||
vendorId = 0;
|
||||
productId = 0;
|
||||
if (string.IsNullOrEmpty(capabilities)) return false;
|
||||
|
||||
try
|
||||
{
|
||||
var decVendor = Regex.Match(capabilities, "\"vendorId\"\\s*:\\s*(\\d+)", RegexOptions.IgnoreCase);
|
||||
var decProduct = Regex.Match(capabilities, "\"productId\"\\s*:\\s*(\\d+)", RegexOptions.IgnoreCase);
|
||||
|
||||
if (decVendor.Success) int.TryParse(decVendor.Groups[1].Value, out vendorId);
|
||||
if (decProduct.Success) int.TryParse(decProduct.Groups[1].Value, out productId);
|
||||
|
||||
return vendorId != 0 || productId != 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmitChange()
|
||||
{
|
||||
if (CurrentCategory == _lastEmittedCategory)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int vid = GetVendorId();
|
||||
int pid = GetProductId();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
Debug.Log($"输入设备变更 -> {CurrentCategory} 触发设备: {CurrentDeviceName} vid=0x{vid:X} pid=0x{pid:X}");
|
||||
#endif
|
||||
|
||||
OnDeviceChanged?.Invoke(CurrentCategory);
|
||||
_lastEmittedCategory = CurrentCategory;
|
||||
}
|
||||
|
||||
private static int GetVendorId()
|
||||
{
|
||||
foreach (var d in InputSystem.devices)
|
||||
{
|
||||
if ((d.displayName ?? "") == CurrentDeviceName &&
|
||||
TryParseVidPidFromCapabilities(d.description.capabilities, out int v, out int _))
|
||||
return v;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int GetProductId()
|
||||
{
|
||||
foreach (var d in InputSystem.devices)
|
||||
{
|
||||
if ((d.displayName ?? "") == CurrentDeviceName &&
|
||||
TryParseVidPidFromCapabilities(d.description.capabilities, out int _, out int p))
|
||||
return p;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e78f6224467e13742a70115f1942d941
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -3,6 +3,10 @@ using System.Collections.Generic;
|
||||
|
||||
namespace AlicizaX.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// RecyclerView 的通用适配器,支持数据绑定、选中状态和列表操作
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数据类型,必须实现 ISimpleViewData 接口</typeparam>
|
||||
public class Adapter<T> : IAdapter where T : ISimpleViewData
|
||||
{
|
||||
protected RecyclerView recyclerView;
|
||||
@ -12,20 +16,38 @@ namespace AlicizaX.UI
|
||||
|
||||
protected int choiceIndex = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 当前选中项的索引
|
||||
/// </summary>
|
||||
public int ChoiceIndex
|
||||
{
|
||||
get => choiceIndex;
|
||||
set { SetChoiceIndex(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="recyclerView">RecyclerView 实例</param>
|
||||
public Adapter(RecyclerView recyclerView) : this(recyclerView, new List<T>(), null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="recyclerView">RecyclerView 实例</param>
|
||||
/// <param name="list">数据列表</param>
|
||||
public Adapter(RecyclerView recyclerView, List<T> list) : this(recyclerView, list, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="recyclerView">RecyclerView 实例</param>
|
||||
/// <param name="list">数据列表</param>
|
||||
/// <param name="onItemClick">列表项点击回调</param>
|
||||
public Adapter(RecyclerView recyclerView, List<T> list, Action<T> onItemClick)
|
||||
{
|
||||
this.recyclerView = recyclerView;
|
||||
@ -33,21 +55,33 @@ namespace AlicizaX.UI
|
||||
this.onItemClick = onItemClick;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取列表项总数
|
||||
/// </summary>
|
||||
public virtual int GetItemCount()
|
||||
{
|
||||
return list == null ? 0 : list.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取实际数据项数量
|
||||
/// </summary>
|
||||
public virtual int GetRealCount()
|
||||
{
|
||||
return GetItemCount();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定索引位置的视图名称
|
||||
/// </summary>
|
||||
public virtual string GetViewName(int index)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绑定视图持有者与数据
|
||||
/// </summary>
|
||||
public virtual void OnBindViewHolder(ViewHolder viewHolder, int index)
|
||||
{
|
||||
if (index < 0 || index >= GetItemCount()) return;
|
||||
@ -63,12 +97,19 @@ namespace AlicizaX.UI
|
||||
viewHolder.BindChoiceState(index == choiceIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知数据已更改
|
||||
/// </summary>
|
||||
public virtual void NotifyDataChanged()
|
||||
{
|
||||
recyclerView.RequestLayout();
|
||||
recyclerView.Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置数据列表并刷新视图
|
||||
/// </summary>
|
||||
/// <param name="list">新的数据列表</param>
|
||||
public virtual void SetList(List<T> list)
|
||||
{
|
||||
this.list = list;
|
||||
@ -76,6 +117,11 @@ namespace AlicizaX.UI
|
||||
NotifyDataChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定索引的数据
|
||||
/// </summary>
|
||||
/// <param name="index">数据索引</param>
|
||||
/// <returns>数据对象,索引无效时返回 default</returns>
|
||||
public T GetData(int index)
|
||||
{
|
||||
if (index < 0 || index >= GetItemCount()) return default;
|
||||
@ -83,36 +129,62 @@ namespace AlicizaX.UI
|
||||
return list[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加单个数据项
|
||||
/// </summary>
|
||||
/// <param name="item">要添加的数据项</param>
|
||||
public void Add(T item)
|
||||
{
|
||||
list.Add(item);
|
||||
NotifyDataChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量添加数据项
|
||||
/// </summary>
|
||||
/// <param name="collection">要添加的数据集合</param>
|
||||
public void AddRange(IEnumerable<T> collection)
|
||||
{
|
||||
list.AddRange(collection);
|
||||
NotifyDataChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在指定位置插入数据项
|
||||
/// </summary>
|
||||
/// <param name="index">插入位置</param>
|
||||
/// <param name="item">要插入的数据项</param>
|
||||
public void Insert(int index, T item)
|
||||
{
|
||||
list.Insert(index, item);
|
||||
NotifyDataChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在指定位置批量插入数据项
|
||||
/// </summary>
|
||||
/// <param name="index">插入位置</param>
|
||||
/// <param name="collection">要插入的数据集合</param>
|
||||
public void InsertRange(int index, IEnumerable<T> collection)
|
||||
{
|
||||
list.InsertRange(index, collection);
|
||||
NotifyDataChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除指定的数据项
|
||||
/// </summary>
|
||||
/// <param name="item">要移除的数据项</param>
|
||||
public void Remove(T item)
|
||||
{
|
||||
int index = list.IndexOf(item);
|
||||
RemoveAt(index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除指定索引位置的数据项
|
||||
/// </summary>
|
||||
/// <param name="index">要移除的索引</param>
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
if (index < 0 || index >= GetItemCount()) return;
|
||||
@ -121,47 +193,79 @@ namespace AlicizaX.UI
|
||||
NotifyDataChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除指定范围的数据项
|
||||
/// </summary>
|
||||
/// <param name="index">起始索引</param>
|
||||
/// <param name="count">移除数量</param>
|
||||
public void RemoveRange(int index, int count)
|
||||
{
|
||||
list.RemoveRange(index, count);
|
||||
NotifyDataChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除所有符合条件的数据项
|
||||
/// </summary>
|
||||
/// <param name="match">匹配条件</param>
|
||||
public void RemoveAll(Predicate<T> match)
|
||||
{
|
||||
list.RemoveAll(match);
|
||||
NotifyDataChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有数据
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
list.Clear();
|
||||
NotifyDataChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反转指定范围的数据项顺序
|
||||
/// </summary>
|
||||
/// <param name="index">起始索引</param>
|
||||
/// <param name="count">反转数量</param>
|
||||
public void Reverse(int index, int count)
|
||||
{
|
||||
list.Reverse(index, count);
|
||||
NotifyDataChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反转所有数据项顺序
|
||||
/// </summary>
|
||||
public void Reverse()
|
||||
{
|
||||
list.Reverse();
|
||||
NotifyDataChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定的比较器排序数据
|
||||
/// </summary>
|
||||
/// <param name="comparison">比较器</param>
|
||||
public void Sort(Comparison<T> comparison)
|
||||
{
|
||||
list.Sort(comparison);
|
||||
NotifyDataChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置列表项点击回调
|
||||
/// </summary>
|
||||
/// <param name="onItemClick">点击回调</param>
|
||||
public void SetOnItemClick(Action<T> onItemClick)
|
||||
{
|
||||
this.onItemClick = onItemClick;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置选中项索引
|
||||
/// </summary>
|
||||
/// <param name="index">要选中的索引</param>
|
||||
protected void SetChoiceIndex(int index)
|
||||
{
|
||||
if (index == choiceIndex) return;
|
||||
@ -185,6 +289,12 @@ namespace AlicizaX.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取指定索引的视图持有者
|
||||
/// </summary>
|
||||
/// <param name="index">数据索引</param>
|
||||
/// <param name="viewHolder">输出的视图持有者</param>
|
||||
/// <returns>是否成功获取</returns>
|
||||
private bool TryGetViewHolder(int index, out ViewHolder viewHolder)
|
||||
{
|
||||
viewHolder = recyclerView.ViewProvider.GetViewHolder(index);
|
||||
|
||||
@ -1,15 +1,39 @@
|
||||
namespace AlicizaX.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// RecyclerView 适配器接口,负责提供数据和绑定视图
|
||||
/// </summary>
|
||||
public interface IAdapter
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取列表项总数(包括循环或分组后的虚拟数量)
|
||||
/// </summary>
|
||||
/// <returns>列表项总数</returns>
|
||||
int GetItemCount();
|
||||
|
||||
/// <summary>
|
||||
/// 获取实际数据项数量(不包括循环或分组的虚拟数量)
|
||||
/// </summary>
|
||||
/// <returns>实际数据项数量</returns>
|
||||
int GetRealCount();
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定索引位置的视图名称,用于视图类型区分
|
||||
/// </summary>
|
||||
/// <param name="index">列表项索引</param>
|
||||
/// <returns>视图名称</returns>
|
||||
string GetViewName(int index);
|
||||
|
||||
/// <summary>
|
||||
/// 绑定视图持有者与数据
|
||||
/// </summary>
|
||||
/// <param name="viewHolder">视图持有者</param>
|
||||
/// <param name="index">数据索引</param>
|
||||
void OnBindViewHolder(ViewHolder viewHolder, int index);
|
||||
|
||||
/// <summary>
|
||||
/// 通知数据已更改,触发视图刷新
|
||||
/// </summary>
|
||||
void NotifyDataChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,23 @@
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// 缓动函数工具类
|
||||
/// 提供各种常用的缓动函数,用于实现平滑的动画效果
|
||||
/// 基于 https://easings.net/ 的标准缓动函数
|
||||
/// </summary>
|
||||
public class EaseUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// 正弦缓入函数
|
||||
/// </summary>
|
||||
public static double EaseInSine(float x)
|
||||
{
|
||||
return 1 - Math.Cos(x * Math.PI / 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 正弦缓出函数
|
||||
/// </summary>
|
||||
public static double EaseOutSine(float x)
|
||||
{
|
||||
return Math.Sin(x * Math.PI / 2);
|
||||
|
||||
@ -2,10 +2,18 @@ using UnityEngine;
|
||||
|
||||
namespace AlicizaX.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 布局管理器抽象基类
|
||||
/// 负责计算和管理 RecyclerView 中列表项的位置、大小和可见性
|
||||
/// 子类需要实现具体的布局算法(如线性、网格、圆形等)
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public abstract class LayoutManager : ILayoutManager
|
||||
{
|
||||
protected Vector2 viewportSize;
|
||||
/// <summary>
|
||||
/// 获取视口大小(可见区域的尺寸)
|
||||
/// </summary>
|
||||
public Vector2 ViewportSize
|
||||
{
|
||||
get => viewportSize;
|
||||
@ -13,6 +21,9 @@ namespace AlicizaX.UI
|
||||
}
|
||||
|
||||
protected Vector2 contentSize;
|
||||
/// <summary>
|
||||
/// 获取内容总大小(所有列表项占据的总尺寸)
|
||||
/// </summary>
|
||||
public Vector2 ContentSize
|
||||
{
|
||||
get => contentSize;
|
||||
@ -20,6 +31,9 @@ namespace AlicizaX.UI
|
||||
}
|
||||
|
||||
protected Vector2 contentOffset;
|
||||
/// <summary>
|
||||
/// 获取内容偏移量(用于对齐计算)
|
||||
/// </summary>
|
||||
public Vector2 ContentOffset
|
||||
{
|
||||
get => contentOffset;
|
||||
@ -27,6 +41,9 @@ namespace AlicizaX.UI
|
||||
}
|
||||
|
||||
protected Vector2 viewportOffset;
|
||||
/// <summary>
|
||||
/// 获取视口偏移量(用于对齐计算)
|
||||
/// </summary>
|
||||
public Vector2 ViewportOffset
|
||||
{
|
||||
get => viewportOffset;
|
||||
@ -34,6 +51,9 @@ namespace AlicizaX.UI
|
||||
}
|
||||
|
||||
protected IAdapter adapter;
|
||||
/// <summary>
|
||||
/// 获取或设置数据适配器
|
||||
/// </summary>
|
||||
public IAdapter Adapter
|
||||
{
|
||||
get => adapter;
|
||||
@ -41,6 +61,9 @@ namespace AlicizaX.UI
|
||||
}
|
||||
|
||||
protected ViewProvider viewProvider;
|
||||
/// <summary>
|
||||
/// 获取或设置视图提供器
|
||||
/// </summary>
|
||||
public ViewProvider ViewProvider
|
||||
{
|
||||
get => viewProvider;
|
||||
@ -48,6 +71,9 @@ namespace AlicizaX.UI
|
||||
}
|
||||
|
||||
protected RecyclerView recyclerView;
|
||||
/// <summary>
|
||||
/// 获取或设置关联的 RecyclerView 实例
|
||||
/// </summary>
|
||||
public virtual RecyclerView RecyclerView
|
||||
{
|
||||
get => recyclerView;
|
||||
@ -55,6 +81,9 @@ namespace AlicizaX.UI
|
||||
}
|
||||
|
||||
protected Direction direction;
|
||||
/// <summary>
|
||||
/// 获取或设置滚动方向
|
||||
/// </summary>
|
||||
public Direction Direction
|
||||
{
|
||||
get => direction;
|
||||
@ -62,6 +91,9 @@ namespace AlicizaX.UI
|
||||
}
|
||||
|
||||
protected Alignment alignment;
|
||||
/// <summary>
|
||||
/// 获取或设置对齐方式
|
||||
/// </summary>
|
||||
public Alignment Alignment
|
||||
{
|
||||
get => alignment;
|
||||
@ -69,6 +101,9 @@ namespace AlicizaX.UI
|
||||
}
|
||||
|
||||
protected Vector2 spacing;
|
||||
/// <summary>
|
||||
/// 获取或设置列表项间距
|
||||
/// </summary>
|
||||
public Vector2 Spacing
|
||||
{
|
||||
get => spacing;
|
||||
@ -76,6 +111,9 @@ namespace AlicizaX.UI
|
||||
}
|
||||
|
||||
protected Vector2 padding;
|
||||
/// <summary>
|
||||
/// 获取或设置内边距
|
||||
/// </summary>
|
||||
public Vector2 Padding
|
||||
{
|
||||
get => padding;
|
||||
@ -83,6 +121,9 @@ namespace AlicizaX.UI
|
||||
}
|
||||
|
||||
protected int unit = 1;
|
||||
/// <summary>
|
||||
/// 获取或设置布局单元(用于网格布局等,表示一次处理多少个项)
|
||||
/// </summary>
|
||||
public int Unit
|
||||
{
|
||||
get => unit;
|
||||
@ -90,10 +131,17 @@ namespace AlicizaX.UI
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前滚动位置
|
||||
/// </summary>
|
||||
public float ScrollPosition => recyclerView.GetScrollPosition();
|
||||
|
||||
public LayoutManager() { }
|
||||
|
||||
/// <summary>
|
||||
/// 设置内容大小
|
||||
/// 计算视口大小、内容大小以及各种偏移量
|
||||
/// </summary>
|
||||
public void SetContentSize()
|
||||
{
|
||||
viewportSize = recyclerView.GetComponent<RectTransform>().rect.size;
|
||||
@ -102,6 +150,10 @@ namespace AlicizaX.UI
|
||||
viewportOffset = CalculateViewportOffset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新所有可见 ViewHolder 的布局
|
||||
/// 遍历所有当前显示的 ViewHolder 并重新计算其位置
|
||||
/// </summary>
|
||||
public void UpdateLayout()
|
||||
{
|
||||
foreach (var viewHolder in viewProvider.ViewHolders)
|
||||
@ -110,6 +162,11 @@ namespace AlicizaX.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为指定的 ViewHolder 设置布局位置
|
||||
/// </summary>
|
||||
/// <param name="viewHolder">要布局的 ViewHolder</param>
|
||||
/// <param name="index">ViewHolder 对应的数据索引</param>
|
||||
public virtual void Layout(ViewHolder viewHolder, int index)
|
||||
{
|
||||
Vector2 pos = CalculatePosition(index);
|
||||
@ -119,24 +176,67 @@ namespace AlicizaX.UI
|
||||
viewHolder.RectTransform.anchoredPosition3D = position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算内容总大小(抽象方法,由子类实现)
|
||||
/// </summary>
|
||||
/// <returns>内容的总尺寸</returns>
|
||||
public abstract Vector2 CalculateContentSize();
|
||||
|
||||
/// <summary>
|
||||
/// 计算指定索引的 ViewHolder 位置(抽象方法,由子类实现)
|
||||
/// </summary>
|
||||
/// <param name="index">数据索引</param>
|
||||
/// <returns>ViewHolder 的位置</returns>
|
||||
public abstract Vector2 CalculatePosition(int index);
|
||||
|
||||
/// <summary>
|
||||
/// 计算内容偏移量(抽象方法,由子类实现)
|
||||
/// </summary>
|
||||
/// <returns>内容偏移量</returns>
|
||||
public abstract Vector2 CalculateContentOffset();
|
||||
|
||||
/// <summary>
|
||||
/// 计算视口偏移量(抽象方法,由子类实现)
|
||||
/// </summary>
|
||||
/// <returns>视口偏移量</returns>
|
||||
public abstract Vector2 CalculateViewportOffset();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前可见区域的起始索引(抽象方法,由子类实现)
|
||||
/// </summary>
|
||||
/// <returns>起始索引</returns>
|
||||
public abstract int GetStartIndex();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前可见区域的结束索引(抽象方法,由子类实现)
|
||||
/// </summary>
|
||||
/// <returns>结束索引</returns>
|
||||
public abstract int GetEndIndex();
|
||||
|
||||
/// <summary>
|
||||
/// 将数据索引转换为滚动位置(抽象方法,由子类实现)
|
||||
/// </summary>
|
||||
/// <param name="index">数据索引</param>
|
||||
/// <returns>对应的滚动位置</returns>
|
||||
public abstract float IndexToPosition(int index);
|
||||
|
||||
/// <summary>
|
||||
/// 将滚动位置转换为数据索引(抽象方法,由子类实现)
|
||||
/// </summary>
|
||||
/// <param name="position">滚动位置</param>
|
||||
/// <returns>对应的数据索引</returns>
|
||||
public abstract int PositionToIndex(float position);
|
||||
|
||||
/// <summary>
|
||||
/// 执行列表项动画(虚方法,子类可选择性重写)
|
||||
/// </summary>
|
||||
public virtual void DoItemAnimation() { }
|
||||
|
||||
/// <summary>
|
||||
/// 判断起始位置的 ViewHolder 是否完全可见
|
||||
/// </summary>
|
||||
/// <param name="index">数据索引</param>
|
||||
/// <returns>如果完全可见返回 true,否则返回 false</returns>
|
||||
public virtual bool IsFullVisibleStart(int index)
|
||||
{
|
||||
Vector2 vector2 = CalculatePosition(index);
|
||||
@ -144,6 +244,11 @@ namespace AlicizaX.UI
|
||||
return position + GetOffset() >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断起始位置的 ViewHolder 是否完全不可见
|
||||
/// </summary>
|
||||
/// <param name="index">数据索引</param>
|
||||
/// <returns>如果完全不可见返回 true,否则返回 false</returns>
|
||||
public virtual bool IsFullInvisibleStart(int index)
|
||||
{
|
||||
Vector2 vector2 = CalculatePosition(index + unit);
|
||||
@ -151,6 +256,11 @@ namespace AlicizaX.UI
|
||||
return position + GetOffset() < 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断结束位置的 ViewHolder 是否完全可见
|
||||
/// </summary>
|
||||
/// <param name="index">数据索引</param>
|
||||
/// <returns>如果完全可见返回 true,否则返回 false</returns>
|
||||
public virtual bool IsFullVisibleEnd(int index)
|
||||
{
|
||||
Vector2 vector2 = CalculatePosition(index + unit);
|
||||
@ -159,6 +269,11 @@ namespace AlicizaX.UI
|
||||
return position + GetOffset() <= viewLength;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断结束位置的 ViewHolder 是否完全不可见
|
||||
/// </summary>
|
||||
/// <param name="index">数据索引</param>
|
||||
/// <returns>如果完全不可见返回 true,否则返回 false</returns>
|
||||
public virtual bool IsFullInvisibleEnd(int index)
|
||||
{
|
||||
Vector2 vector2 = CalculatePosition(index);
|
||||
@ -167,6 +282,11 @@ namespace AlicizaX.UI
|
||||
return position + GetOffset() > viewLength;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断指定索引的 ViewHolder 是否可见(部分或完全)
|
||||
/// </summary>
|
||||
/// <param name="index">数据索引</param>
|
||||
/// <returns>如果可见返回 true,否则返回 false</returns>
|
||||
public virtual bool IsVisible(int index)
|
||||
{
|
||||
float position, viewLength;
|
||||
@ -189,6 +309,11 @@ namespace AlicizaX.UI
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取适配内容大小
|
||||
/// 根据对齐方式计算实际显示的内容长度
|
||||
/// </summary>
|
||||
/// <returns>适配后的内容大小</returns>
|
||||
protected virtual float GetFitContentSize()
|
||||
{
|
||||
float len;
|
||||
@ -203,23 +328,40 @@ namespace AlicizaX.UI
|
||||
return len;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取偏移量
|
||||
/// 计算内容偏移和视口偏移的组合值
|
||||
/// </summary>
|
||||
/// <returns>总偏移量</returns>
|
||||
protected virtual float GetOffset()
|
||||
{
|
||||
return direction == Direction.Vertical ? -contentOffset.y + viewportOffset.y : -contentOffset.x + viewportOffset.x;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 滚动方向枚举
|
||||
/// </summary>
|
||||
public enum Direction
|
||||
{
|
||||
/// <summary>垂直滚动</summary>
|
||||
Vertical = 0,
|
||||
/// <summary>水平滚动</summary>
|
||||
Horizontal = 1,
|
||||
/// <summary>自定义滚动</summary>
|
||||
Custom = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对齐方式枚举
|
||||
/// </summary>
|
||||
public enum Alignment
|
||||
{
|
||||
/// <summary>左对齐</summary>
|
||||
Left,
|
||||
/// <summary>居中对齐</summary>
|
||||
Center,
|
||||
/// <summary>顶部对齐</summary>
|
||||
Top
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,11 @@ using UnityEngine.UI;
|
||||
|
||||
namespace AlicizaX.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// RecyclerView 核心组件,用于高效显示大量列表数据
|
||||
/// 通过视图回收和复用机制,只渲染可见区域的项目,大幅提升性能
|
||||
/// 支持垂直/水平滚动、网格布局、循环滚动等多种布局模式
|
||||
/// </summary>
|
||||
public class RecyclerView : MonoBehaviour
|
||||
{
|
||||
#region Serialized Fields - Layout Settings
|
||||
@ -57,24 +62,36 @@ namespace AlicizaX.UI
|
||||
|
||||
#region Public Properties - Layout Settings
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置列表的滚动方向(垂直、水平或自定义)
|
||||
/// </summary>
|
||||
public Direction Direction
|
||||
{
|
||||
get => direction;
|
||||
set => direction = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置列表项的对齐方式(起始、居中或结束)
|
||||
/// </summary>
|
||||
public Alignment Alignment
|
||||
{
|
||||
get => alignment;
|
||||
set => alignment = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置列表项之间的间距(X轴和Y轴)
|
||||
/// </summary>
|
||||
public Vector2 Spacing
|
||||
{
|
||||
get => spacing;
|
||||
set => spacing = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置列表内容的内边距(X轴和Y轴)
|
||||
/// </summary>
|
||||
public Vector2 Padding
|
||||
{
|
||||
get => padding;
|
||||
@ -85,6 +102,10 @@ namespace AlicizaX.UI
|
||||
|
||||
#region Public Properties - Scroll Settings
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否启用滚动功能
|
||||
/// 启用时会激活 Scroller 组件并显示滚动条(如果配置了)
|
||||
/// </summary>
|
||||
public bool Scroll
|
||||
{
|
||||
get => scroll;
|
||||
@ -116,6 +137,11 @@ namespace AlicizaX.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否启用吸附功能
|
||||
/// 启用时滚动停止后会自动对齐到最近的列表项
|
||||
/// 注意:此功能依赖于 Scroll 属性,只有在滚动启用时才生效
|
||||
/// </summary>
|
||||
public bool Snap
|
||||
{
|
||||
get => snap;
|
||||
@ -136,6 +162,10 @@ namespace AlicizaX.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置滚动速度(范围:1-50)
|
||||
/// 值越大,滚动响应越快
|
||||
/// </summary>
|
||||
public float ScrollSpeed
|
||||
{
|
||||
get => scrollSpeed;
|
||||
@ -151,6 +181,10 @@ namespace AlicizaX.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置鼠标滚轮的滚动速度(范围:10-50)
|
||||
/// 值越大,滚轮滚动的距离越大
|
||||
/// </summary>
|
||||
public float WheelSpeed
|
||||
{
|
||||
get => wheelSpeed;
|
||||
@ -171,12 +205,20 @@ namespace AlicizaX.UI
|
||||
|
||||
#region Public Properties - Components
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 ViewHolder 模板数组
|
||||
/// 用于创建和复用列表项视图
|
||||
/// </summary>
|
||||
public ViewHolder[] Templates
|
||||
{
|
||||
get => templates;
|
||||
set => templates = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取内容容器的 RectTransform
|
||||
/// 所有列表项都会作为此容器的子对象
|
||||
/// </summary>
|
||||
public RectTransform Content
|
||||
{
|
||||
get
|
||||
@ -190,10 +232,21 @@ namespace AlicizaX.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取滚动条组件
|
||||
/// </summary>
|
||||
public Scrollbar Scrollbar => scrollbar;
|
||||
|
||||
/// <summary>
|
||||
/// 获取滚动控制器组件
|
||||
/// </summary>
|
||||
public Scroller Scroller => scroller;
|
||||
|
||||
/// <summary>
|
||||
/// 获取视图提供器
|
||||
/// 负责创建、回收和管理 ViewHolder 实例
|
||||
/// 根据模板数量自动选择 SimpleViewProvider 或 MixedViewProvider
|
||||
/// </summary>
|
||||
public ViewProvider ViewProvider
|
||||
{
|
||||
get
|
||||
@ -213,8 +266,15 @@ namespace AlicizaX.UI
|
||||
|
||||
#region Public Properties - State
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前绑定的适配器
|
||||
/// 适配器负责提供数据和创建 ViewHolder
|
||||
/// </summary>
|
||||
public IAdapter RecyclerViewAdapter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前显示的列表项索引
|
||||
/// </summary>
|
||||
public int CurrentIndex
|
||||
{
|
||||
get => currentIndex;
|
||||
@ -225,7 +285,15 @@ namespace AlicizaX.UI
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// 当前索引改变时触发的事件
|
||||
/// 参数为新的索引值
|
||||
/// </summary>
|
||||
public Action<int> OnIndexChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 滚动位置改变时触发的事件
|
||||
/// </summary>
|
||||
public Action OnScrollValueChanged;
|
||||
|
||||
#endregion
|
||||
@ -287,6 +355,10 @@ namespace AlicizaX.UI
|
||||
|
||||
#region Public Methods - Setup
|
||||
|
||||
/// <summary>
|
||||
/// 设置数据适配器并初始化布局管理器
|
||||
/// </summary>
|
||||
/// <param name="adapter">要绑定的适配器实例</param>
|
||||
public void SetAdapter(IAdapter adapter)
|
||||
{
|
||||
if (adapter == null)
|
||||
@ -308,6 +380,10 @@ namespace AlicizaX.UI
|
||||
layoutManager.Padding = padding;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置列表状态
|
||||
/// 清空所有 ViewHolder 并将滚动位置重置为起始位置
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
viewProvider?.Reset();
|
||||
@ -327,6 +403,10 @@ namespace AlicizaX.UI
|
||||
|
||||
#region Public Methods - Layout
|
||||
|
||||
/// <summary>
|
||||
/// 刷新列表显示
|
||||
/// 清空当前所有 ViewHolder 并根据当前滚动位置重新创建可见项
|
||||
/// </summary>
|
||||
public void Refresh()
|
||||
{
|
||||
ViewProvider.Clear();
|
||||
@ -342,6 +422,10 @@ namespace AlicizaX.UI
|
||||
layoutManager.DoItemAnimation();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 请求重新布局
|
||||
/// 重新计算内容大小、视口大小,并更新滚动条显示状态
|
||||
/// </summary>
|
||||
public void RequestLayout()
|
||||
{
|
||||
layoutManager.SetContentSize();
|
||||
@ -359,11 +443,20 @@ namespace AlicizaX.UI
|
||||
|
||||
#region Public Methods - Scrolling
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前的滚动位置
|
||||
/// </summary>
|
||||
/// <returns>当前滚动位置值,如果没有 Scroller 则返回 0</returns>
|
||||
public float GetScrollPosition()
|
||||
{
|
||||
return scroller != null ? scroller.Position : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 滚动到指定索引的列表项
|
||||
/// </summary>
|
||||
/// <param name="index">目标列表项的索引</param>
|
||||
/// <param name="smooth">是否使用平滑滚动动画</param>
|
||||
public void ScrollTo(int index, bool smooth = false)
|
||||
{
|
||||
if (!scroll || scroller == null) return;
|
||||
@ -378,6 +471,14 @@ namespace AlicizaX.UI
|
||||
UpdateCurrentIndex(index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 滚动到指定索引的列表项,并使用指定的对齐方式
|
||||
/// </summary>
|
||||
/// <param name="index">目标列表项的索引</param>
|
||||
/// <param name="alignment">对齐方式(起始、居中或结束)</param>
|
||||
/// <param name="offset">额外的偏移量</param>
|
||||
/// <param name="smooth">是否使用平滑滚动动画</param>
|
||||
/// <param name="duration">滚动动画持续时间(秒)</param>
|
||||
public void ScrollToWithAlignment(int index, ScrollAlignment alignment, float offset = 0f, bool smooth = false, float duration = 0.3f)
|
||||
{
|
||||
if (!scroll || scroller == null) return;
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
namespace AlicizaX.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines how an item should be aligned when scrolling to it
|
||||
/// 定义滚动到列表项时的对齐方式
|
||||
/// </summary>
|
||||
public enum ScrollAlignment
|
||||
{
|
||||
/// <summary>
|
||||
/// Align item to the top/left of the viewport
|
||||
/// 将列表项对齐到视口的顶部/左侧
|
||||
/// </summary>
|
||||
Start,
|
||||
|
||||
/// <summary>
|
||||
/// Align item to the center of the viewport
|
||||
/// 将列表项对齐到视口的中心
|
||||
/// </summary>
|
||||
Center,
|
||||
|
||||
/// <summary>
|
||||
/// Align item to the bottom/right of the viewport
|
||||
/// 将列表项对齐到视口的底部/右侧
|
||||
/// </summary>
|
||||
End
|
||||
}
|
||||
|
||||
@ -2,14 +2,37 @@ using UnityEngine.Events;
|
||||
|
||||
namespace AlicizaX.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 滚动控制器接口
|
||||
/// 定义滚动行为的基本契约
|
||||
/// </summary>
|
||||
public interface IScroller
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置当前滚动位置
|
||||
/// </summary>
|
||||
float Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 滚动到指定位置
|
||||
/// </summary>
|
||||
/// <param name="position">目标位置</param>
|
||||
/// <param name="smooth">是否使用平滑滚动</param>
|
||||
void ScrollTo(float position, bool smooth = false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 滚动位置改变事件
|
||||
/// </summary>
|
||||
public class ScrollerEvent : UnityEvent<float> { }
|
||||
|
||||
/// <summary>
|
||||
/// 滚动停止事件
|
||||
/// </summary>
|
||||
public class MoveStopEvent : UnityEvent { }
|
||||
|
||||
/// <summary>
|
||||
/// 拖拽状态改变事件
|
||||
/// </summary>
|
||||
public class DraggingEvent : UnityEvent<bool> { }
|
||||
}
|
||||
|
||||
@ -3,14 +3,29 @@ using System.Collections.Generic;
|
||||
|
||||
namespace AlicizaX.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// UGList 基类
|
||||
/// 提供简化的列表操作接口,封装 RecyclerView 和 Adapter 的交互
|
||||
/// </summary>
|
||||
/// <typeparam name="TData">数据类型</typeparam>
|
||||
/// <typeparam name="TAdapter">适配器类型</typeparam>
|
||||
public abstract class UGListBase<TData, TAdapter> where TAdapter : Adapter<TData> where TData : ISimpleViewData
|
||||
{
|
||||
protected readonly RecyclerView _recyclerView;
|
||||
|
||||
protected readonly TAdapter _adapter;
|
||||
|
||||
/// <summary>
|
||||
/// 获取关联的 RecyclerView 实例
|
||||
/// </summary>
|
||||
public RecyclerView RecyclerView => _recyclerView;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="recyclerView">RecyclerView 实例</param>
|
||||
/// <param name="adapter">适配器实例</param>
|
||||
/// <param name="onItemClick">列表项点击回调</param>
|
||||
public UGListBase(RecyclerView recyclerView, TAdapter adapter, Action<TData> onItemClick = null)
|
||||
{
|
||||
_recyclerView = recyclerView;
|
||||
@ -27,10 +42,17 @@ namespace AlicizaX.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取适配器实例
|
||||
/// </summary>
|
||||
public TAdapter Adapter => _adapter;
|
||||
|
||||
private List<TData> _datas;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置数据列表
|
||||
/// 设置时会自动更新适配器
|
||||
/// </summary>
|
||||
public List<TData> Data
|
||||
{
|
||||
get => _datas;
|
||||
@ -42,49 +64,106 @@ namespace AlicizaX.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通用列表类
|
||||
/// 用于显示简单的单一类型数据列表
|
||||
/// </summary>
|
||||
/// <typeparam name="TData">数据类型,必须实现 ISimpleViewData</typeparam>
|
||||
public class UGList<TData> : UGListBase<TData, Adapter<TData>> where TData : ISimpleViewData
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="recyclerView">RecyclerView 实例</param>
|
||||
/// <param name="onItemClick">列表项点击回调</param>
|
||||
public UGList(RecyclerView recyclerView, Action<TData> onItemClick = null)
|
||||
: base(recyclerView, new Adapter<TData>(recyclerView), onItemClick)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分组列表类
|
||||
/// 用于显示带有分组头的列表数据
|
||||
/// </summary>
|
||||
/// <typeparam name="TData">数据类型,必须实现 IGroupViewData</typeparam>
|
||||
public class UGGroupList<TData> : UGListBase<TData, GroupAdapter<TData>> where TData : class, IGroupViewData, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="recyclerView">RecyclerView 实例</param>
|
||||
/// <param name="groupViewName">分组头视图名称</param>
|
||||
/// <param name="onItemClick">列表项点击回调</param>
|
||||
public UGGroupList(RecyclerView recyclerView, string groupViewName, Action<TData> onItemClick = null)
|
||||
: base(recyclerView, new GroupAdapter<TData>(recyclerView, groupViewName), onItemClick)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 循环列表类
|
||||
/// 用于实现无限循环滚动的列表
|
||||
/// </summary>
|
||||
/// <typeparam name="TData">数据类型,必须实现 ISimpleViewData</typeparam>
|
||||
public class UGLoopList<TData> : UGListBase<TData, LoopAdapter<TData>> where TData : ISimpleViewData, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="recyclerView">RecyclerView 实例</param>
|
||||
/// <param name="onItemClick">列表项点击回调</param>
|
||||
public UGLoopList(RecyclerView recyclerView, Action<TData> onItemClick = null)
|
||||
: base(recyclerView, new LoopAdapter<TData>(recyclerView), onItemClick)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 混合列表类
|
||||
/// 用于显示多种不同类型的列表项
|
||||
/// </summary>
|
||||
/// <typeparam name="TData">数据类型,必须实现 IMixedViewData</typeparam>
|
||||
public class UGMixedList<TData> : UGListBase<TData, MixedAdapter<TData>> where TData : IMixedViewData
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="recyclerView">RecyclerView 实例</param>
|
||||
/// <param name="onItemClick">列表项点击回调</param>
|
||||
public UGMixedList(RecyclerView recyclerView, Action<TData> onItemClick = null)
|
||||
: base(recyclerView, new MixedAdapter<TData>(recyclerView), onItemClick)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UGList 创建辅助类
|
||||
/// 提供便捷的静态方法来创建各种类型的列表
|
||||
/// </summary>
|
||||
public static class UGListCreateHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建通用列表
|
||||
/// </summary>
|
||||
public static UGList<TData> Create<TData>(RecyclerView recyclerView, Action<TData> onItemClick = null) where TData : ISimpleViewData
|
||||
=> new UGList<TData>(recyclerView, onItemClick);
|
||||
|
||||
/// <summary>
|
||||
/// 创建分组列表
|
||||
/// </summary>
|
||||
public static UGGroupList<TData> CreateGroup<TData>(RecyclerView recyclerView, string groupViewName, Action<TData> onItemClick = null) where TData : class, IGroupViewData, new()
|
||||
=> new UGGroupList<TData>(recyclerView, groupViewName, onItemClick);
|
||||
|
||||
/// <summary>
|
||||
/// 创建循环列表
|
||||
/// </summary>
|
||||
public static UGLoopList<TData> CreateLoop<TData>(RecyclerView recyclerView, Action<TData> onItemClick = null) where TData : ISimpleViewData, new()
|
||||
=> new UGLoopList<TData>(recyclerView, onItemClick);
|
||||
|
||||
/// <summary>
|
||||
/// 创建混合列表
|
||||
/// </summary>
|
||||
public static UGMixedList<TData> CreateMixed<TData>(RecyclerView recyclerView, Action<TData> onItemClick = null) where TData : IMixedViewData
|
||||
=> new UGMixedList<TData>(recyclerView, onItemClick);
|
||||
}
|
||||
|
||||
@ -4,24 +4,25 @@ using UnityEngine;
|
||||
namespace AlicizaX.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for UGList to provide enhanced scrolling functionality
|
||||
/// UGList 扩展方法类
|
||||
/// 提供增强的滚动功能
|
||||
/// </summary>
|
||||
public static class UGListExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable debug logging for ScrollTo operations
|
||||
/// 启用 ScrollTo 操作的调试日志
|
||||
/// </summary>
|
||||
public static bool DebugScrollTo { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Scrolls to a specific item with alignment and animation options
|
||||
/// 滚动到指定的列表项,支持对齐方式和动画选项
|
||||
/// </summary>
|
||||
/// <param name="ugList">The UGList instance</param>
|
||||
/// <param name="index">The index of the item to scroll to</param>
|
||||
/// <param name="alignment">How to align the item in the viewport (Start, Center, or End)</param>
|
||||
/// <param name="offset">Additional offset in pixels to apply after alignment</param>
|
||||
/// <param name="smooth">Whether to animate the scroll</param>
|
||||
/// <param name="duration">Animation duration in seconds (only used when smooth is true)</param>
|
||||
/// <param name="ugList">UGList 实例</param>
|
||||
/// <param name="index">要滚动到的列表项索引</param>
|
||||
/// <param name="alignment">列表项在视口中的对齐方式(起始、居中或结束)</param>
|
||||
/// <param name="offset">对齐后额外应用的偏移量(像素)</param>
|
||||
/// <param name="smooth">是否使用动画滚动</param>
|
||||
/// <param name="duration">动画持续时间(秒),仅在 smooth 为 true 时使用</param>
|
||||
public static void ScrollTo<TData, TAdapter>(
|
||||
this UGListBase<TData, TAdapter> ugList,
|
||||
int index,
|
||||
@ -47,7 +48,7 @@ namespace AlicizaX.UI
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scrolls to a specific item and aligns it at the start (top/left) of the viewport
|
||||
/// 滚动到指定的列表项并将其对齐到视口的起始位置(顶部/左侧)
|
||||
/// </summary>
|
||||
public static void ScrollToStart<TData, TAdapter>(
|
||||
this UGListBase<TData, TAdapter> ugList,
|
||||
@ -62,7 +63,7 @@ namespace AlicizaX.UI
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scrolls to a specific item and aligns it at the center of the viewport
|
||||
/// 滚动到指定的列表项并将其对齐到视口的中心位置
|
||||
/// </summary>
|
||||
public static void ScrollToCenter<TData, TAdapter>(
|
||||
this UGListBase<TData, TAdapter> ugList,
|
||||
@ -77,7 +78,7 @@ namespace AlicizaX.UI
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scrolls to a specific item and aligns it at the end (bottom/right) of the viewport
|
||||
/// 滚动到指定的列表项并将其对齐到视口的结束位置(底部/右侧)
|
||||
/// </summary>
|
||||
public static void ScrollToEnd<TData, TAdapter>(
|
||||
this UGListBase<TData, TAdapter> ugList,
|
||||
|
||||
@ -5,10 +5,16 @@ using UnityEngine.UI;
|
||||
|
||||
namespace AlicizaX.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 视图持有者基类,用于缓存和复用列表项视图
|
||||
/// </summary>
|
||||
public abstract class ViewHolder : MonoBehaviour
|
||||
{
|
||||
private RectTransform rectTransform;
|
||||
|
||||
/// <summary>
|
||||
/// 获取 RectTransform 组件
|
||||
/// </summary>
|
||||
public RectTransform RectTransform
|
||||
{
|
||||
get
|
||||
@ -23,24 +29,55 @@ namespace AlicizaX.UI
|
||||
private set { rectTransform = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 视图名称,用于区分不同类型的视图
|
||||
/// </summary>
|
||||
public string Name { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前绑定的数据索引
|
||||
/// </summary>
|
||||
public int Index { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// 选中状态
|
||||
/// </summary>
|
||||
public bool ChoiseState { private set; get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取视图的尺寸
|
||||
/// </summary>
|
||||
public Vector2 SizeDelta => RectTransform.sizeDelta;
|
||||
|
||||
private IButton _button;
|
||||
|
||||
/// <summary>
|
||||
/// 视图首次创建时调用
|
||||
/// </summary>
|
||||
protected internal virtual void OnStart()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 视图被回收到对象池时调用
|
||||
/// </summary>
|
||||
protected internal virtual void OnRecycled()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绑定视图数据(抽象方法,子类必须实现)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数据类型</typeparam>
|
||||
/// <param name="data">要绑定的数据</param>
|
||||
public abstract void BindViewData<T>(T data);
|
||||
|
||||
/// <summary>
|
||||
/// 绑定列表项点击事件
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数据类型</typeparam>
|
||||
/// <param name="data">数据对象</param>
|
||||
/// <param name="action">点击回调</param>
|
||||
protected internal virtual void BindItemClick<T>(T data, Action<T> action)
|
||||
{
|
||||
if (_button is null && !TryGetComponent(out _button))
|
||||
@ -53,6 +90,10 @@ namespace AlicizaX.UI
|
||||
_button.onClick.AddListener(() => action?.Invoke(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绑定选中状态
|
||||
/// </summary>
|
||||
/// <param name="state">是否选中</param>
|
||||
protected internal void BindChoiceState(bool state)
|
||||
{
|
||||
if (ChoiseState != state)
|
||||
@ -62,6 +103,10 @@ namespace AlicizaX.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 选中状态改变时的回调(可在子类中重写)
|
||||
/// </summary>
|
||||
/// <param name="state">是否选中</param>
|
||||
protected internal virtual void OnBindChoiceState(bool state)
|
||||
{
|
||||
}
|
||||
|
||||
@ -5,35 +5,78 @@ namespace AlicizaX.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 提供和管理 ViewHolder
|
||||
/// 负责 ViewHolder 的创建、回收和复用
|
||||
/// </summary>
|
||||
public abstract class ViewProvider
|
||||
{
|
||||
private readonly List<ViewHolder> viewHolders = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置数据适配器
|
||||
/// </summary>
|
||||
public IAdapter Adapter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置布局管理器
|
||||
/// </summary>
|
||||
public LayoutManager LayoutManager { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前所有活动的 ViewHolder 列表
|
||||
/// </summary>
|
||||
public List<ViewHolder> ViewHolders => viewHolders;
|
||||
|
||||
protected RecyclerView recyclerView;
|
||||
protected ViewHolder[] templates;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="recyclerView">关联的 RecyclerView 实例</param>
|
||||
/// <param name="templates">ViewHolder 模板数组</param>
|
||||
public ViewProvider(RecyclerView recyclerView, ViewHolder[] templates)
|
||||
{
|
||||
this.recyclerView = recyclerView;
|
||||
this.templates = templates;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据视图名称获取对应的模板(抽象方法,由子类实现)
|
||||
/// </summary>
|
||||
/// <param name="viewName">视图名称</param>
|
||||
/// <returns>对应的 ViewHolder 模板</returns>
|
||||
public abstract ViewHolder GetTemplate(string viewName);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有模板(抽象方法,由子类实现)
|
||||
/// </summary>
|
||||
/// <returns>所有 ViewHolder 模板数组</returns>
|
||||
public abstract ViewHolder[] GetTemplates();
|
||||
|
||||
/// <summary>
|
||||
/// 从对象池中分配一个 ViewHolder(抽象方法,由子类实现)
|
||||
/// </summary>
|
||||
/// <param name="viewName">视图名称</param>
|
||||
/// <returns>分配的 ViewHolder 实例</returns>
|
||||
public abstract ViewHolder Allocate(string viewName);
|
||||
|
||||
/// <summary>
|
||||
/// 将 ViewHolder 回收到对象池(抽象方法,由子类实现)
|
||||
/// </summary>
|
||||
/// <param name="viewName">视图名称</param>
|
||||
/// <param name="viewHolder">要回收的 ViewHolder</param>
|
||||
public abstract void Free(string viewName, ViewHolder viewHolder);
|
||||
|
||||
/// <summary>
|
||||
/// 重置 ViewProvider 状态(抽象方法,由子类实现)
|
||||
/// </summary>
|
||||
public abstract void Reset();
|
||||
|
||||
/// <summary>
|
||||
/// 创建指定索引的 ViewHolder
|
||||
/// 从对象池中获取或创建新的 ViewHolder,并进行布局和数据绑定
|
||||
/// </summary>
|
||||
/// <param name="index">数据索引</param>
|
||||
public void CreateViewHolder(int index)
|
||||
{
|
||||
for (int i = index; i < index + LayoutManager.Unit; i++)
|
||||
@ -51,6 +94,11 @@ namespace AlicizaX.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除指定索引的 ViewHolder
|
||||
/// 将 ViewHolder 从活动列表中移除并回收到对象池
|
||||
/// </summary>
|
||||
/// <param name="index">数据索引</param>
|
||||
public void RemoveViewHolder(int index)
|
||||
{
|
||||
for (int i = index; i < index + LayoutManager.Unit; i++)
|
||||
@ -104,6 +152,10 @@ namespace AlicizaX.UI
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有 ViewHolder
|
||||
/// 将所有活动的 ViewHolder 回收到对象池并清空列表
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var viewHolder in viewHolders)
|
||||
@ -125,6 +177,10 @@ namespace AlicizaX.UI
|
||||
return size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取数据项总数
|
||||
/// </summary>
|
||||
/// <returns>数据项总数,如果没有适配器则返回 0</returns>
|
||||
public int GetItemCount()
|
||||
{
|
||||
return Adapter == null ? 0 : Adapter.GetItemCount();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user