2026-03-27 18:38:29 +08:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Reflection;
|
|
|
|
|
|
|
|
|
|
namespace AlicizaX.UI
|
|
|
|
|
{
|
|
|
|
|
public interface IItemRender
|
|
|
|
|
{
|
|
|
|
|
void Bind(object data, int index, Action defaultClickAction);
|
|
|
|
|
|
|
|
|
|
void UpdateSelection(bool selected);
|
|
|
|
|
|
|
|
|
|
void Unbind();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 15:40:00 +08:00
|
|
|
internal interface IItemRenderInitializer
|
|
|
|
|
{
|
|
|
|
|
void Reset(ViewHolder viewHolder);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 18:38:29 +08:00
|
|
|
public abstract class ItemRender<TData, THolder> : IItemRender
|
2026-03-30 15:40:00 +08:00
|
|
|
, IItemRenderInitializer
|
2026-03-27 18:38:29 +08:00
|
|
|
where THolder : ViewHolder
|
|
|
|
|
{
|
|
|
|
|
private Action defaultClickAction;
|
2026-03-30 15:40:00 +08:00
|
|
|
protected THolder Holder { get; private set; }
|
2026-03-27 18:38:29 +08:00
|
|
|
|
|
|
|
|
protected TData CurrentData { get; private set; }
|
|
|
|
|
|
2026-03-30 15:40:00 +08:00
|
|
|
protected int CurrentIndex { get; private set; } = -1;
|
|
|
|
|
|
|
|
|
|
protected bool IsSelected { get; private set; }
|
2026-03-27 18:38:29 +08:00
|
|
|
|
|
|
|
|
public void Bind(object data, int index, Action defaultClickAction)
|
|
|
|
|
{
|
2026-03-30 15:40:00 +08:00
|
|
|
EnsureHolder();
|
|
|
|
|
|
2026-03-27 18:38:29 +08:00
|
|
|
if (data is not TData itemData)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidCastException(
|
|
|
|
|
$"ItemRender '{GetType().Name}' expected data '{typeof(TData).Name}', but got '{data?.GetType().Name ?? "null"}'.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CurrentData = itemData;
|
|
|
|
|
CurrentIndex = index;
|
|
|
|
|
this.defaultClickAction = defaultClickAction;
|
|
|
|
|
Holder.SetInteractionCallbacks(HandleClick, HandlePointerEnter, HandlePointerExit);
|
|
|
|
|
Bind(itemData, index);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void UpdateSelection(bool selected)
|
|
|
|
|
{
|
2026-03-30 15:40:00 +08:00
|
|
|
EnsureHolder();
|
|
|
|
|
IsSelected = selected;
|
2026-03-27 18:38:29 +08:00
|
|
|
OnSelectionChanged(selected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Unbind()
|
|
|
|
|
{
|
2026-03-30 15:40:00 +08:00
|
|
|
ResetState();
|
2026-03-27 18:38:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected abstract void Bind(TData data, int index);
|
|
|
|
|
|
2026-03-30 15:40:00 +08:00
|
|
|
protected virtual void OnHolderChanged()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 18:38:29 +08:00
|
|
|
protected virtual void OnSelectionChanged(bool selected)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected virtual void OnClear()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected virtual void OnClick()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected virtual void OnPointerEnter()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected virtual void OnPointerExit()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void HandleClick()
|
|
|
|
|
{
|
|
|
|
|
defaultClickAction?.Invoke();
|
|
|
|
|
OnClick();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void HandlePointerEnter()
|
|
|
|
|
{
|
|
|
|
|
OnPointerEnter();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void HandlePointerExit()
|
|
|
|
|
{
|
|
|
|
|
OnPointerExit();
|
|
|
|
|
}
|
2026-03-30 15:40:00 +08:00
|
|
|
|
|
|
|
|
void IItemRenderInitializer.Reset(ViewHolder viewHolder)
|
|
|
|
|
{
|
|
|
|
|
if (viewHolder == null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(viewHolder));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (viewHolder is not THolder holder)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"RecyclerView item render '{GetType().FullName}' expects holder '{typeof(THolder).FullName}', but got '{viewHolder.GetType().FullName}'.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ResetState();
|
|
|
|
|
Holder = holder;
|
|
|
|
|
OnHolderChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EnsureHolder()
|
|
|
|
|
{
|
|
|
|
|
if (Holder == null)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"RecyclerView item render '{GetType().FullName}' has not been initialized with a holder.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ResetState()
|
|
|
|
|
{
|
|
|
|
|
if (Holder != null)
|
|
|
|
|
{
|
|
|
|
|
if (IsSelected)
|
|
|
|
|
{
|
|
|
|
|
IsSelected = false;
|
|
|
|
|
OnSelectionChanged(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OnClear();
|
|
|
|
|
Holder.ClearInteractionCallbacks();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CurrentData = default;
|
|
|
|
|
CurrentIndex = -1;
|
|
|
|
|
IsSelected = false;
|
|
|
|
|
defaultClickAction = null;
|
|
|
|
|
}
|
2026-03-27 18:38:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static class ItemRenderResolver
|
|
|
|
|
{
|
|
|
|
|
internal sealed class ItemRenderDefinition
|
|
|
|
|
{
|
2026-03-30 15:40:00 +08:00
|
|
|
public ItemRenderDefinition(Type itemRenderType, Type holderType)
|
2026-03-27 18:38:29 +08:00
|
|
|
{
|
|
|
|
|
ItemRenderType = itemRenderType;
|
|
|
|
|
HolderType = holderType;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Type ItemRenderType { get; }
|
|
|
|
|
|
|
|
|
|
public Type HolderType { get; }
|
|
|
|
|
|
|
|
|
|
public IItemRender Create(ViewHolder viewHolder)
|
|
|
|
|
{
|
|
|
|
|
if (viewHolder == null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(viewHolder));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!HolderType.IsInstanceOfType(viewHolder))
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"RecyclerView item render '{ItemRenderType.FullName}' expects holder '{HolderType.FullName}', but got '{viewHolder.GetType().FullName}'.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 15:40:00 +08:00
|
|
|
if (Activator.CreateInstance(ItemRenderType, true) is not IItemRender itemRender)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"RecyclerView item render '{ItemRenderType.FullName}' could not be created.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (itemRender is not IItemRenderInitializer initializer)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"RecyclerView item render '{ItemRenderType.FullName}' must inherit from ItemRender<TData, THolder>.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initializer.Reset(viewHolder);
|
|
|
|
|
return itemRender;
|
2026-03-27 18:38:29 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static readonly Dictionary<Type, ItemRenderDefinition> Definitions = new();
|
|
|
|
|
|
|
|
|
|
public static ItemRenderDefinition GetOrCreate(Type itemRenderType)
|
|
|
|
|
{
|
|
|
|
|
if (itemRenderType == null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(itemRenderType));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Definitions.TryGetValue(itemRenderType, out ItemRenderDefinition definition))
|
|
|
|
|
{
|
|
|
|
|
return definition;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
definition = CreateDefinition(itemRenderType);
|
|
|
|
|
Definitions[itemRenderType] = definition;
|
|
|
|
|
return definition;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static ItemRenderDefinition CreateDefinition(Type itemRenderType)
|
|
|
|
|
{
|
|
|
|
|
if (itemRenderType.IsAbstract ||
|
|
|
|
|
itemRenderType.IsInterface ||
|
|
|
|
|
itemRenderType.ContainsGenericParameters ||
|
|
|
|
|
!typeof(IItemRender).IsAssignableFrom(itemRenderType))
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"RecyclerView item render type '{itemRenderType.FullName}' is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!TryGetHolderType(itemRenderType, out Type holderType))
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"RecyclerView item render '{itemRenderType.FullName}' must inherit from ItemRender<TData, THolder>.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ConstructorInfo constructor = itemRenderType.GetConstructor(
|
|
|
|
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
|
|
|
|
|
null,
|
2026-03-30 15:40:00 +08:00
|
|
|
Type.EmptyTypes,
|
2026-03-27 18:38:29 +08:00
|
|
|
null);
|
|
|
|
|
|
|
|
|
|
if (constructor == null)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
2026-03-30 15:40:00 +08:00
|
|
|
$"RecyclerView item render '{itemRenderType.FullName}' must have a parameterless constructor.");
|
2026-03-27 18:38:29 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 15:40:00 +08:00
|
|
|
return new ItemRenderDefinition(itemRenderType, holderType);
|
2026-03-27 18:38:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool TryGetHolderType(Type itemRenderType, out Type holderType)
|
|
|
|
|
{
|
|
|
|
|
for (Type current = itemRenderType; current != null && current != typeof(object); current = current.BaseType)
|
|
|
|
|
{
|
|
|
|
|
if (current.IsGenericType &&
|
|
|
|
|
current.GetGenericTypeDefinition() == typeof(ItemRender<,>))
|
|
|
|
|
{
|
|
|
|
|
Type[] arguments = current.GetGenericArguments();
|
|
|
|
|
holderType = arguments[1];
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
holderType = null;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|