maui-linux/Xamarin.Forms.Platform.Tizen/Native/ListView.cs

690 строки
20 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using Xamarin.Forms.Internals;
using ElmSharp;
using ERect = ElmSharp.Rect;
using EScroller = ElmSharp.Scroller;
namespace Xamarin.Forms.Platform.Tizen.Native
{
/// <summary>
/// Type alias which identifies list of cells whose data model was transformed by Xamarin.
/// </summary>
using GroupList = TemplatedItemsList<ItemsView<Cell>, Cell>;
/// <summary>
/// Native ListView implementation for Xamarin renderer
/// </summary>
/// <remarks>
/// This internally uses GenList class.
/// One should note that it is optimized for displaying many elements which may be
/// unavailable at first. This means that only currently visible elements will be constructed.
/// Whenever element disappears from visible space its content is destroyed for time being.
/// This is carried out by so called Cell Handlers.
/// </remarks>
public class ListView : GenList
{
/// <summary>
/// ItemContext helper class. This represents the association between Xamarin.Forms.Cell and
/// native elements. It also stores useful context for them.
/// </summary>
public class ItemContext
{
public ItemContext()
{
Item = null;
Cell = null;
Renderer = null;
ListOfSubItems = null;
}
public GenItem Item;
public Cell Cell;
public bool IsGroupItem;
public CellRenderer Renderer;
internal TemplatedItemsList<ItemsView<Cell>, Cell> ListOfSubItems;
}
public class HeaderFooterItemContext : ItemContext
{
public VisualElement Element;
}
class ScrollerExtension : EScroller
{
public ScrollerExtension(GenList scrollableLayout) : base(scrollableLayout)
{
}
protected override IntPtr CreateHandle(EvasObject parent)
{
return parent.RealHandle;
}
}
/// <summary>
/// The item context list for each added element.
/// </summary>
readonly List<ItemContext> _itemContextList = new List<ItemContext>();
/// <summary>
/// Registered cell handlers.
/// </summary>
protected readonly IDictionary<Type, CellRenderer> _cellRendererCache = new Dictionary<Type, CellRenderer>();
/// <summary>
/// Registered group handlers.
/// </summary>
protected readonly IDictionary<Type, CellRenderer> _groupCellRendererCache = new Dictionary<Type, CellRenderer>();
/// <summary>
/// The header element.
/// </summary>
VisualElement _headerElement;
/// <summary>
/// The footer element.
/// </summary>
VisualElement _footerElement;
/// <summary>
/// The item class for header and footer.
/// </summary>
GenItemClass _headerFooterItemClass = null;
/// <summary>
/// The object to handle scroller properties.
/// </summary>
protected virtual EScroller Scroller { get; set; }
protected HeaderFooterItemContext HeaderItemContext { get; set; }
protected HeaderFooterItemContext FooterItemContext { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance has grouping enabled.
/// </summary>
/// <value><c>true</c> if this instance has grouping enabled.</value>
public bool IsGroupingEnabled { get; set; }
/// <summary>
/// Gets the current region in the content object that is visible through the Scroller.
/// </summary>
public virtual ERect CurrentRegion => Scroller?.CurrentRegion ?? new ERect();
/// <summary>
/// Sets or gets the value of VerticalScrollBarVisibility
/// </summary>
public virtual ScrollBarVisiblePolicy VerticalScrollBarVisibility
{
get
{
return Scroller?.VerticalScrollBarVisiblePolicy ?? ScrollBarVisiblePolicy.Auto;
}
set
{
if (Scroller != null)
Scroller.VerticalScrollBarVisiblePolicy = value;
}
}
/// <summary>
/// Sets or gets the value of HorizontalScrollBarVisibility
/// </summary>
public virtual ScrollBarVisiblePolicy HorizontalScrollBarVisibility
{
get
{
return Scroller?.HorizontalScrollBarVisiblePolicy ?? ScrollBarVisiblePolicy.Auto;
}
set
{
if (Scroller != null)
Scroller.HorizontalScrollBarVisiblePolicy = value;
}
}
/// <summary>
/// Occurs when the ListView has scrolled.
/// </summary>
public event EventHandler Scrolled;
/// <summary>
/// Constructor of ListView native control.
/// </summary>
/// <param name="parent">ElmSharp object which is parent of particular list view</param>
public ListView(EvasObject parent)
: base(parent)
{
Scroller = new ScrollerExtension(this);
Scroller.Scrolled += OnScrolled;
}
protected ListView()
{
}
/// <summary>
/// Gets the item context based on Cell item.
/// </summary>
/// <returns>The item context.</returns>
/// <param name="cell">Cell for which context should be found.</param>
internal ItemContext GetItemContext(Cell cell)
{
if (cell == null)
{
return null;
}
else
{
return _itemContextList.Find(X => X.Cell == cell);
}
}
/// <summary>
/// Sets the HasUnevenRows property.
/// </summary>
/// <param name="hasUnevenRows">If <c>true</c>, the list will allow uneven sizes for its rows.</param>
public void SetHasUnevenRows(bool hasUnevenRows)
{
Homogeneous = !hasUnevenRows;
UpdateRealizedItems();
}
/// <summary>
/// Adds elements to the list and defines its presentation based on Cell type.
/// </summary>
/// <param name="source">IEnumerable on Cell collection.</param>
/// <param name="beforeCell">Cell before which new items will be placed.
/// Null value may also be passed as this parameter, which results in appending new items to the end.
/// </param>
public virtual void AddSource(IEnumerable source, Cell beforeCell = null)
{
foreach (var data in source)
{
GroupList groupList = data as GroupList;
if (groupList != null)
{
AddGroupItem(groupList, beforeCell);
foreach (var item in groupList)
{
AddItem(item, groupList.HeaderContent);
}
}
else
{
AddItem(data as Cell, null, beforeCell);
}
}
}
/// <summary>
/// Deletes all items from a given group.
/// </summary>
/// <param name="group">Group of items to be deleted.</param>
internal void ResetGroup(GroupList group)
{
var items = _itemContextList.FindAll(x => x.ListOfSubItems == group && x.Cell != group.HeaderContent);
foreach (var item in items)
{
item.Item?.Delete();
}
}
/// <summary>
/// Adds items to the group.
/// </summary>
/// <param name="itemGroup">Group to which elements will be added.</param>
/// <param name="newItems">New list items to be added.</param>
/// <param name="cellBefore">A reference to the Cell already existing in a ListView.
/// Newly added cells will be put just before this cell.</param>
public void AddItemsToGroup(IEnumerable itemGroup, IEnumerable newItems, Cell cellBefore = null)
{
ItemContext groupCtx = GetItemContext((itemGroup as GroupList)?.HeaderContent);
if (groupCtx != null)
{
foreach (var item in newItems)
{
AddItem(item as Cell, groupCtx.Cell, cellBefore);
}
}
}
/// <summary>
/// Removes the specified cells.
/// </summary>
/// <param name="cells">Cells to be removed.</param>
public void Remove(IEnumerable cells)
{
foreach (var data in cells)
{
var group = data as GroupList;
if (group != null)
{
ItemContext groupCtx = GetItemContext(group.HeaderContent);
Remove(groupCtx.ListOfSubItems);
groupCtx.Item.Delete();
}
else
{
ItemContext itemCtx = GetItemContext(data as Cell);
itemCtx?.Item?.Delete();
}
}
}
/// <summary>
/// Scrolls the list to a specified cell.
/// </summary>
/// <remarks>
/// Different scrolling behaviors are also possible. The element may be positioned in the center,
/// top or bottom of the visible part of the list depending on the value of the <c>position</c> parameter.
/// </remarks>
/// <param name="cell">Cell which will be displayed after scrolling .</param>
/// <param name="position">This will defines scroll to behavior based on ScrollToPosition values.</param>
/// <param name="animated">If <c>true</c>, scrolling will be animated. Otherwise the cell will be moved instantaneously.</param>
public void ApplyScrollTo(Cell cell, ScrollToPosition position, bool animated)
{
GenListItem item = GetItemContext(cell)?.Item as GenListItem;
if (item != null)
this.ScrollTo(item, position.ToNative(), animated);
}
/// <summary>
/// Selects the specified cell.
/// </summary>
/// <param name="cell">Cell to be selected.</param>
public void ApplySelectedItem(Cell cell)
{
GenListItem item = GetItemContext(cell)?.Item as GenListItem;
if (item != null)
item.IsSelected = true;
}
/// <summary>
/// Sets the header.
/// </summary>
/// <param name="header">Header of the list.</param>
public virtual void SetHeader(VisualElement header)
{
_headerElement = header;
UpdateHeader();
}
protected virtual void UpdateHeader()
{
if (GetHeader() == null)
{
if (HasHeaderContext())
{
RemoveHeaderItemContext();
}
return;
}
var headerTemplate = GetHeaderFooterItemClass();
if (!HasHeaderContext())
{
InitializeHeaderItemContext(headerTemplate);
}
else
{
HeaderItemContext.Element = GetHeader();
(HeaderItemContext.Item as GenListItem).UpdateItemClass(headerTemplate, HeaderItemContext);
}
}
protected void InitializeHeaderItemContext(GenItemClass headerTemplate)
{
var context = new HeaderFooterItemContext();
context.Element = GetHeader();
if (FirstItem != null)
{
context.Item = InsertBefore(headerTemplate, context, FirstItem);
}
else
{
context.Item = Append(headerTemplate, context);
}
context.Item.SelectionMode = GenItemSelectionMode.None;
context.Item.Deleted += OnHeaderItemDeleted;
HeaderItemContext = context;
}
void OnHeaderItemDeleted(object sender, EventArgs e)
{
HeaderItemContext = null;
}
/// <summary>
/// Sets the footer.
/// </summary>
/// <param name="footer">Footer of the list.</param>
public virtual void SetFooter(VisualElement footer)
{
_footerElement = footer;
UpdateFooter();
}
protected virtual void UpdateFooter()
{
if (GetFooter() == null)
{
if (HasFooterContext())
{
RemoveFooterItemContext();
}
return;
}
var footerTemplate = GetHeaderFooterItemClass();
if (!HasFooterContext())
{
InitializeFooterItemContext(footerTemplate);
}
else
{
FooterItemContext.Element = GetFooter();
(HeaderItemContext.Item as GenListItem).UpdateItemClass(footerTemplate, HeaderItemContext);
}
}
protected void InitializeFooterItemContext(GenItemClass footerTemplate)
{
var context = new HeaderFooterItemContext();
context.Element = GetFooter();
context.Item = Append(footerTemplate, context);
context.Item.SelectionMode = GenItemSelectionMode.None;
context.Item.Deleted += OnFooterItemDeleted;
FooterItemContext = context;
}
void OnFooterItemDeleted(object sender, EventArgs e)
{
FooterItemContext = null;
}
protected void RemoveHeaderItemContext()
{
HeaderItemContext?.Item?.Delete();
HeaderItemContext = null;
}
protected void RemoveFooterItemContext()
{
FooterItemContext?.Item?.Delete();
FooterItemContext = null;
}
public bool HasHeaderContext()
{
return HeaderItemContext != null;
}
public bool HasFooterContext()
{
return FooterItemContext != null;
}
/// <summary>
/// Gets the header.
/// </summary>
/// <returns>The header.</returns>
public VisualElement GetHeader()
{
return _headerElement;
}
/// <summary>
/// Gets the footer.
/// </summary>
/// <returns>The footer.</returns>
public VisualElement GetFooter()
{
return _footerElement;
}
protected virtual void OnScrolled(object sender, EventArgs e)
{
Scrolled?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Called every time an object gets realized.
/// </summary>
/// <param name="sender">Sender of the event.</param>
/// <param name="evt">GenListItemEventArgs.</param>
void OnItemAppear(object sender, GenListItemEventArgs evt)
{
ItemContext itemContext = (evt.Item.Data as ItemContext);
if (itemContext != null && itemContext.Cell != null)
{
(itemContext.Cell as ICellController).SendAppearing();
}
}
/// <summary>
/// Called every time an object gets unrealized.
/// </summary>
/// <param name="sender">Sender of the event.</param>
/// <param name="evt">GenListItemEventArgs.</param>
void OnItemDisappear(object sender, GenListItemEventArgs evt)
{
ItemContext itemContext = (evt.Item.Data as ItemContext);
if (itemContext != null && itemContext.Cell != null)
{
(itemContext.Cell as ICellController).SendDisappearing();
itemContext.Renderer?.SendUnrealizedCell(itemContext.Cell);
}
}
protected override void OnRealized()
{
base.OnRealized();
ItemRealized += OnItemAppear;
ItemUnrealized += OnItemDisappear;
}
/// <summary>
/// A convenience shorthand method for derivate classes.
/// </summary>
/// <param name="cell">Cell to be added.</param>
protected void AddCell(Cell cell)
{
AddItem(cell);
}
/// <summary>
/// Gets the cell renderer for given cell type.
/// </summary>
/// <returns>The cell handler.</returns>
/// <param name="cell">Cell to be added.</param>
/// <param name="isGroup">If <c>true</c>, then group handlers will be included in the lookup as well.</param>
protected virtual CellRenderer GetCellRenderer(Cell cell, bool isGroup = false)
{
Type type = cell.GetType();
var cache = isGroup ? _groupCellRendererCache : _cellRendererCache;
if (cache.ContainsKey(type))
{
var cacheCellRenderer = cache[type];
cacheCellRenderer.SendCreatedCell(cell, isGroup);
return cacheCellRenderer;
}
CellRenderer renderer = null;
renderer = Forms.GetHandler<CellRenderer>(type);
if (renderer == null)
{
Log.Error("Cell type is not handled: {0}", cell.GetType());
throw new ArgumentException("Unsupported cell type");
}
renderer.SetGroupMode(isGroup);
renderer.SendCreatedCell(cell, isGroup);
return cache[type] = renderer;
}
/// <summary>
/// Adds the group item. Group item is actually of class GroupList because
/// group item has sub items (can be zero) which needs to be added.
/// If beforeCell is not null, new group will be added just before it.
/// </summary>
/// <param name="groupList">Group to be added.</param>
/// <param name="beforeCell">Before cell.</param>
void AddGroupItem(GroupList groupList, Cell beforeCell = null)
{
Cell groupCell = groupList.HeaderContent;
CellRenderer groupRenderer = GetCellRenderer(groupCell, true);
ItemContext groupItemContext = new ItemContext();
groupItemContext.Cell = groupCell;
groupItemContext.Renderer = groupRenderer;
groupItemContext.IsGroupItem = true;
groupItemContext.ListOfSubItems = groupList;
_itemContextList.Add(groupItemContext);
if (beforeCell != null)
{
GenListItem beforeItem = GetItemContext(beforeCell)?.Item as GenListItem;
groupItemContext.Item = InsertBefore(groupRenderer.Class, groupItemContext, beforeItem, GenListItemType.Group);
}
else
{
groupItemContext.Item = Append(groupRenderer.Class, groupItemContext, GenListItemType.Group);
}
groupItemContext.Item.SelectionMode = GenItemSelectionMode.None;
groupItemContext.Item.IsEnabled = groupCell.IsEnabled;
groupItemContext.Item.Deleted += ItemDeletedHandler;
}
/// <summary>
/// Adds the item.
/// </summary>
/// <param name="cell">Cell to be added.</param>
/// <param name="groupCell">Group to which the new item should belong.</param>
/// <remark>If the value of <c>groupCell</c> is not null, the new item will be put into the requested group. </remark>
/// <param name="beforeCell">The cell before which the new item should be placed.</param>
/// <remarks> If the value of <c>beforeCell</c> is not null, the new item will be placed just before the requested cell. </remarks>
void AddItem(Cell cell, Cell groupCell = null, Cell beforeCell = null)
{
CellRenderer renderer = GetCellRenderer(cell);
GenListItem parentItem = null;
ItemContext itemContext = new ItemContext();
itemContext.Cell = cell;
itemContext.Renderer = renderer;
_itemContextList.Add(itemContext);
if (IsGroupingEnabled && groupCell != null)
{
var groupContext = GetItemContext(groupCell);
itemContext.ListOfSubItems = groupContext.ListOfSubItems;
parentItem = groupContext.Item as GenListItem;
}
if (beforeCell != null)
{
GenListItem beforeItem = GetItemContext(beforeCell)?.Item as GenListItem;
itemContext.Item = InsertBefore(renderer.Class, itemContext, beforeItem, GenListItemType.Normal, parentItem);
}
else if (HasFooterContext())
{
itemContext.Item = InsertBefore(renderer.Class, itemContext, FooterItemContext.Item as GenListItem, GenListItemType.Normal, parentItem);
}
else
{
itemContext.Item = Append(renderer.Class, itemContext, GenListItemType.Normal, parentItem);
}
itemContext.Item.SelectionMode = GenItemSelectionMode.Always;
itemContext.Item.IsEnabled = cell.IsEnabled;
itemContext.Item.Deleted += ItemDeletedHandler;
cell.PropertyChanged += OnCellPropertyChanged;
(cell as ICellController).ForceUpdateSizeRequested += OnForceUpdateSizeRequested;
}
/// <summary>
/// Handles item deleted event.
/// </summary>
/// <param name="sender">Sender of the event.</param>
/// <param name="e">Empty argument.</param>
void ItemDeletedHandler(object sender, EventArgs e)
{
ItemContext itemContext = (sender as GenListItem).Data as ItemContext;
if (itemContext.Cell != null)
{
itemContext.Cell.PropertyChanged -= OnCellPropertyChanged;
(itemContext.Cell as ICellController).ForceUpdateSizeRequested -= OnForceUpdateSizeRequested;
}
_itemContextList.Remove(itemContext);
}
/// <summary>
/// Invoked whenever the properties of data model change.
/// </summary>
/// <param name="sender">Sender of the event.</param>
/// <param name="e">PropertyChangedEventArgs.</param>
/// <remarks>
/// The purpose of this method is to propagate these changes to the presentation layer.
/// </remarks>
void OnCellPropertyChanged(object sender, PropertyChangedEventArgs e)
{
var cell = sender as Cell;
var context = GetItemContext(cell);
context.Renderer.SendCellPropertyChanged(cell, context.Item, e.PropertyName);
}
void OnForceUpdateSizeRequested(object sender, EventArgs e)
{
var cell = sender as Cell;
var itemContext = GetItemContext(cell);
if (itemContext.Item != null)
itemContext.Item.Update();
}
/// <summary>
/// Gets the item class used for header and footer cells.
/// </summary>
/// <returns>The header and footer item class.</returns>
protected GenItemClass GetHeaderFooterItemClass()
{
if (_headerFooterItemClass == null)
{
_headerFooterItemClass = new GenItemClass(ThemeConstants.GenItemClass.Styles.Full)
{
GetContentHandler = (data, part) =>
{
var context = data as HeaderFooterItemContext;
if (context == null || context.Element == null)
return null;
var renderer = Platform.GetOrCreateRenderer(context.Element);
if (context.Element.MinimumHeightRequest == -1)
{
SizeRequest request = context.Element.Measure(double.PositiveInfinity, double.PositiveInfinity);
renderer.NativeView.MinimumHeight = Forms.ConvertToScaledPixel(request.Request.Height);
}
else
{
renderer.NativeView.MinimumHeight = Forms.ConvertToScaledPixel(context.Element.MinimumHeightRequest);
}
(renderer as LayoutRenderer)?.RegisterOnLayoutUpdated();
return renderer.NativeView;
}
};
}
return _headerFooterItemClass;
}
}
}