579 lines
16 KiB
C#
579 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace AlicizaX.UI
|
|
{
|
|
internal interface IItemRenderCacheOwner
|
|
{
|
|
void ReleaseAllItemRenders();
|
|
}
|
|
|
|
public class Adapter<T> : IAdapter, IItemRenderCacheOwner where T : ISimpleViewData
|
|
{
|
|
private sealed class ItemRenderEntry
|
|
{
|
|
public ItemRenderEntry(string viewName, IItemRender itemRender)
|
|
{
|
|
ViewName = viewName;
|
|
ItemRender = itemRender;
|
|
}
|
|
|
|
public string ViewName { get; }
|
|
|
|
public IItemRender ItemRender { get; }
|
|
}
|
|
|
|
protected RecyclerView recyclerView;
|
|
protected List<T> list;
|
|
|
|
protected int choiceIndex = -1;
|
|
private readonly Dictionary<string, ItemRenderResolver.ItemRenderDefinition> itemRenderDefinitions = new(StringComparer.Ordinal);
|
|
private readonly Dictionary<ViewHolder, ItemRenderEntry> itemRenders = new();
|
|
private ItemRenderResolver.ItemRenderDefinition defaultItemRenderDefinition;
|
|
|
|
public int ChoiceIndex
|
|
{
|
|
get => choiceIndex;
|
|
set => SetChoiceIndex(value);
|
|
}
|
|
|
|
internal Action<int> OnChoiceIndexChanged;
|
|
|
|
public Adapter(RecyclerView recyclerView) : this(recyclerView, new List<T>())
|
|
{
|
|
}
|
|
|
|
public Adapter(RecyclerView recyclerView, List<T> list)
|
|
{
|
|
this.recyclerView = recyclerView;
|
|
this.list = list ?? new List<T>();
|
|
}
|
|
|
|
public virtual int GetItemCount()
|
|
{
|
|
return list == null ? 0 : list.Count;
|
|
}
|
|
|
|
public virtual int GetRealCount()
|
|
{
|
|
return GetItemCount();
|
|
}
|
|
|
|
public virtual string GetViewName(int index)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
public virtual void OnBindViewHolder(ViewHolder viewHolder, int index)
|
|
{
|
|
if (viewHolder == null) return;
|
|
if (!TryGetBindData(index, out var data)) return;
|
|
|
|
string viewName = GetViewName(index);
|
|
viewHolder.AdvanceBindingVersion();
|
|
viewHolder.DataIndex = index;
|
|
if (TryGetOrCreateItemRender(viewHolder, viewName, out var itemRender))
|
|
{
|
|
if (itemRender is ITypedItemRender<T> typedItemRender)
|
|
{
|
|
typedItemRender.BindData(data, index);
|
|
}
|
|
|
|
bool selected = index == choiceIndex;
|
|
itemRender.SyncSelection(selected);
|
|
return;
|
|
}
|
|
|
|
string resolvedViewName = string.IsNullOrEmpty(viewName) ? "<default>" : viewName;
|
|
throw new InvalidOperationException(
|
|
$"RecyclerView item render is missing for view '{resolvedViewName}'. Holder='{viewHolder.GetType().Name}', Adapter='{GetType().Name}'.");
|
|
}
|
|
|
|
public virtual void OnRecycleViewHolder(ViewHolder viewHolder)
|
|
{
|
|
if (viewHolder == null) return;
|
|
|
|
if (TryGetItemRender(viewHolder, out var itemRender))
|
|
{
|
|
itemRender.Unbind();
|
|
}
|
|
}
|
|
|
|
public virtual void NotifyDataChanged()
|
|
{
|
|
CoerceChoiceIndex();
|
|
recyclerView.RequestLayout();
|
|
recyclerView.Refresh();
|
|
}
|
|
|
|
public virtual void SetList(List<T> list)
|
|
{
|
|
this.list = list ?? new List<T>();
|
|
recyclerView.Reset();
|
|
NotifyDataChanged();
|
|
}
|
|
|
|
public virtual void NotifyItemChanged(int index, bool relayout = false)
|
|
{
|
|
if (index < 0 || index >= GetRealCount())
|
|
{
|
|
return;
|
|
}
|
|
|
|
CoerceChoiceIndex();
|
|
if (relayout)
|
|
{
|
|
recyclerView.RequestLayout();
|
|
recyclerView.Refresh();
|
|
return;
|
|
}
|
|
|
|
recyclerView.RebindVisibleDataIndex(index);
|
|
}
|
|
|
|
public virtual void NotifyItemRangeChanged(int index, int count, bool relayout = false)
|
|
{
|
|
if (count <= 0 || index < 0 || index >= GetRealCount())
|
|
{
|
|
return;
|
|
}
|
|
|
|
CoerceChoiceIndex();
|
|
if (relayout)
|
|
{
|
|
recyclerView.RequestLayout();
|
|
recyclerView.Refresh();
|
|
return;
|
|
}
|
|
|
|
recyclerView.RebindVisibleDataRange(index, count);
|
|
}
|
|
|
|
public virtual void NotifyItemInserted(int index)
|
|
{
|
|
CoerceChoiceIndex();
|
|
recyclerView.RequestLayout();
|
|
recyclerView.Refresh();
|
|
}
|
|
|
|
public virtual void NotifyItemRangeInserted(int index, int count)
|
|
{
|
|
if (count <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CoerceChoiceIndex();
|
|
recyclerView.RequestLayout();
|
|
recyclerView.Refresh();
|
|
}
|
|
|
|
public virtual void NotifyItemRemoved(int index)
|
|
{
|
|
CoerceChoiceIndex();
|
|
recyclerView.RequestLayout();
|
|
recyclerView.Refresh();
|
|
}
|
|
|
|
public virtual void NotifyItemRangeRemoved(int index, int count)
|
|
{
|
|
if (count <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CoerceChoiceIndex();
|
|
recyclerView.RequestLayout();
|
|
recyclerView.Refresh();
|
|
}
|
|
|
|
public void RegisterItemRender<TItemRender>(string viewName = "") where TItemRender : ItemRenderBase
|
|
{
|
|
RegisterItemRender(typeof(TItemRender), viewName);
|
|
}
|
|
|
|
public void RegisterItemRender(Type itemRenderType, string viewName = "")
|
|
{
|
|
var definition = ItemRenderResolver.GetOrCreate(itemRenderType);
|
|
|
|
if (string.IsNullOrEmpty(viewName))
|
|
{
|
|
ReleaseCachedItemRenders(string.Empty);
|
|
defaultItemRenderDefinition = definition;
|
|
return;
|
|
}
|
|
|
|
ReleaseCachedItemRenders(viewName);
|
|
itemRenderDefinitions[viewName] = definition;
|
|
}
|
|
|
|
public bool UnregisterItemRender(string viewName = "")
|
|
{
|
|
if (string.IsNullOrEmpty(viewName))
|
|
{
|
|
bool hadDefault = defaultItemRenderDefinition != null;
|
|
defaultItemRenderDefinition = null;
|
|
if (hadDefault)
|
|
{
|
|
ReleaseCachedItemRenders(string.Empty);
|
|
}
|
|
|
|
return hadDefault;
|
|
}
|
|
|
|
bool removed = itemRenderDefinitions.Remove(viewName);
|
|
if (removed)
|
|
{
|
|
ReleaseCachedItemRenders(viewName);
|
|
}
|
|
|
|
return removed;
|
|
}
|
|
|
|
public void ClearItemRenderRegistrations()
|
|
{
|
|
ReleaseAllItemRenders();
|
|
itemRenderDefinitions.Clear();
|
|
defaultItemRenderDefinition = null;
|
|
}
|
|
|
|
public T GetData(int index)
|
|
{
|
|
if (index < 0 || index >= GetItemCount()) return default;
|
|
|
|
return list[index];
|
|
}
|
|
|
|
public void Add(T item)
|
|
{
|
|
if (list == null)
|
|
{
|
|
list = new List<T>();
|
|
}
|
|
|
|
list.Add(item);
|
|
NotifyItemInserted(list.Count - 1);
|
|
}
|
|
|
|
public void AddRange(IEnumerable<T> collection)
|
|
{
|
|
if (collection == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int startIndex = list.Count;
|
|
list.AddRange(collection);
|
|
if (collection is ICollection<T> itemCollection)
|
|
{
|
|
NotifyItemRangeInserted(startIndex, itemCollection.Count);
|
|
return;
|
|
}
|
|
|
|
NotifyDataChanged();
|
|
}
|
|
|
|
public void Insert(int index, T item)
|
|
{
|
|
list.Insert(index, item);
|
|
NotifyItemInserted(index);
|
|
}
|
|
|
|
public void InsertRange(int index, IEnumerable<T> collection)
|
|
{
|
|
if (collection == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
list.InsertRange(index, collection);
|
|
if (collection is ICollection<T> itemCollection)
|
|
{
|
|
NotifyItemRangeInserted(index, itemCollection.Count);
|
|
return;
|
|
}
|
|
|
|
NotifyDataChanged();
|
|
}
|
|
|
|
public void Remove(T item)
|
|
{
|
|
int index = list.IndexOf(item);
|
|
RemoveAt(index);
|
|
}
|
|
|
|
public void RemoveAt(int index)
|
|
{
|
|
if (index < 0 || index >= GetItemCount()) return;
|
|
|
|
list.RemoveAt(index);
|
|
NotifyItemRemoved(index);
|
|
}
|
|
|
|
public void RemoveRange(int index, int count)
|
|
{
|
|
list.RemoveRange(index, count);
|
|
NotifyItemRangeRemoved(index, count);
|
|
}
|
|
|
|
public void RemoveAll(Predicate<T> match)
|
|
{
|
|
list.RemoveAll(match);
|
|
NotifyDataChanged();
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
if (list == null || list.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int count = list.Count;
|
|
list.Clear();
|
|
NotifyItemRangeRemoved(0, count);
|
|
}
|
|
|
|
public void Reverse(int index, int count)
|
|
{
|
|
list.Reverse(index, count);
|
|
NotifyDataChanged();
|
|
}
|
|
|
|
public void Reverse()
|
|
{
|
|
list.Reverse();
|
|
NotifyDataChanged();
|
|
}
|
|
|
|
public void Sort(Comparison<T> comparison)
|
|
{
|
|
list.Sort(comparison);
|
|
NotifyDataChanged();
|
|
}
|
|
|
|
protected void SetChoiceIndex(int index)
|
|
{
|
|
int itemCount = GetRealCount();
|
|
if (itemCount <= 0)
|
|
{
|
|
index = -1;
|
|
}
|
|
else if (index >= itemCount)
|
|
{
|
|
index = itemCount - 1;
|
|
}
|
|
else if (index < -1)
|
|
{
|
|
index = -1;
|
|
}
|
|
|
|
if (index == choiceIndex) return;
|
|
int previousChoice = choiceIndex;
|
|
|
|
if (choiceIndex != -1 && TryGetViewHolder(choiceIndex, out var oldHolder))
|
|
{
|
|
UpdateSelectionState(oldHolder, false);
|
|
}
|
|
|
|
choiceIndex = index;
|
|
|
|
if (choiceIndex != -1 && TryGetViewHolder(choiceIndex, out var newHolder))
|
|
{
|
|
UpdateSelectionState(newHolder, true);
|
|
}
|
|
|
|
OnChoiceIndexChanged?.Invoke(choiceIndex);
|
|
}
|
|
|
|
protected virtual bool TryGetBindData(int index, out T data)
|
|
{
|
|
if (list == null || index < 0 || index >= list.Count)
|
|
{
|
|
data = default;
|
|
return false;
|
|
}
|
|
|
|
data = list[index];
|
|
return true;
|
|
}
|
|
|
|
private bool TryGetViewHolder(int index, out ViewHolder viewHolder)
|
|
{
|
|
viewHolder = recyclerView.ViewProvider.GetViewHolderByDataIndex(index);
|
|
return viewHolder != null;
|
|
}
|
|
|
|
private bool TryGetItemRenderDefinition(string viewName, out ItemRenderResolver.ItemRenderDefinition definition)
|
|
{
|
|
if (!string.IsNullOrEmpty(viewName) && itemRenderDefinitions.TryGetValue(viewName, out definition))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
definition = defaultItemRenderDefinition;
|
|
return definition != null;
|
|
}
|
|
|
|
private void UpdateSelectionState(ViewHolder viewHolder, bool selected)
|
|
{
|
|
if (viewHolder == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (TryGetItemRender(viewHolder, out var itemRender))
|
|
{
|
|
itemRender.UpdateSelection(selected);
|
|
return;
|
|
}
|
|
|
|
string viewName = string.IsNullOrEmpty(viewHolder.Name) ? "<default>" : viewHolder.Name;
|
|
throw new InvalidOperationException(
|
|
$"RecyclerView item render is missing for selection update. View='{viewName}', Holder='{viewHolder.GetType().Name}', Adapter='{GetType().Name}'.");
|
|
}
|
|
|
|
private bool TryGetItemRender(ViewHolder viewHolder, out IItemRender itemRender)
|
|
{
|
|
if (viewHolder != null &&
|
|
itemRenders.TryGetValue(viewHolder, out var entry) &&
|
|
entry.ItemRender != null)
|
|
{
|
|
itemRender = entry.ItemRender;
|
|
return true;
|
|
}
|
|
|
|
itemRender = null;
|
|
return false;
|
|
}
|
|
|
|
private static void ReleaseItemRender(ItemRenderEntry entry)
|
|
{
|
|
if (entry?.ItemRender == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
entry.ItemRender.Unbind();
|
|
if (entry.ItemRender is ItemRenderBase itemRender)
|
|
{
|
|
itemRender.Detach();
|
|
}
|
|
}
|
|
|
|
private bool TryGetOrCreateItemRender(ViewHolder viewHolder, string viewName, out IItemRender itemRender)
|
|
{
|
|
if (viewHolder == null)
|
|
{
|
|
itemRender = null;
|
|
return false;
|
|
}
|
|
|
|
if (itemRenders.TryGetValue(viewHolder, out var entry))
|
|
{
|
|
if (entry.ItemRender != null && string.Equals(entry.ViewName, viewName, StringComparison.Ordinal))
|
|
{
|
|
itemRender = entry.ItemRender;
|
|
return true;
|
|
}
|
|
|
|
ReleaseItemRender(entry);
|
|
viewHolder.Destroyed -= OnViewHolderDestroyed;
|
|
itemRenders.Remove(viewHolder);
|
|
}
|
|
|
|
if (!TryGetItemRenderDefinition(viewName, out var definition))
|
|
{
|
|
itemRender = null;
|
|
return false;
|
|
}
|
|
|
|
itemRender = definition.Create(viewHolder, recyclerView, this, SetChoiceIndex);
|
|
itemRenders[viewHolder] = new ItemRenderEntry(viewName, itemRender);
|
|
viewHolder.Destroyed += OnViewHolderDestroyed;
|
|
return true;
|
|
}
|
|
|
|
void IItemRenderCacheOwner.ReleaseAllItemRenders()
|
|
{
|
|
ReleaseAllItemRenders();
|
|
}
|
|
|
|
private void ReleaseAllItemRenders()
|
|
{
|
|
foreach (var pair in itemRenders)
|
|
{
|
|
ReleaseItemRender(pair.Value);
|
|
if (pair.Key != null)
|
|
{
|
|
pair.Key.Destroyed -= OnViewHolderDestroyed;
|
|
}
|
|
}
|
|
|
|
itemRenders.Clear();
|
|
}
|
|
|
|
private void ReleaseCachedItemRenders(string viewName)
|
|
{
|
|
if (itemRenders.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
List<ViewHolder> viewHoldersToRemove = null;
|
|
foreach (var pair in itemRenders)
|
|
{
|
|
if (!string.Equals(pair.Value.ViewName, viewName, StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ReleaseItemRender(pair.Value);
|
|
pair.Key.Destroyed -= OnViewHolderDestroyed;
|
|
viewHoldersToRemove ??= new List<ViewHolder>();
|
|
viewHoldersToRemove.Add(pair.Key);
|
|
}
|
|
|
|
if (viewHoldersToRemove == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < viewHoldersToRemove.Count; i++)
|
|
{
|
|
itemRenders.Remove(viewHoldersToRemove[i]);
|
|
}
|
|
}
|
|
|
|
private void OnViewHolderDestroyed(ViewHolder viewHolder)
|
|
{
|
|
if (viewHolder == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
viewHolder.Destroyed -= OnViewHolderDestroyed;
|
|
if (itemRenders.TryGetValue(viewHolder, out var entry))
|
|
{
|
|
ReleaseItemRender(entry);
|
|
itemRenders.Remove(viewHolder);
|
|
}
|
|
}
|
|
|
|
private void CoerceChoiceIndex()
|
|
{
|
|
int itemCount = GetRealCount();
|
|
if (itemCount <= 0)
|
|
{
|
|
SetChoiceIndex(-1);
|
|
return;
|
|
}
|
|
|
|
if (choiceIndex >= itemCount)
|
|
{
|
|
SetChoiceIndex(itemCount - 1);
|
|
}
|
|
}
|
|
}
|
|
}
|