File: Android\Renderers\ListViewAdapter.cs
Web Access
Project: src\src\Compatibility\Core\src\Compatibility.csproj (Microsoft.Maui.Controls.Compatibility)
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Android.Content;
using Android.Util;
using Android.Views;
using Android.Widget;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Graphics;
using AListView = Android.Widget.ListView;
using AView = Android.Views.View;
 
namespace Microsoft.Maui.Controls.Compatibility.Platform.Android
{
	[Obsolete("Use Microsoft.Maui.Controls.Handlers.Compatibility.ListViewAdapter instead")]
	internal class ListViewAdapter : Handlers.Compatibility.CellAdapter
	{
		bool _disposed;
		static readonly object DefaultItemTypeOrDataTemplate = new object();
		const int DefaultGroupHeaderTemplateId = 0;
		const int DefaultItemTemplateId = 1;
 
		static int s_dividerHorizontalDarkId = int.MinValue;
 
		internal static readonly BindableProperty IsSelectedProperty = BindableProperty.CreateAttached("IsSelected", typeof(bool), typeof(Cell), false);
 
		readonly Context _context;
		protected readonly ListView _listView;
		readonly AListView _realListView;
		readonly Dictionary<DataTemplate, int> _templateToId = new Dictionary<DataTemplate, int>();
		readonly List<Handlers.Compatibility.ConditionalFocusLayout> _layoutsCreated = new List<Handlers.Compatibility.ConditionalFocusLayout>();
		int _dataTemplateIncrementer = 2; // lets start at not 0 because ... 
 
		// We will use _dataTemplateIncrementer to get the proper ViewType key for the item's DataTemplate and store these keys in  _templateToId.
		// If an item does _not_ use a DataTemplate, then the ViewType key will be DefaultItemTemplateId (1) or DefaultGroupHeaderTemplateId (0).
		// To prevent a conflict in the event that a ListView supports both templates and non-templates, we will start the DataTemplate key at 2.
 
		int _listCount = -1; // -1 we need to get count from the list
		Dictionary<object, Cell> _prototypicalCellByTypeOrDataTemplate;
 
		bool _fromNative;
		AView _lastSelected;
		WeakReference<Cell> _selectedCell;
 
		IListViewController Controller => _listView;
		protected ITemplatedItemsView<Cell> TemplatedItemsView => _listView;
 
		public ListViewAdapter(Context context, AListView realListView, ListView listView) : base(context)
		{
			_context = context;
			_realListView = realListView;
			_listView = listView;
			_prototypicalCellByTypeOrDataTemplate = new Dictionary<object, Cell>();
 
			if (listView.SelectedItem != null)
				SelectItem(listView.SelectedItem);
 
			var templatedItems = ((ITemplatedItemsView<Cell>)listView).TemplatedItems;
			templatedItems.CollectionChanged += OnCollectionChanged;
			templatedItems.GroupedCollectionChanged += OnGroupedCollectionChanged;
			listView.ItemSelected += OnItemSelected;
 
			realListView.OnItemClickListener = this;
			realListView.OnItemLongClickListener = this;
 
			MessagingCenter.Subscribe<ListViewAdapter>(this, Platform.CloseContextActionsSignalName, lva => CloseContextActions());
 
			InvalidateCount();
		}
 
		public override int Count
		{
			get
			{
				if (_listCount == -1)
				{
					var templatedItems = TemplatedItemsView.TemplatedItems;
					int count = templatedItems.Count;
 
					if (_listView.IsGroupingEnabled)
					{
						for (var i = 0; i < templatedItems.Count; i++)
							count += templatedItems.GetGroup(i).Count;
					}
 
					_listCount = count;
				}
				return _listCount;
			}
		}
 
		public AView FooterView { get; set; }
 
		public override bool HasStableIds
		{
			get { return false; }
		}
 
		public AView HeaderView { get; set; }
 
		public bool IsAttachedToWindow { get; set; }
 
		public override object this[int index]
		{
			get
			{
				if (_listView.IsGroupingEnabled)
				{
					Cell cell = GetCellForPosition(index);
					return cell.BindingContext;
				}
 
				return TemplatedItemsView.ListProxy[index];
			}
		}
 
		public override int ViewTypeCount
		{
			get
			{
				// We have a documented limit of 20 templates on Android.
				// ViewTypes are selected on a zero-based index, so this count must be at least 20 + 1.
				// Plus, we arbitrarily increased the index of the DataTemplate index by 2 (see _dataTemplateIncrementer).
				return 23;
			}
		}
 
		public override bool AreAllItemsEnabled()
		{
			return false;
		}
 
		public override long GetItemId(int position)
		{
			return position;
		}
 
		public override int GetItemViewType(int position)
		{
			var group = 0;
			var row = 0;
			DataTemplate itemTemplate;
			if (!_listView.IsGroupingEnabled)
				itemTemplate = _listView.ItemTemplate;
			else
			{
				group = TemplatedItemsView.TemplatedItems.GetGroupIndexFromGlobal(position, out row);
 
				if (row == 0)
				{
					itemTemplate = _listView.GroupHeaderTemplate;
					if (itemTemplate == null)
						return DefaultGroupHeaderTemplateId;
				}
				else
				{
					itemTemplate = _listView.ItemTemplate;
					row--;
				}
			}
 
			if (itemTemplate == null)
				return DefaultItemTemplateId;
 
			if (itemTemplate is DataTemplateSelector selector)
			{
				object item = null;
 
				if (_listView.IsGroupingEnabled)
				{
					if (TemplatedItemsView.TemplatedItems.GetGroup(group).ListProxy.Count > 0)
						item = TemplatedItemsView.TemplatedItems.GetGroup(group).ListProxy[row];
				}
				else
				{
					if (TemplatedItemsView.TemplatedItems.ListProxy.Count > 0)
						item = TemplatedItemsView.TemplatedItems.ListProxy[position];
				}
 
				itemTemplate = selector.SelectTemplate(item, _listView);
			}
 
			// check again to guard against DataTemplateSelectors that return null
			if (itemTemplate == null)
				return DefaultItemTemplateId;
 
			if (!_templateToId.TryGetValue(itemTemplate, out int key))
			{
				_dataTemplateIncrementer++;
				key = _dataTemplateIncrementer;
				_templateToId[itemTemplate] = key;
			}
 
			if (key >= ViewTypeCount)
			{
				throw new Exception($"ItemTemplate count has exceeded the limit of {ViewTypeCount}" + Environment.NewLine +
									 "Please make sure to reuse DataTemplate objects");
			}
 
			return key;
		}
 
		public override AView GetView(int position, AView convertView, ViewGroup parent)
		{
			Cell cell = null;
 
			Performance.Start(out string reference);
 
			ListViewCachingStrategy cachingStrategy = Controller.CachingStrategy;
			var nextCellIsHeader = false;
			if (cachingStrategy == ListViewCachingStrategy.RetainElement || convertView == null)
			{
				if (_listView.IsGroupingEnabled)
				{
					List<Cell> cells = GetCellsFromPosition(position, 2);
					if (cells.Count > 0)
						cell = cells[0];
 
					if (cells.Count == 2)
						nextCellIsHeader = cells[1].GetIsGroupHeader<ItemsView<Cell>, Cell>();
				}
 
				if (cell == null)
				{
					cell = GetCellForPosition(position);
 
					if (cell == null)
					{
						Performance.Stop(reference);
 
						return new AView(_context);
					}
				}
			}
 
			var cellIsBeingReused = false;
			var layout = convertView as Handlers.Compatibility.ConditionalFocusLayout;
			if (layout != null)
			{
				cellIsBeingReused = true;
				convertView = layout.GetChildAt(0);
			}
			else
			{
				layout = new Handlers.Compatibility.ConditionalFocusLayout(_context) { Orientation = Orientation.Vertical };
				_layoutsCreated.Add(layout);
			}
 
			if (((cachingStrategy & ListViewCachingStrategy.RecycleElement) != 0) && convertView != null)
			{
				var boxedCell = convertView as INativeElementView;
				if (boxedCell == null)
				{
					throw new InvalidOperationException($"View for cell must implement {nameof(INativeElementView)} to enable recycling.");
				}
				cell = (Cell)boxedCell.Element;
 
				ICellController cellController = cell;
				cellController.SendDisappearing();
 
				int row = position;
				var group = 0;
				var templatedItems = TemplatedItemsView.TemplatedItems;
				if (_listView.IsGroupingEnabled)
					group = templatedItems.GetGroupIndexFromGlobal(position, out row);
 
				var templatedList = templatedItems.GetGroup(group);
 
				if (_listView.IsGroupingEnabled)
				{
					if (row == 0)
						templatedList.UpdateHeader(cell, group);
					else
						templatedList.UpdateContent(cell, row - 1);
				}
				else
					templatedList.UpdateContent(cell, row);
 
				cellController.SendAppearing();
 
				if (cell.BindingContext == ActionModeObject)
				{
					ActionModeContext = cell;
					ContextView = layout;
				}
 
				if (ReferenceEquals(_listView.SelectedItem, cell.BindingContext))
					Select(_listView.IsGroupingEnabled ? row - 1 : row, layout);
				else if (cell.BindingContext == ActionModeObject)
					SetSelectedBackground(layout, true);
				else
					UnsetSelectedBackground(layout);
 
				Performance.Stop(reference);
				return layout;
			}
 
			AView view = CellFactory.GetCell(cell, convertView, parent, _context, _listView);
 
			Performance.Start(reference, "AddView");
 
			if (cellIsBeingReused)
			{
				if (convertView != view)
				{
					layout.RemoveViewAt(0);
					layout.AddView(view, 0);
				}
			}
			else
				layout.AddView(view, 0);
 
			Performance.Stop(reference, "AddView");
 
			bool isHeader = cell.GetIsGroupHeader<ItemsView<Cell>, Cell>();
 
			AView bline;
 
			bool isSeparatorVisible = _listView.SeparatorVisibility == SeparatorVisibility.Default;
 
			if (isSeparatorVisible)
			{
				UpdateSeparatorVisibility(cell, cellIsBeingReused, isHeader, nextCellIsHeader, isSeparatorVisible, layout, out bline);
 
				UpdateSeparatorColor(isHeader, bline);
			}
			else if (layout.ChildCount > 1)
			{
				layout.RemoveViewAt(1);
			}
 
			if ((bool)cell.GetValue(IsSelectedProperty))
				Select(position, layout);
			else
				UnsetSelectedBackground(layout);
 
			layout.ApplyTouchListenersToSpecialCells(cell);
 
			Performance.Stop(reference);
 
			return layout;
		}
 
		internal void InvalidatePrototypicalCellCache()
		{
			_prototypicalCellByTypeOrDataTemplate.Clear();
		}
 
		internal ITemplatedItemsList<Cell> GetTemplatedItemsListForPath(int indexPath)
		{
			var templatedItems = TemplatedItemsView.TemplatedItems;
			if (_listView.IsGroupingEnabled)
				templatedItems = (ITemplatedItemsList<Cell>)((IList)templatedItems)[indexPath];
 
			return templatedItems;
		}
 
		internal DataTemplate GetDataTemplateForPath(int indexPath)
		{
			var templatedItemsList = GetTemplatedItemsListForPath(indexPath);
			var item = templatedItemsList.ListProxy[indexPath];
			return templatedItemsList.SelectDataTemplate(item);
		}
 
		internal Type GetItemTypeForPath(int indexPath)
		{
			var templatedItemsList = GetTemplatedItemsListForPath(indexPath);
			var item = templatedItemsList.ListProxy[indexPath];
			return item.GetType();
		}
 
		internal Cell GetCellForPath(int indexPath)
		{
			var templatedItemsList = GetTemplatedItemsListForPath(indexPath);
			var cell = templatedItemsList[indexPath];
			return cell;
		}
 
		internal Cell GetPrototypicalCell(int indexPath)
		{
			var itemTypeOrDataTemplate = default(object);
 
			var cachingStrategy = _listView.CachingStrategy;
			if (cachingStrategy == ListViewCachingStrategy.RecycleElement)
				itemTypeOrDataTemplate = GetDataTemplateForPath(indexPath);
 
			else if (cachingStrategy == ListViewCachingStrategy.RecycleElementAndDataTemplate)
				itemTypeOrDataTemplate = GetItemTypeForPath(indexPath);
 
			else // ListViewCachingStrategy.RetainElement
				return GetCellForPosition(indexPath);
 
			if (itemTypeOrDataTemplate == null)
				itemTypeOrDataTemplate = DefaultItemTypeOrDataTemplate;
 
			Cell protoCell;
			if (!_prototypicalCellByTypeOrDataTemplate.TryGetValue(itemTypeOrDataTemplate, out protoCell))
			{
				// cache prototypical cell by item type; Items of the same Type share
				// the same DataTemplate (this is enforced by RecycleElementAndDataTemplate)
				protoCell = GetCellForPosition(indexPath);
				_prototypicalCellByTypeOrDataTemplate[itemTypeOrDataTemplate] = protoCell;
			}
 
			var templatedItems = GetTemplatedItemsListForPath(indexPath);
			return templatedItems.UpdateContent(protoCell, indexPath);
		}
 
		public override bool IsEnabled(int position)
		{
			ListView list = _listView;
 
			ITemplatedItemsView<Cell> templatedItemsView = list;
			if (list.IsGroupingEnabled)
			{
				int leftOver;
				templatedItemsView.TemplatedItems.GetGroupIndexFromGlobal(position, out leftOver);
				return leftOver > 0;
			}
 
			Cell item = GetPrototypicalCell(position);
			return item?.IsEnabled ?? false;
		}
 
		protected override void Dispose(bool disposing)
		{
			if (_disposed)
			{
				return;
			}
 
			_disposed = true;
 
			if (disposing)
			{
				CloseContextActions();
 
				MessagingCenter.Unsubscribe<ListViewAdapter>(this, Platform.CloseContextActionsSignalName);
 
				_realListView.OnItemClickListener = null;
				_realListView.OnItemLongClickListener = null;
 
				var templatedItems = TemplatedItemsView.TemplatedItems;
				templatedItems.CollectionChanged -= OnCollectionChanged;
				templatedItems.GroupedCollectionChanged -= OnGroupedCollectionChanged;
				_listView.ItemSelected -= OnItemSelected;
 
				if (_lastSelected != null)
				{
					_lastSelected.Dispose();
					_lastSelected = null;
				}
 
				DisposeCells();
			}
 
			base.Dispose(disposing);
		}
 
		protected override Cell GetCellForPosition(int position)
		{
			return GetCellsFromPosition(position, 1).FirstOrDefault();
		}
 
		protected override void HandleItemClick(AdapterView parent, AView view, int position, long id)
		{
			Cell cell = null;
 
			if ((Controller.CachingStrategy & ListViewCachingStrategy.RecycleElement) != 0)
			{
				AView cellOwner = view;
				var layout = cellOwner as Handlers.Compatibility.ConditionalFocusLayout;
				if (layout != null)
					cellOwner = layout.GetChildAt(0);
				cell = (Cell)(cellOwner as INativeElementView)?.Element;
			}
 
			// All our ListView's have called AddHeaderView. This effectively becomes index 0, so our index 0 is index 1 to the listView.
			position--;
 
			if (position < 0 || position >= Count)
				return;
 
			if (_lastSelected != view)
				_fromNative = true;
			if (_listView.SelectionMode != ListViewSelectionMode.None)
				Select(position, view);
			Controller.NotifyRowTapped(position, cell);
		}
 
		void DisposeCells()
		{
			var cellCount = _layoutsCreated.Count;
 
			for (int i = 0; i < cellCount; i++)
			{
				var layout = _layoutsCreated[i];
 
				if (layout.IsDisposed())
					continue;
 
				DisposeOfConditionalFocusLayout(layout);
			}
 
			_layoutsCreated.Clear();
		}
 
		void DisposeOfConditionalFocusLayout(Handlers.Compatibility.ConditionalFocusLayout layout)
		{
			var renderedView = layout?.GetChildAt(0);
 
			var element = (renderedView as INativeElementView)?.Element;
			var view = (element as ViewCell)?.View;
 
			if (view != null)
			{
				var renderer = Platform.GetRenderer(view);
 
				if (renderer == renderedView)
					element.ClearValue(Platform.RendererProperty);
 
				renderer?.Dispose();
				renderer = null;
			}
 
			renderedView?.Dispose();
			renderedView = null;
		}
 
		// TODO: We can optimize this by storing the last position, group index and global index
		// and increment/decrement from that starting place.	
		List<Cell> GetCellsFromPosition(int position, int take)
		{
			var cells = new List<Cell>(take);
			if (position < 0)
				return cells;
 
			var templatedItems = TemplatedItemsView.TemplatedItems;
			var templatedItemsCount = templatedItems.Count;
			if (!_listView.IsGroupingEnabled)
			{
				for (var x = 0; x < take; x++)
				{
					if (position + x >= templatedItemsCount)
						return cells;
 
					cells.Add(templatedItems[x + position]);
				}
 
				return cells;
			}
 
			var i = 0;
			var global = 0;
			for (; i < templatedItemsCount; i++)
			{
				var group = templatedItems.GetGroup(i);
 
				if (global == position || cells.Count > 0)
				{
					//Always create a new cell if we are using the RecycleElement strategy
					var recycleElement = (_listView.CachingStrategy & ListViewCachingStrategy.RecycleElement) != 0;
					var headerCell = recycleElement ? GetNewGroupHeaderCell(group) : group.HeaderContent;
					cells.Add(headerCell);
 
					if (cells.Count == take)
						return cells;
				}
 
				global++;
 
				if (global + group.Count < position)
				{
					global += group.Count;
					continue;
				}
 
				for (var g = 0; g < group.Count; g++)
				{
					if (global == position || cells.Count > 0)
					{
						cells.Add(group[g]);
						if (cells.Count == take)
							return cells;
					}
 
					global++;
				}
			}
 
			return cells;
		}
 
		void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			OnDataChanged();
		}
 
		void OnDataChanged()
		{
			InvalidateCount();
			if (ActionModeContext != null && !TemplatedItemsView.TemplatedItems.Contains(ActionModeContext))
				CloseContextActions();
 
			if (IsAttachedToWindow)
				NotifyDataSetChanged();
			else
			{
				// In a TabbedPage page with two pages, Page A and Page B with ListView, if A changes B's ListView,
				// we need to reset the ListView's adapter to reflect the changes on page B
				// If there header and footer are present at the reset time of the adapter
				// they will be DOUBLE added to the ViewGround (the ListView) causing indexes to be off by one. 
 
				if (_realListView.IsDisposed())
					return;
 
				_realListView.RemoveHeaderView(HeaderView);
				_realListView.RemoveFooterView(FooterView);
				_realListView.Adapter = _realListView.Adapter;
				_realListView.AddHeaderView(HeaderView);
				_realListView.AddFooterView(FooterView);
			}
		}
 
		void OnGroupedCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			OnDataChanged();
		}
 
		void OnItemSelected(object sender, SelectedItemChangedEventArgs eventArg)
		{
			if (_fromNative)
			{
				_fromNative = false;
				return;
			}
 
			SelectItem(eventArg.SelectedItem);
		}
 
		void Select(int index, AView view)
		{
			if (_lastSelected != null)
			{
				UnsetSelectedBackground(_lastSelected);
				Cell previousCell;
				if (_selectedCell.TryGetTarget(out previousCell))
					previousCell.SetValue(IsSelectedProperty, false);
			}
 
			_lastSelected = view;
 
			if (index == -1)
				return;
 
			Cell cell = GetCellForPosition(index);
			cell.SetValue(IsSelectedProperty, true);
			_selectedCell = new WeakReference<Cell>(cell);
 
			if (view != null)
				SetSelectedBackground(view);
		}
 
		void SelectItem(object item)
		{
			if (_listView == null)
				return;
 
			int position = TemplatedItemsView.TemplatedItems.GetGlobalIndexOfItem(item);
			AView view = null;
			if (position != -1)
				view = _realListView.GetChildAt(position + 1 - _realListView.FirstVisiblePosition);
 
			Select(position, view);
		}
 
		void UpdateSeparatorVisibility(Cell cell, bool cellIsBeingReused, bool isHeader, bool nextCellIsHeader, bool isSeparatorVisible, Handlers.Compatibility.ConditionalFocusLayout layout, out AView bline)
		{
			bline = null;
			if (cellIsBeingReused && layout.ChildCount > 1)
			{
				layout.RemoveViewAt(1);
			}
			var makeBline = isSeparatorVisible || isHeader && isSeparatorVisible && !nextCellIsHeader;
			if (makeBline)
			{
				bline = new AView(_context) { LayoutParameters = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, 1) };
				layout.AddView(bline);
			}
			else if (layout.ChildCount > 1)
			{
				layout.RemoveViewAt(1);
			}
		}
 
 
		void UpdateSeparatorColor(bool isHeader, AView bline)
		{
			if (bline == null)
				return;
 
			Color separatorColor = _listView.SeparatorColor;
 
			if (isHeader || separatorColor != null)
				bline.SetBackgroundColor(separatorColor.ToAndroid(Application.AccentColor));
			else
			{
				if (s_dividerHorizontalDarkId == int.MinValue)
				{
					using (var value = new TypedValue())
					{
						int id = global::Android.Resource.Drawable.DividerHorizontalDark;
						if (_context.Theme.ResolveAttribute(global::Android.Resource.Attribute.ListDivider, value, true))
							id = value.ResourceId;
						else if (_context.Theme.ResolveAttribute(global::Android.Resource.Attribute.Divider, value, true))
							id = value.ResourceId;
 
						s_dividerHorizontalDarkId = id;
					}
				}
 
				bline.SetBackgroundResource(s_dividerHorizontalDarkId);
			}
		}
 
		Cell GetNewGroupHeaderCell(ITemplatedItemsList<Cell> group)
		{
			var groupHeaderCell = _listView.TemplatedItems.GroupHeaderTemplate?.CreateContent(group.ItemsSource, _listView) as Cell;
 
			if (groupHeaderCell != null)
			{
				groupHeaderCell.BindingContext = group.ItemsSource;
			}
			else
			{
				groupHeaderCell = new TextCell();
				groupHeaderCell.SetBinding(
					TextCell.TextProperty,
					static (ITemplatedItemsList<Cell> g) => g.Name);
				groupHeaderCell.BindingContext = group;
			}
 
			groupHeaderCell.Parent = _listView;
			groupHeaderCell.SetIsGroupHeader<ItemsView<Cell>, Cell>(true);
			return groupHeaderCell;
		}
 
		enum CellType
		{
			Row,
			Header
		}
 
		protected virtual void InvalidateCount()
		{
			_listCount = -1;
		}
	}
 
}