using System; using System.Collections.Generic; namespace AlicizaX.UI { internal interface IItemRenderCacheOwner { void ReleaseAllItemRenders(); } public class Adapter : 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 list; protected int choiceIndex = -1; private readonly Dictionary itemRenderDefinitions = new(StringComparer.Ordinal); private readonly Dictionary itemRenders = new(); private ItemRenderResolver.ItemRenderDefinition defaultItemRenderDefinition; public int ChoiceIndex { get => choiceIndex; set => SetChoiceIndex(value); } public Adapter(RecyclerView recyclerView) : this(recyclerView, new List()) { } public Adapter(RecyclerView recyclerView, List list) { this.recyclerView = recyclerView; this.list = list; } 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); Action defaultClickAction = CreateItemClickAction(index, data); if (TryGetOrCreateItemRender(viewHolder, viewName, out var itemRender)) { itemRender.Bind(data, index, defaultClickAction); itemRender.UpdateSelection(index == choiceIndex); return; } string resolvedViewName = string.IsNullOrEmpty(viewName) ? "" : 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.UpdateSelection(false); itemRender.Unbind(); } viewHolder.ClearInteractionCallbacks(); } public virtual void NotifyDataChanged() { recyclerView.RequestLayout(); recyclerView.Refresh(); } public virtual void SetList(List list) { this.list = list; recyclerView.Reset(); NotifyDataChanged(); } public void RegisterItemRender(string viewName = "") where TItemRender : IItemRender { 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) { list.Add(item); NotifyDataChanged(); } public void AddRange(IEnumerable collection) { list.AddRange(collection); NotifyDataChanged(); } public void Insert(int index, T item) { list.Insert(index, item); NotifyDataChanged(); } public void InsertRange(int index, IEnumerable collection) { list.InsertRange(index, collection); 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); NotifyDataChanged(); } public void RemoveRange(int index, int count) { list.RemoveRange(index, count); NotifyDataChanged(); } public void RemoveAll(Predicate match) { list.RemoveAll(match); NotifyDataChanged(); } public void Clear() { list.Clear(); NotifyDataChanged(); } public void Reverse(int index, int count) { list.Reverse(index, count); NotifyDataChanged(); } public void Reverse() { list.Reverse(); NotifyDataChanged(); } public void Sort(Comparison comparison) { list.Sort(comparison); NotifyDataChanged(); } protected void SetChoiceIndex(int index) { if (index == choiceIndex) return; if (choiceIndex != -1 && TryGetViewHolder(choiceIndex, out var oldHolder)) { UpdateSelectionState(oldHolder, false); } choiceIndex = index; if (choiceIndex != -1 && TryGetViewHolder(choiceIndex, out var newHolder)) { UpdateSelectionState(newHolder, true); } } private bool TryGetViewHolder(int index, out ViewHolder viewHolder) { viewHolder = recyclerView.ViewProvider.GetViewHolder(index); return viewHolder != null; } 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; } protected virtual Action CreateItemClickAction(int index, T data) { return () => { SetChoiceIndex(index); }; } 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) ? "" : 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 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; } entry.ItemRender?.Unbind(); viewHolder.Destroyed -= OnViewHolderDestroyed; itemRenders.Remove(viewHolder); } if (!TryGetItemRenderDefinition(viewName, out var definition)) { itemRender = null; return false; } itemRender = definition.Create(viewHolder); itemRenders[viewHolder] = new ItemRenderEntry(viewName, itemRender); viewHolder.Destroyed += OnViewHolderDestroyed; return true; } void IItemRenderCacheOwner.ReleaseAllItemRenders() { ReleaseAllItemRenders(); } private void ReleaseAllItemRenders() { foreach (var pair in itemRenders) { pair.Value.ItemRender?.Unbind(); if (pair.Key != null) { pair.Key.Destroyed -= OnViewHolderDestroyed; } } itemRenders.Clear(); } private void ReleaseCachedItemRenders(string viewName) { if (itemRenders.Count == 0) { return; } List viewHoldersToRemove = null; foreach (var pair in itemRenders) { if (!string.Equals(pair.Value.ViewName, viewName, StringComparison.Ordinal)) { continue; } pair.Value.ItemRender?.Unbind(); pair.Key.Destroyed -= OnViewHolderDestroyed; viewHoldersToRemove ??= new List(); 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; itemRenders.Remove(viewHolder); } } }