[Handler]Collectionview handler for iOS (#2156)

* [Handlers] Add iOS handler

* Fix for Nav

* Fix mappers on iOS

* Add ToHandler extension

* [iOS] Fix templated cell

* [iOS] Fix more measure on cels

* Refactor ItemsViewHandler

* Refactor ScrollBarVisibility

* Add windows handlers

* Fix rebase error

Co-authored-by: E.Z. Hart <hartez@gmail.com>
This commit is contained in:
Rui Marinho 2021-10-21 10:17:59 +01:00 коммит произвёл GitHub
Родитель 1695287e17
Коммит c9d6b64f9f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
72 изменённых файлов: 6588 добавлений и 1 удалений

Просмотреть файл

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class CollectionViewHandler : GroupableItemsViewHandler<GroupableItemsView>
{
protected override Android.Views.View CreateNativeView()
{
throw new NotImplementedException();
}
}
}

Просмотреть файл

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class CollectionViewHandler : GroupableItemsViewHandler<GroupableItemsView>
{
protected override object CreateNativeView()
{
throw new NotImplementedException();
}
}
}

Просмотреть файл

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class CollectionViewHandler : GroupableItemsViewHandler<GroupableItemsView>
{
protected override UserControl CreateNativeView()
{
throw new NotImplementedException();
}
}
}

Просмотреть файл

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class CollectionViewHandler
{
public CollectionViewHandler() : base(CollectionViewMapper)
{
}
public CollectionViewHandler(PropertyMapper mapper = null) : base(mapper ?? CollectionViewMapper)
{
}
public static PropertyMapper<CollectionView, CollectionViewHandler> CollectionViewMapper = new PropertyMapper<CollectionView, CollectionViewHandler>(ViewMapper)
{
[Controls.ItemsView.ItemsSourceProperty.PropertyName] = MapItemsSource,
[Controls.ItemsView.HorizontalScrollBarVisibilityProperty.PropertyName] = MapHorizontalScrollBarVisibility,
[Controls.ItemsView.VerticalScrollBarVisibilityProperty.PropertyName] = MapVerticalScrollBarVisibility,
[Controls.ItemsView.ItemTemplateProperty.PropertyName] = MapItemTemplate,
[Controls.ItemsView.EmptyViewProperty.PropertyName] = MapEmptyView,
[Controls.ItemsView.EmptyViewTemplateProperty.PropertyName] = MapEmptyViewTemplate,
[Controls.ItemsView.FlowDirectionProperty.PropertyName] = MapFlowDirection,
[Controls.ItemsView.IsVisibleProperty.PropertyName] = MapIsVisible,
[Controls.ItemsView.ItemsUpdatingScrollModeProperty.PropertyName] = MapItemsUpdatingScrollMode,
[StructuredItemsView.HeaderTemplateProperty.PropertyName] = MapHeaderTemplate,
[StructuredItemsView.FooterTemplateProperty.PropertyName] = MapFooterTemplate,
[StructuredItemsView.ItemsLayoutProperty.PropertyName] = MapItemsLayout,
[StructuredItemsView.ItemSizingStrategyProperty.PropertyName] = MapItemSizingStrategy,
[SelectableItemsView.SelectedItemProperty.PropertyName] = MapSelectedItem,
[SelectableItemsView.SelectedItemsProperty.PropertyName] = MapSelectedItems,
[SelectableItemsView.SelectionModeProperty.PropertyName] = MapSelectionMode,
[GroupableItemsView.IsGroupedProperty.PropertyName] = MapIsGrouped
};
}
}

Просмотреть файл

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
using Foundation;
using Microsoft.Maui.Handlers;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class CollectionViewHandler : GroupableItemsViewHandler<GroupableItemsView>
{
}
}

Просмотреть файл

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class GroupableItemsViewHandler<TItemsView> : SelectableItemsViewHandler<TItemsView> where TItemsView : GroupableItemsView
{
protected override Android.Views.View CreateNativeView()
{
throw new NotImplementedException();
}
public static void MapIsGrouped(GroupableItemsViewHandler<TItemsView> handler, GroupableItemsView itemsView)
{
}
}
}

Просмотреть файл

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class GroupableItemsViewHandler<TItemsView> : SelectableItemsViewHandler<TItemsView> where TItemsView : GroupableItemsView
{
protected override object CreateNativeView()
{
throw new NotImplementedException();
}
public static void MapIsGrouped(GroupableItemsViewHandler<TItemsView> handler, GroupableItemsView itemsView)
{
}
}
}

Просмотреть файл

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class GroupableItemsViewHandler<TItemsView> : SelectableItemsViewHandler<TItemsView> where TItemsView : GroupableItemsView
{
protected override UserControl CreateNativeView()
{
throw new NotImplementedException();
}
public static void MapIsGrouped(GroupableItemsViewHandler<TItemsView> handler, GroupableItemsView itemsView)
{
}
}
}

Просмотреть файл

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class GroupableItemsViewHandler<TItemsView> where TItemsView : GroupableItemsView
{
public GroupableItemsViewHandler() : base(GroupableItemsViewMapper)
{
}
public GroupableItemsViewHandler(PropertyMapper mapper = null) : base(mapper ?? GroupableItemsViewMapper)
{
}
public static PropertyMapper<TItemsView, GroupableItemsViewHandler<TItemsView>> GroupableItemsViewMapper = new PropertyMapper<TItemsView, GroupableItemsViewHandler<TItemsView>>(SelectableItemsViewHandler<SelectableItemsView>.SelectableItemsViewMapper)
{
[Controls.ItemsView.ItemsSourceProperty.PropertyName] = MapItemsSource,
[Controls.ItemsView.HorizontalScrollBarVisibilityProperty.PropertyName] = MapHorizontalScrollBarVisibility,
[Controls.ItemsView.VerticalScrollBarVisibilityProperty.PropertyName] = MapVerticalScrollBarVisibility,
[Controls.ItemsView.ItemTemplateProperty.PropertyName] = MapItemTemplate,
[Controls.ItemsView.EmptyViewProperty.PropertyName] = MapEmptyView,
[Controls.ItemsView.EmptyViewTemplateProperty.PropertyName] = MapEmptyViewTemplate,
[Controls.ItemsView.FlowDirectionProperty.PropertyName] = MapFlowDirection,
[Controls.ItemsView.IsVisibleProperty.PropertyName] = MapIsVisible,
[Controls.ItemsView.ItemsUpdatingScrollModeProperty.PropertyName] = MapItemsUpdatingScrollMode,
[StructuredItemsView.HeaderTemplateProperty.PropertyName] = MapHeaderTemplate,
[StructuredItemsView.FooterTemplateProperty.PropertyName] = MapFooterTemplate,
[StructuredItemsView.ItemsLayoutProperty.PropertyName] = MapItemsLayout,
[StructuredItemsView.ItemSizingStrategyProperty.PropertyName] = MapItemSizingStrategy,
[SelectableItemsView.SelectedItemProperty.PropertyName] = MapSelectedItem,
[SelectableItemsView.SelectedItemsProperty.PropertyName] = MapSelectedItems,
[SelectableItemsView.SelectionModeProperty.PropertyName] = MapSelectionMode,
[GroupableItemsView.IsGroupedProperty.PropertyName] = MapIsGrouped
};
}
}

Просмотреть файл

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Text;
using Foundation;
using Microsoft.Maui.Handlers;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class GroupableItemsViewHandler<TItemsView> : SelectableItemsViewHandler<TItemsView> where TItemsView : GroupableItemsView
{
protected override ItemsViewController<TItemsView> CreateController(TItemsView itemsView, ItemsViewLayout layout)
=> new GroupableItemsViewController<TItemsView>(itemsView, layout);
protected override void ScrollToRequested(object sender, ScrollToRequestEventArgs args)
{
if (WillNeedScrollAdjustment(args))
{
if (args.IsAnimated)
{
(Controller as GroupableItemsViewController<TItemsView>).SetScrollAnimationEndedCallback(() => base.ScrollToRequested(sender, args));
}
else
{
base.ScrollToRequested(sender, args);
}
}
base.ScrollToRequested(sender, args);
}
public static void MapIsGrouped(GroupableItemsViewHandler<TItemsView> handler, GroupableItemsView itemsView)
{
handler.Controller?.UpdateItemsSource();
}
bool WillNeedScrollAdjustment(ScrollToRequestEventArgs args)
{
return ItemsView.ItemSizingStrategy == ItemSizingStrategy.MeasureAllItems
&& ItemsView.IsGrouped
&& (args.ScrollToPosition == ScrollToPosition.End || args.ScrollToPosition == ScrollToPosition.MakeVisible);
}
}
}

Просмотреть файл

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public abstract partial class ItemsViewHandler<TItemsView> : ViewHandler<TItemsView, Android.Views.View> where TItemsView : ItemsView
{
protected ItemsViewHandler(PropertyMapper mapper, CommandMapper commandMapper = null) : base(mapper, commandMapper)
{
}
protected override Android.Views.View CreateNativeView()
{
throw new NotImplementedException();
}
public static void MapItemsSource(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
// handler.NativeView?.UpdateBackground(entry);
}
public static void MapHorizontalScrollBarVisibility(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapVerticalScrollBarVisibility(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapItemTemplate(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapEmptyView(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapEmptyViewTemplate(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapFlowDirection(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapIsVisible(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapItemsUpdatingScrollMode(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
}
}

Просмотреть файл

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public abstract partial class ItemsViewHandler<TItemsView> : ViewHandler<TItemsView, object> where TItemsView : ItemsView
{
protected override object CreateNativeView()
{
throw new NotImplementedException();
}
public static void MapItemsSource(ItemsViewHandler<TItemsView> handler, ItemsView itemsView) { }
public static void MapHorizontalScrollBarVisibility(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapVerticalScrollBarVisibility(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapItemTemplate(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapEmptyView(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapEmptyViewTemplate(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapFlowDirection(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapIsVisible(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapItemsUpdatingScrollMode(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
}
}

Просмотреть файл

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public abstract partial class ItemsViewHandler<TItemsView> : ViewHandler<TItemsView, UserControl> where TItemsView : ItemsView
{
protected override UserControl CreateNativeView()
{
throw new NotImplementedException();
}
public static void MapItemsSource(ItemsViewHandler<TItemsView> handler, ItemsView itemsView) { }
public static void MapHorizontalScrollBarVisibility(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapVerticalScrollBarVisibility(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapItemTemplate(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapEmptyView(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapEmptyViewTemplate(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapFlowDirection(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapIsVisible(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
public static void MapItemsUpdatingScrollMode(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
}
}
}

Просмотреть файл

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public abstract partial class ItemsViewHandler<TItemsView> where TItemsView : ItemsView
{
public ItemsViewHandler() : base(ItemsViewMapper)
{
}
public ItemsViewHandler(PropertyMapper mapper = null) : base(mapper ?? ItemsViewMapper)
{
}
public static PropertyMapper<TItemsView, ItemsViewHandler<TItemsView>> ItemsViewMapper = new PropertyMapper<TItemsView, ItemsViewHandler<TItemsView>>(ViewHandler.ViewMapper)
{ [Controls.ItemsView.ItemsSourceProperty.PropertyName] = MapItemsSource,
[Controls.ItemsView.HorizontalScrollBarVisibilityProperty.PropertyName] = MapHorizontalScrollBarVisibility,
[Controls.ItemsView.VerticalScrollBarVisibilityProperty.PropertyName] = MapVerticalScrollBarVisibility,
[Controls.ItemsView.ItemTemplateProperty.PropertyName] = MapItemTemplate,
[Controls.ItemsView.EmptyViewProperty.PropertyName] = MapEmptyView,
[Controls.ItemsView.EmptyViewTemplateProperty.PropertyName] = MapEmptyViewTemplate,
[Controls.ItemsView.FlowDirectionProperty.PropertyName] = MapFlowDirection,
[Controls.ItemsView.IsVisibleProperty.PropertyName] = MapIsVisible,
[Controls.ItemsView.ItemsUpdatingScrollModeProperty.PropertyName] = MapItemsUpdatingScrollMode,
};
}
}

Просмотреть файл

@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Text;
using Foundation;
using Microsoft.Maui.Handlers;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public abstract partial class ItemsViewHandler<TItemsView> : ViewHandler<TItemsView, UIView> where TItemsView : ItemsView
{
ItemsViewLayout _layout;
protected override void DisconnectHandler(UIView nativeView)
{
ItemsView.ScrollToRequested -= ScrollToRequested;
base.DisconnectHandler(nativeView);
}
protected override void ConnectHandler(UIView nativeView)
{
base.ConnectHandler(nativeView);
Controller.CollectionView.BackgroundColor = UIColor.Clear;
ItemsView.ScrollToRequested += ScrollToRequested;
}
private protected override UIView OnCreateNativeView()
{
UpdateLayout();
Controller = CreateController(ItemsView, _layout);
return base.OnCreateNativeView();
}
protected TItemsView ItemsView => VirtualView;
protected ItemsViewController<TItemsView> Controller { get; private set; }
protected abstract ItemsViewLayout SelectLayout();
protected abstract ItemsViewController<TItemsView> CreateController(TItemsView newElement, ItemsViewLayout layout);
protected override UIView CreateNativeView() => Controller?.View;
public static void MapItemsSource(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
MapItemsUpdatingScrollMode(handler, itemsView);
handler.Controller?.UpdateItemsSource();
}
public static void MapHorizontalScrollBarVisibility(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
handler.Controller?.CollectionView?.UpdateHorizontalScrollBarVisibility(itemsView.HorizontalScrollBarVisibility);
}
public static void MapVerticalScrollBarVisibility(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
handler.Controller?.CollectionView?.UpdateVerticalScrollBarVisibility(itemsView.VerticalScrollBarVisibility);
}
public static void MapItemTemplate(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
handler.UpdateLayout();
}
public static void MapEmptyView(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
handler.Controller?.UpdateEmptyView();
}
public static void MapEmptyViewTemplate(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
handler.Controller?.UpdateEmptyView();
}
public static void MapFlowDirection(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
handler.Controller?.UpdateFlowDirection();
}
public static void MapIsVisible(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
handler.Controller?.UpdateVisibility();
}
public static void MapItemsUpdatingScrollMode(ItemsViewHandler<TItemsView> handler, ItemsView itemsView)
{
handler._layout.ItemsUpdatingScrollMode = itemsView.ItemsUpdatingScrollMode;
}
protected virtual void UpdateLayout()
{
_layout = SelectLayout();
Controller?.UpdateLayout(_layout);
}
protected virtual void ScrollToRequested(object sender, ScrollToRequestEventArgs args)
{
using (var indexPath = DetermineIndex(args))
{
if (!IsIndexPathValid(indexPath))
{
// Specified path wasn't valid, or item wasn't found
return;
}
Controller.CollectionView.ScrollToItem(indexPath,
args.ScrollToPosition.ToCollectionViewScrollPosition(_layout.ScrollDirection), args.IsAnimated);
}
NSIndexPath DetermineIndex(ScrollToRequestEventArgs args)
{
if (args.Mode == ScrollToMode.Position)
{
if (args.GroupIndex == -1)
{
return NSIndexPath.Create(0, args.Index);
}
return NSIndexPath.Create(args.GroupIndex, args.Index);
}
return Controller.GetIndexForItem(args.Item);
}
}
protected bool IsIndexPathValid(NSIndexPath indexPath)
{
if (indexPath.Item < 0 || indexPath.Section < 0)
{
return false;
}
var collectionView = Controller.CollectionView;
if (indexPath.Section >= collectionView.NumberOfSections())
{
return false;
}
if (indexPath.Item >= collectionView.NumberOfItemsInSection(indexPath.Section))
{
return false;
}
return true;
}
}
}

Просмотреть файл

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class SelectableItemsViewHandler<TItemsView> : StructuredItemsViewHandler<TItemsView> where TItemsView : SelectableItemsView
{
protected override Android.Views.View CreateNativeView()
{
throw new NotImplementedException();
}
public static void MapSelectedItem(SelectableItemsViewHandler<TItemsView> handler, SelectableItemsView itemsView)
{
}
public static void MapSelectedItems(SelectableItemsViewHandler<TItemsView> handler, SelectableItemsView itemsView)
{
}
public static void MapSelectionMode(SelectableItemsViewHandler<TItemsView> handler, SelectableItemsView itemsView)
{
}
}
}

Просмотреть файл

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class SelectableItemsViewHandler<TItemsView> : StructuredItemsViewHandler<TItemsView> where TItemsView : SelectableItemsView
{
protected override object CreateNativeView()
{
throw new NotImplementedException();
}
public static void MapSelectedItem(SelectableItemsViewHandler<TItemsView> handler, SelectableItemsView itemsView)
{
}
public static void MapSelectedItems(SelectableItemsViewHandler<TItemsView> handler, SelectableItemsView itemsView)
{
}
public static void MapSelectionMode(SelectableItemsViewHandler<TItemsView> handler, SelectableItemsView itemsView)
{
}
}
}

Просмотреть файл

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class SelectableItemsViewHandler<TItemsView> : StructuredItemsViewHandler<TItemsView> where TItemsView : SelectableItemsView
{
protected override UserControl CreateNativeView()
{
throw new NotImplementedException();
}
public static void MapSelectedItem(SelectableItemsViewHandler<TItemsView> handler, SelectableItemsView itemsView)
{
}
public static void MapSelectedItems(SelectableItemsViewHandler<TItemsView> handler, SelectableItemsView itemsView)
{
}
public static void MapSelectionMode(SelectableItemsViewHandler<TItemsView> handler, SelectableItemsView itemsView)
{
}
}
}

Просмотреть файл

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class SelectableItemsViewHandler<TItemsView> where TItemsView : SelectableItemsView
{
public SelectableItemsViewHandler() : base(SelectableItemsViewMapper)
{
}
public SelectableItemsViewHandler(PropertyMapper mapper = null) : base(mapper ?? SelectableItemsViewMapper)
{
}
public static PropertyMapper<TItemsView, SelectableItemsViewHandler<TItemsView>> SelectableItemsViewMapper = new PropertyMapper<TItemsView, SelectableItemsViewHandler<TItemsView>>(ViewMapper)
{
[Controls.ItemsView.ItemsSourceProperty.PropertyName] = MapItemsSource,
[Controls.ItemsView.HorizontalScrollBarVisibilityProperty.PropertyName] = MapHorizontalScrollBarVisibility,
[Controls.ItemsView.VerticalScrollBarVisibilityProperty.PropertyName] = MapVerticalScrollBarVisibility,
[Controls.ItemsView.ItemTemplateProperty.PropertyName] = MapItemTemplate,
[Controls.ItemsView.EmptyViewProperty.PropertyName] = MapEmptyView,
[Controls.ItemsView.EmptyViewTemplateProperty.PropertyName] = MapEmptyViewTemplate,
[Controls.ItemsView.FlowDirectionProperty.PropertyName] = MapFlowDirection,
[Controls.ItemsView.IsVisibleProperty.PropertyName] = MapIsVisible,
[Controls.ItemsView.ItemsUpdatingScrollModeProperty.PropertyName] = MapItemsUpdatingScrollMode,
[StructuredItemsView.HeaderTemplateProperty.PropertyName] = MapHeaderTemplate,
[StructuredItemsView.FooterTemplateProperty.PropertyName] = MapFooterTemplate,
[StructuredItemsView.ItemsLayoutProperty.PropertyName] = MapItemsLayout,
[StructuredItemsView.ItemSizingStrategyProperty.PropertyName] = MapItemSizingStrategy,
[SelectableItemsView.SelectedItemProperty.PropertyName] = MapSelectedItem,
[SelectableItemsView.SelectedItemsProperty.PropertyName] = MapSelectedItems,
[SelectableItemsView.SelectionModeProperty.PropertyName] = MapSelectionMode,
};
}
}

Просмотреть файл

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Text;
using Foundation;
using Microsoft.Maui.Handlers;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class SelectableItemsViewHandler<TItemsView> : StructuredItemsViewHandler<TItemsView> where TItemsView : SelectableItemsView
{
protected override ItemsViewController<TItemsView> CreateController(TItemsView itemsView, ItemsViewLayout layout)
=> new SelectableItemsViewController<TItemsView>(itemsView, layout);
public static void MapItemsSource(SelectableItemsViewHandler<TItemsView> handler, SelectableItemsView itemsView)
{
ItemsViewHandler<TItemsView>.MapItemsSource(handler, itemsView);
MapSelectedItem(handler, itemsView);
}
public static void MapSelectedItem(SelectableItemsViewHandler<TItemsView> handler, SelectableItemsView itemsView)
{
(handler.Controller as SelectableItemsViewController<TItemsView>)?.UpdateNativeSelection();
}
public static void MapSelectedItems(SelectableItemsViewHandler<TItemsView> handler, SelectableItemsView itemsView)
{
(handler.Controller as SelectableItemsViewController<TItemsView>)?.UpdateNativeSelection();
}
public static void MapSelectionMode(SelectableItemsViewHandler<TItemsView> handler, SelectableItemsView itemsView)
{
(handler.Controller as SelectableItemsViewController<TItemsView>)?.UpdateSelectionMode();
}
}
}

Просмотреть файл

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class StructuredItemsViewHandler<TItemsView> : ItemsViewHandler<TItemsView> where TItemsView : StructuredItemsView
{
protected override Android.Views.View CreateNativeView()
{
throw new NotImplementedException();
}
public static void MapHeaderTemplate(StructuredItemsViewHandler<TItemsView> handler, StructuredItemsView itemsView)
{
}
public static void MapFooterTemplate(StructuredItemsViewHandler<TItemsView> handler, StructuredItemsView itemsView)
{
}
public static void MapItemsLayout(StructuredItemsViewHandler<TItemsView> handler, StructuredItemsView itemsView)
{
}
public static void MapItemSizingStrategy(StructuredItemsViewHandler<TItemsView> handler, StructuredItemsView itemsView)
{
}
}
}

Просмотреть файл

@ -0,0 +1,31 @@
using System;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class StructuredItemsViewHandler<TItemsView> : ItemsViewHandler<TItemsView> where TItemsView : StructuredItemsView
{
protected override object CreateNativeView()
{
throw new NotImplementedException();
}
public static void MapHeaderTemplate(StructuredItemsViewHandler<TItemsView> handler, StructuredItemsView itemsView)
{
}
public static void MapFooterTemplate(StructuredItemsViewHandler<TItemsView> handler, StructuredItemsView itemsView)
{
}
public static void MapItemsLayout(StructuredItemsViewHandler<TItemsView> handler, StructuredItemsView itemsView)
{
}
public static void MapItemSizingStrategy(StructuredItemsViewHandler<TItemsView> handler, StructuredItemsView itemsView)
{
}
}
}

Просмотреть файл

@ -0,0 +1,32 @@
using System;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class StructuredItemsViewHandler<TItemsView> : ItemsViewHandler<TItemsView> where TItemsView : StructuredItemsView
{
protected override UserControl CreateNativeView()
{
throw new NotImplementedException();
}
public static void MapHeaderTemplate(StructuredItemsViewHandler<TItemsView> handler, StructuredItemsView itemsView)
{
}
public static void MapFooterTemplate(StructuredItemsViewHandler<TItemsView> handler, StructuredItemsView itemsView)
{
}
public static void MapItemsLayout(StructuredItemsViewHandler<TItemsView> handler, StructuredItemsView itemsView)
{
}
public static void MapItemSizingStrategy(StructuredItemsViewHandler<TItemsView> handler, StructuredItemsView itemsView)
{
}
}
}

Просмотреть файл

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class StructuredItemsViewHandler<TItemsView> where TItemsView : StructuredItemsView
{
public StructuredItemsViewHandler() : base(StructuredItemsViewMapper)
{
}
public StructuredItemsViewHandler(PropertyMapper mapper = null) : base(mapper ?? StructuredItemsViewMapper)
{
}
public static PropertyMapper<TItemsView, StructuredItemsViewHandler<TItemsView>> StructuredItemsViewMapper = new PropertyMapper<TItemsView, StructuredItemsViewHandler<TItemsView>>(ViewMapper)
{
[Controls.ItemsView.ItemsSourceProperty.PropertyName] = MapItemsSource,
[Controls.ItemsView.HorizontalScrollBarVisibilityProperty.PropertyName] = MapHorizontalScrollBarVisibility,
[Controls.ItemsView.VerticalScrollBarVisibilityProperty.PropertyName] = MapVerticalScrollBarVisibility,
[Controls.ItemsView.ItemTemplateProperty.PropertyName] = MapItemTemplate,
[Controls.ItemsView.EmptyViewProperty.PropertyName] = MapEmptyView,
[Controls.ItemsView.EmptyViewTemplateProperty.PropertyName] = MapEmptyViewTemplate,
[Controls.ItemsView.FlowDirectionProperty.PropertyName] = MapFlowDirection,
[Controls.ItemsView.IsVisibleProperty.PropertyName] = MapIsVisible,
[Controls.ItemsView.ItemsUpdatingScrollModeProperty.PropertyName] = MapItemsUpdatingScrollMode,
[StructuredItemsView.HeaderTemplateProperty.PropertyName] = MapHeaderTemplate,
[StructuredItemsView.FooterTemplateProperty.PropertyName] = MapFooterTemplate,
[StructuredItemsView.ItemsLayoutProperty.PropertyName] = MapItemsLayout,
[StructuredItemsView.ItemSizingStrategyProperty.PropertyName] = MapItemSizingStrategy
};
}
}

Просмотреть файл

@ -0,0 +1,49 @@
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public partial class StructuredItemsViewHandler<TItemsView> : ItemsViewHandler<TItemsView> where TItemsView : StructuredItemsView
{
protected override ItemsViewController<TItemsView> CreateController(TItemsView itemsView, ItemsViewLayout layout)
=> new StructuredItemsViewController<TItemsView>(itemsView, layout);
protected override ItemsViewLayout SelectLayout()
{
var itemSizingStrategy = ItemsView.ItemSizingStrategy;
var itemsLayout = ItemsView.ItemsLayout;
if (itemsLayout is GridItemsLayout gridItemsLayout)
{
return new GridViewLayout(gridItemsLayout, itemSizingStrategy);
}
if (itemsLayout is LinearItemsLayout listItemsLayout)
{
return new ListViewLayout(listItemsLayout, itemSizingStrategy);
}
// Fall back to vertical list
return new ListViewLayout(new LinearItemsLayout(ItemsLayoutOrientation.Vertical), itemSizingStrategy);
}
public static void MapHeaderTemplate(StructuredItemsViewHandler<TItemsView> handler, StructuredItemsView itemsView)
{
(handler.Controller as StructuredItemsViewController<TItemsView>)?.UpdateHeaderView();
}
public static void MapFooterTemplate(StructuredItemsViewHandler<TItemsView> handler, StructuredItemsView itemsView)
{
(handler.Controller as StructuredItemsViewController<TItemsView>)?.UpdateFooterView();
}
public static void MapItemsLayout(StructuredItemsViewHandler<TItemsView> handler, StructuredItemsView itemsView)
{
handler.UpdateLayout();
}
public static void MapItemSizingStrategy(StructuredItemsViewHandler<TItemsView> handler, StructuredItemsView itemsView)
{
handler.UpdateLayout();
}
}
}

Просмотреть файл

@ -0,0 +1,46 @@
using System;
using CoreGraphics;
using Foundation;
using Microsoft.Maui.Graphics;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public class CarouselTemplatedCell : TemplatedCell
{
public static NSString ReuseId = new NSString("Microsoft.Maui.Controls.Compatibility.Platform.iOS.CarouselTemplatedCell");
CGSize _constraint;
[Export("initWithFrame:")]
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
protected CarouselTemplatedCell(CGRect frame) : base(frame)
{
}
public override void ConstrainTo(nfloat constant)
{
}
public override void ConstrainTo(CGSize constraint)
{
ClearConstraints();
_constraint = constraint;
}
public override CGSize Measure()
{
return new CGSize(_constraint.Width, _constraint.Height);
}
protected override (bool, Size) NeedsContentSizeUpdate(Size currentSize)
{
return (false, Size.Zero);
}
protected override bool AttributesConsistentWithConstrainedDimension(UICollectionViewLayoutAttributes attributes)
{
return false;
}
}
}

Просмотреть файл

@ -0,0 +1,689 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using CoreGraphics;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public class CarouselViewController : ItemsViewController<CarouselView>
{
protected readonly CarouselView Carousel;
CarouselViewLoopManager _carouselViewLoopManager;
bool _initialPositionSet;
bool _updatingScrollOffset;
List<View> _oldViews;
int _gotoPosition = -1;
CGSize _size;
ILoopItemsViewSource LoopItemsSource => ItemsSource as ILoopItemsViewSource;
bool _isDragging;
public CarouselViewController(CarouselView itemsView, ItemsViewLayout layout) : base(itemsView, layout)
{
Carousel = itemsView;
CollectionView.AllowsSelection = false;
CollectionView.AllowsMultipleSelection = false;
Carousel.PropertyChanged += CarouselViewPropertyChanged;
Carousel.Scrolled += CarouselViewScrolled;
_oldViews = new List<View>();
}
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
UICollectionViewCell cell;
if (Carousel?.Loop == true && _carouselViewLoopManager != null)
{
var cellAndCorrectedIndex = _carouselViewLoopManager.GetCellAndCorrectIndex(collectionView, indexPath, DetermineCellReuseId());
cell = cellAndCorrectedIndex.cell;
var correctedIndexPath = NSIndexPath.FromRowSection(cellAndCorrectedIndex.correctedIndex, 0);
if (cell is DefaultCell defaultCell)
UpdateDefaultCell(defaultCell, correctedIndexPath);
if (cell is TemplatedCell templatedCell)
UpdateTemplatedCell(templatedCell, correctedIndexPath);
}
else
{
cell = base.GetCell(collectionView, indexPath);
}
var element = (cell as TemplatedCell)?.NativeHandler?.VirtualView as VisualElement;
if (element != null)
VisualStateManager.GoToState(element, CarouselView.DefaultItemVisualState);
return cell;
}
public override nint GetItemsCount(UICollectionView collectionView, nint section) => LoopItemsSource.LoopCount;
public override void ViewDidLoad()
{
_carouselViewLoopManager = new CarouselViewLoopManager(Layout as UICollectionViewFlowLayout);
base.ViewDidLoad();
}
public override void ViewWillLayoutSubviews()
{
base.ViewWillLayoutSubviews();
UpdateVisualStates();
}
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
if (Carousel?.Loop == true && _carouselViewLoopManager != null)
{
_updatingScrollOffset = true;
_carouselViewLoopManager.CenterIfNeeded(CollectionView, IsHorizontal);
_updatingScrollOffset = false;
}
if (CollectionView.Bounds.Size != _size)
{
_size = CollectionView.Bounds.Size;
BoundsSizeChanged();
}
else
{
UpdateInitialPosition();
}
}
void BoundsSizeChanged()
{
//if the size changed center the item
Carousel.ScrollTo(Carousel.Position, position: Microsoft.Maui.Controls.ScrollToPosition.Center, animate: false);
}
public override void DraggingStarted(UIScrollView scrollView)
{
_isDragging = true;
Carousel.SetIsDragging(true);
}
public override void DraggingEnded(UIScrollView scrollView, bool willDecelerate)
{
Carousel.SetIsDragging(false);
_isDragging = false;
}
public override void UpdateItemsSource()
{
UnsubscribeCollectionItemsSourceChanged(ItemsSource);
base.UpdateItemsSource();
//we don't need to Subscribe because base calls CreateItemsViewSource
_carouselViewLoopManager?.SetItemsSource(LoopItemsSource);
if (_initialPositionSet)
{
Carousel.SetValueFromRenderer(CarouselView.CurrentItemProperty, null);
Carousel.SetValueFromRenderer(CarouselView.PositionProperty, 0);
}
_initialPositionSet = false;
UpdateInitialPosition();
}
protected override bool IsHorizontal => (Carousel?.ItemsLayout)?.Orientation == ItemsLayoutOrientation.Horizontal;
protected override UICollectionViewDelegateFlowLayout CreateDelegator() => new CarouselViewDelegator(ItemsViewLayout, this);
protected override string DetermineCellReuseId()
{
if (Carousel.ItemTemplate != null)
return CarouselTemplatedCell.ReuseId;
return base.DetermineCellReuseId();
}
protected override void RegisterViewTypes()
{
CollectionView.RegisterClassForCell(typeof(CarouselTemplatedCell), CarouselTemplatedCell.ReuseId);
base.RegisterViewTypes();
}
protected override IItemsViewSource CreateItemsViewSource()
{
var itemsSource = ItemsSourceFactory.CreateForCarouselView(Carousel.ItemsSource, this, Carousel.Loop);
_carouselViewLoopManager?.SetItemsSource(itemsSource);
SubscribeCollectionItemsSourceChanged(itemsSource);
return itemsSource;
}
protected override void CacheCellAttributes(NSIndexPath indexPath, CGSize size)
{
var itemIndex = GetIndexFromIndexPath(indexPath);
base.CacheCellAttributes(NSIndexPath.FromItemSection(itemIndex, 0), size);
}
internal void TearDown()
{
Carousel.PropertyChanged -= CarouselViewPropertyChanged;
Carousel.Scrolled -= CarouselViewScrolled;
UnsubscribeCollectionItemsSourceChanged(ItemsSource);
_carouselViewLoopManager?.Dispose();
_carouselViewLoopManager = null;
}
internal void UpdateIsScrolling(bool isScrolling) => Carousel.IsScrolling = isScrolling;
internal NSIndexPath GetScrollToIndexPath(int position)
{
if (Carousel?.Loop == true && _carouselViewLoopManager != null)
return _carouselViewLoopManager.GetGoToIndex(CollectionView, position);
return NSIndexPath.FromItemSection(position, 0);
}
internal int GetIndexFromIndexPath(NSIndexPath indexPath)
{
if (Carousel?.Loop == true && _carouselViewLoopManager != null)
return _carouselViewLoopManager.GetCorrectedIndexFromIndexPath(indexPath);
return indexPath.Row;
}
void CarouselViewScrolled(object sender, ItemsViewScrolledEventArgs e)
{
if (_updatingScrollOffset)
return;
if (_isDragging)
{
return;
}
SetPosition(e.CenterItemIndex);
UpdateVisualStates();
}
int _positionAfterUpdate = -1;
void CollectionViewUpdating(object sender, NotifyCollectionChangedEventArgs e)
{
int carouselPosition = Carousel.Position;
_positionAfterUpdate = carouselPosition;
var currentItemPosition = ItemsSource.GetIndexForItem(Carousel.CurrentItem).Row;
var count = ItemsSource.ItemCount;
if (e.Action == NotifyCollectionChangedAction.Remove)
_positionAfterUpdate = GetPositionWhenRemovingItems(e.OldStartingIndex, carouselPosition, currentItemPosition, count);
if (e.Action == NotifyCollectionChangedAction.Reset)
_positionAfterUpdate = GetPositionWhenResetItems();
if (e.Action == NotifyCollectionChangedAction.Add)
_positionAfterUpdate = GetPositionWhenAddingItems(carouselPosition, currentItemPosition);
}
void CollectionViewUpdated(object sender, NotifyCollectionChangedEventArgs e)
{
if (_positionAfterUpdate == -1)
{
return;
}
_gotoPosition = -1;
var targetPosition = _positionAfterUpdate;
_positionAfterUpdate = -1;
SetPosition(targetPosition);
SetCurrentItem(targetPosition);
}
int GetPositionWhenAddingItems(int carouselPosition, int currentItemPosition)
{
//If we are adding a new item make sure to maintain the CurrentItemPosition
return currentItemPosition != -1 ? currentItemPosition : carouselPosition;
}
int GetPositionWhenResetItems()
{
//If we are reseting the collection Position should go to 0
Carousel.SetValueFromRenderer(CarouselView.CurrentItemProperty, null);
return 0;
}
int GetPositionWhenRemovingItems(int oldStartingIndex, int carouselPosition, int currentItemPosition, int count)
{
bool removingCurrentElement = currentItemPosition == -1;
bool removingFirstElement = oldStartingIndex == 0;
bool removingLastElement = oldStartingIndex == count;
bool removingCurrentElementAndLast = removingCurrentElement && removingLastElement && Carousel.Position > 0;
if (removingCurrentElementAndLast)
{
//If we are removing the last element update the position
carouselPosition = Carousel.Position - 1;
}
else if (removingFirstElement && !removingCurrentElement)
{
//If we are not removing the current element set position to the CurrentItem
carouselPosition = currentItemPosition;
}
return carouselPosition;
}
void SubscribeCollectionItemsSourceChanged(IItemsViewSource itemsSource)
{
if (itemsSource is ObservableItemsSource newItemsSource)
{
newItemsSource.CollectionViewUpdating += CollectionViewUpdating;
newItemsSource.CollectionViewUpdated += CollectionViewUpdated;
}
}
void UnsubscribeCollectionItemsSourceChanged(IItemsViewSource oldItemsSource)
{
if (oldItemsSource is ObservableItemsSource oldObservableItemsSource)
{
oldObservableItemsSource.CollectionViewUpdating -= CollectionViewUpdating;
oldObservableItemsSource.CollectionViewUpdated -= CollectionViewUpdated;
}
}
void CarouselViewPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs changedProperty)
{
if (changedProperty.Is(CarouselView.PositionProperty))
UpdateFromPosition();
else if (changedProperty.Is(CarouselView.CurrentItemProperty))
UpdateFromCurrentItem();
else if (changedProperty.Is(CarouselView.LoopProperty))
UpdateLoop();
}
void UpdateLoop()
{
var carouselPosition = Carousel.Position;
if (LoopItemsSource != null)
LoopItemsSource.Loop = Carousel.Loop;
CollectionView.ReloadData();
ScrollToPosition(carouselPosition, carouselPosition, false, true);
}
void ScrollToPosition(int goToPosition, int carouselPosition, bool animate, bool forceScroll = false)
{
if (Carousel.Loop)
carouselPosition = _carouselViewLoopManager?.GetCorrectPositionForCenterItem(CollectionView) ?? -1;
//no center item found, collection could be empty
//also if we are dragging we don't need to ScrollTo
if (Carousel.IsDragging || carouselPosition == -1)
return;
if (_gotoPosition == -1 && (goToPosition != carouselPosition || forceScroll))
{
_gotoPosition = goToPosition;
Carousel.ScrollTo(goToPosition, position: Microsoft.Maui.Controls.ScrollToPosition.Center, animate: animate);
}
}
void SetPosition(int position)
{
if (position == -1)
return;
//we arrived center
if (position == _gotoPosition)
_gotoPosition = -1;
//If _gotoPosition is != -1 we are scrolling to that possition
if (_gotoPosition == -1 && Carousel.Position != position)
Carousel.SetValueFromRenderer(CarouselView.PositionProperty, position);
}
void SetCurrentItem(int carouselPosition)
{
if (ItemsSource.ItemCount == 0)
return;
var item = GetItemAtIndex(NSIndexPath.FromItemSection(carouselPosition, 0));
Carousel.SetValueFromRenderer(CarouselView.CurrentItemProperty, item);
UpdateVisualStates();
}
void UpdateFromCurrentItem()
{
if (Carousel?.CurrentItem == null || ItemsSource == null || ItemsSource.ItemCount == 0)
return;
var currentItemPosition = GetIndexForItem(Carousel.CurrentItem).Row;
ScrollToPosition(currentItemPosition, Carousel.Position, Carousel.AnimateCurrentItemChanges);
UpdateVisualStates();
}
void UpdateFromPosition()
{
var itemsCount = ItemsSource?.ItemCount;
if (itemsCount == 0)
return;
var currentItemPosition = GetIndexForItem(Carousel.CurrentItem).Row;
var carouselPosition = Carousel.Position;
if (carouselPosition == _gotoPosition)
_gotoPosition = -1;
ScrollToPosition(carouselPosition, currentItemPosition, Carousel.AnimatePositionChanges);
SetCurrentItem(carouselPosition);
}
void UpdateInitialPosition()
{
var itemsCount = ItemsSource?.ItemCount;
if (itemsCount == 0)
return;
if (!_initialPositionSet)
{
_initialPositionSet = true;
int position = Carousel.Position;
var currentItem = Carousel.CurrentItem;
if (currentItem != null)
{
position = ItemsSource.GetIndexForItem(currentItem).Row;
}
else
{
SetCurrentItem(position);
}
Carousel.ScrollTo(position, -1, Microsoft.Maui.Controls.ScrollToPosition.Center, false);
}
UpdateVisualStates();
}
void UpdateVisualStates()
{
var cells = CollectionView.VisibleCells;
var newViews = new List<View>();
var carouselPosition = Carousel.Position;
var previousPosition = carouselPosition - 1;
var nextPosition = carouselPosition + 1;
foreach (var cell in cells)
{
if (!((cell as CarouselTemplatedCell)?.NativeHandler?.VirtualView is View itemView))
return;
var item = itemView.BindingContext;
var pos = ItemsSource.GetIndexForItem(item).Row;
if (pos == carouselPosition)
{
VisualStateManager.GoToState(itemView, CarouselView.CurrentItemVisualState);
}
else if (pos == previousPosition)
{
VisualStateManager.GoToState(itemView, CarouselView.PreviousItemVisualState);
}
else if (pos == nextPosition)
{
VisualStateManager.GoToState(itemView, CarouselView.NextItemVisualState);
}
else
{
VisualStateManager.GoToState(itemView, CarouselView.DefaultItemVisualState);
}
newViews.Add(itemView);
if (!Carousel.VisibleViews.Contains(itemView))
{
Carousel.VisibleViews.Add(itemView);
}
}
foreach (var itemView in _oldViews)
{
if (!newViews.Contains(itemView))
{
VisualStateManager.GoToState(itemView, CarouselView.DefaultItemVisualState);
if (Carousel.VisibleViews.Contains(itemView))
{
Carousel.VisibleViews.Remove(itemView);
}
}
}
_oldViews = newViews;
}
protected internal override void UpdateVisibility()
{
if (ItemsView.IsVisible)
{
CollectionView.Hidden = false;
}
else
{
CollectionView.Hidden = true;
}
}
}
class CarouselViewLoopManager : IDisposable
{
int _indexOffset = 0;
UICollectionViewFlowLayout _layout;
const int LoopCount = 3;
ILoopItemsViewSource _itemsSource;
bool _disposed;
public CarouselViewLoopManager(UICollectionViewFlowLayout layout)
{
if (layout == null)
throw new ArgumentNullException(nameof(layout), "LoopManager expects a UICollectionViewFlowLayout");
_layout = layout;
}
public void CenterIfNeeded(UICollectionView collectionView, bool isHorizontal)
{
if (isHorizontal)
CenterHorizontalIfNeeded(collectionView);
else
CenterVerticallyIfNeeded(collectionView);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_itemsSource = null;
}
_disposed = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
public (UICollectionViewCell cell, int correctedIndex) GetCellAndCorrectIndex(UICollectionView collectionView, NSIndexPath indexPath, string reuseId)
{
var cell = collectionView.DequeueReusableCell(reuseId, indexPath) as UICollectionViewCell;
var correctedIndex = GetCorrectedIndexFromIndexPath(indexPath);
return (cell, correctedIndex);
}
public int GetCorrectedIndexFromIndexPath(NSIndexPath indexPath)
{
return GetCorrectedIndex(indexPath.Row - _indexOffset);
}
public int GetCorrectPositionForCenterItem(UICollectionView collectionView)
{
NSIndexPath centerIndexPath = GetIndexPathForCenteredItem(collectionView);
if (centerIndexPath == null)
return -1;
return GetCorrectedIndexFromIndexPath(centerIndexPath);
}
public NSIndexPath GetGoToIndex(UICollectionView collectionView, int newPosition)
{
NSIndexPath centerIndexPath = GetIndexPathForCenteredItem(collectionView);
if (centerIndexPath == null)
return NSIndexPath.FromItemSection(0, 0);
var currentCarouselPosition = GetCorrectedIndexFromIndexPath(centerIndexPath);
var itemSourceCount = _itemsSource.ItemCount;
var diffToStart = currentCarouselPosition + (itemSourceCount - newPosition);
var diffToEnd = itemSourceCount - currentCarouselPosition + newPosition;
var increment = currentCarouselPosition - newPosition;
var incrementAbs = Math.Abs(increment);
int goToPosition;
if (diffToStart < incrementAbs)
goToPosition = centerIndexPath.Row - diffToStart;
else if (diffToEnd < incrementAbs)
goToPosition = centerIndexPath.Row + diffToEnd;
else
goToPosition = centerIndexPath.Row - increment;
NSIndexPath goToIndexPath = NSIndexPath.FromItemSection(goToPosition, 0);
return goToIndexPath;
}
public void SetItemsSource(ILoopItemsViewSource itemsSource) => _itemsSource = itemsSource;
void CenterVerticallyIfNeeded(UICollectionView collectionView)
{
var cellHeight = _layout.ItemSize.Height;
var cellPadding = 0;
var currentOffset = collectionView.ContentOffset;
var contentHeight = GetTotalContentHeight();
var boundsHeight = collectionView.Bounds.Size.Height;
if (contentHeight == 0 || cellHeight == 0)
return;
var centerOffsetY = (LoopCount * contentHeight - boundsHeight) / 2;
var distFromCenter = centerOffsetY - currentOffset.Y;
if (Math.Abs(distFromCenter) > (contentHeight / GetMinLoopCount()))
{
var cellcount = distFromCenter / (cellHeight + cellPadding);
var shiftCells = (int)((cellcount > 0) ? Math.Floor(cellcount) : Math.Ceiling(cellcount));
var offsetCorrection = (Math.Abs(cellcount) % 1.0) * (cellHeight + cellPadding);
if (collectionView.ContentOffset.Y < centerOffsetY)
{
collectionView.ContentOffset = new CGPoint(currentOffset.X, centerOffsetY - offsetCorrection);
}
else if (collectionView.ContentOffset.Y > centerOffsetY)
{
collectionView.ContentOffset = new CGPoint(currentOffset.X, centerOffsetY + offsetCorrection);
}
FinishCenterIfNeeded(collectionView, shiftCells);
}
}
void CenterHorizontalIfNeeded(UICollectionView collectionView)
{
var cellWidth = _layout.ItemSize.Width;
var cellPadding = 0;
var currentOffset = collectionView.ContentOffset;
var contentWidth = GetTotalContentWidth();
var boundsWidth = collectionView.Bounds.Size.Width;
if (contentWidth == 0 || cellWidth == 0)
return;
var centerOffsetX = (LoopCount * contentWidth - boundsWidth) / 2;
var distFromCentre = centerOffsetX - currentOffset.X;
if (Math.Abs(distFromCentre) > (contentWidth / GetMinLoopCount()))
{
var cellcount = distFromCentre / (cellWidth + cellPadding);
var shiftCells = (int)((cellcount > 0) ? Math.Floor(cellcount) : Math.Ceiling(cellcount));
var offsetCorrection = (Math.Abs(cellcount % 1.0f)) * (cellWidth + cellPadding);
if (collectionView.ContentOffset.X < centerOffsetX)
{
collectionView.ContentOffset = new CGPoint(centerOffsetX - offsetCorrection, currentOffset.Y);
}
else if (collectionView.ContentOffset.X > centerOffsetX)
{
collectionView.ContentOffset = new CGPoint(centerOffsetX + offsetCorrection, currentOffset.Y);
}
FinishCenterIfNeeded(collectionView, shiftCells);
}
}
void FinishCenterIfNeeded(UICollectionView collectionView, int shiftCells)
{
ShiftContentArray(shiftCells);
collectionView.ReloadData();
}
int GetCorrectedIndex(int indexToCorrect)
{
var itemsCount = GetItemsSourceCount();
if ((indexToCorrect < itemsCount && indexToCorrect >= 0) || itemsCount == 0)
return indexToCorrect;
var countInIndex = (double)(indexToCorrect / itemsCount);
var flooredValue = (int)(Math.Floor(countInIndex));
var offset = itemsCount * flooredValue;
var newIndex = indexToCorrect - offset;
if (newIndex < 0)
return (itemsCount - Math.Abs(newIndex));
return newIndex;
}
NSIndexPath GetIndexPathForCenteredItem(UICollectionView collectionView)
{
var centerPoint = new CGPoint(collectionView.Center.X + collectionView.ContentOffset.X, collectionView.Center.Y + collectionView.ContentOffset.Y);
var centerIndexPath = collectionView.IndexPathForItemAtPoint(centerPoint);
return centerIndexPath;
}
int GetMinLoopCount() => Math.Min(LoopCount, GetItemsSourceCount());
int GetItemsSourceCount() => _itemsSource.ItemCount;
nfloat GetTotalContentWidth() => GetItemsSourceCount() * _layout.ItemSize.Width;
nfloat GetTotalContentHeight() => GetItemsSourceCount() * _layout.ItemSize.Height;
void ShiftContentArray(int shiftCells)
{
var correctedIndex = GetCorrectedIndex(shiftCells);
_indexOffset += correctedIndex;
}
}
}

Просмотреть файл

@ -0,0 +1,59 @@
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public class CarouselViewDelegator : ItemsViewDelegator<CarouselView, CarouselViewController>
{
public CarouselViewDelegator(ItemsViewLayout itemsViewLayout, CarouselViewController itemsViewController)
: base(itemsViewLayout, itemsViewController)
{
}
public override void Scrolled(UIScrollView scrollView)
{
base.Scrolled(scrollView);
ViewController?.UpdateIsScrolling(true);
}
public override void ScrollAnimationEnded(UIScrollView scrollView)
{
ViewController?.UpdateIsScrolling(false);
}
public override void DecelerationEnded(UIScrollView scrollView)
{
ViewController?.UpdateIsScrolling(false);
}
public override void DraggingStarted(UIScrollView scrollView)
{
ViewController?.DraggingStarted(scrollView);
PreviousHorizontalOffset = (float)scrollView.ContentOffset.X;
PreviousVerticalOffset = (float)scrollView.ContentOffset.Y;
}
public override void DraggingEnded(UIScrollView scrollView, bool willDecelerate)
{
PreviousHorizontalOffset = 0;
PreviousVerticalOffset = 0;
ViewController?.DraggingEnded(scrollView, willDecelerate);
}
protected override (bool VisibleItems, int First, int Center, int Last) GetVisibleItemsIndex()
{
var (VisibleItems, First, Center, Last) = GetVisibleItemsIndexPath();
int firstVisibleItemIndex = -1, centerItemIndex = -1, lastVisibleItemIndex = -1;
if (VisibleItems)
{
firstVisibleItemIndex = ViewController.GetIndexFromIndexPath(First);
centerItemIndex = ViewController.GetIndexFromIndexPath(Center);
lastVisibleItemIndex = ViewController.GetIndexFromIndexPath(Last);
}
return (VisibleItems, firstVisibleItemIndex, centerItemIndex, lastVisibleItemIndex);
}
}
}

Просмотреть файл

@ -0,0 +1,91 @@
using System;
using CoreGraphics;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public class CarouselViewLayout : ItemsViewLayout
{
readonly CarouselView _carouselView;
readonly ItemsLayout _itemsLayout;
CGPoint? _pendingOffset;
public CarouselViewLayout(ItemsLayout itemsLayout, CarouselView carouselView) : base(itemsLayout)
{
_carouselView = carouselView;
_itemsLayout = itemsLayout;
}
public override void ConstrainTo(CGSize size)
{
//TODO: Should we scale the items
var width = size.Width - _carouselView.PeekAreaInsets.Left - _carouselView.PeekAreaInsets.Right;
var height = size.Height - _carouselView.PeekAreaInsets.Top - _carouselView.PeekAreaInsets.Bottom;
if (ScrollDirection == UICollectionViewScrollDirection.Horizontal)
{
ItemSize = new CGSize(width, size.Height);
}
else
{
ItemSize = new CGSize(size.Width, height);
}
}
public override nfloat GetMinimumInteritemSpacingForSection(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
{
if (_itemsLayout is LinearItemsLayout linearItemsLayout)
return (nfloat)linearItemsLayout.ItemSpacing;
return base.GetMinimumInteritemSpacingForSection(collectionView, layout, section);
}
public override UIEdgeInsets GetInsetForSection(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
{
var insets = base.GetInsetForSection(collectionView, layout, section);
var left = insets.Left + (float)_carouselView.PeekAreaInsets.Left;
var right = insets.Right + (float)_carouselView.PeekAreaInsets.Right;
var top = insets.Top + (float)_carouselView.PeekAreaInsets.Top;
var bottom = insets.Bottom + (float)_carouselView.PeekAreaInsets.Bottom;
return new UIEdgeInsets(top, left, bottom, right);
}
public override void PrepareForCollectionViewUpdates(UICollectionViewUpdateItem[] updateItems)
{
base.PrepareForCollectionViewUpdates(updateItems);
// Determine whether the change is a removal
if (updateItems.Length == 0 || updateItems[0].UpdateAction != UICollectionUpdateAction.Delete)
{
return;
}
// Determine whether the removed item is before the current position
if (updateItems[0].IndexPathBeforeUpdate.Item >= _carouselView.Position)
{
return;
}
// If an earlier item is being removed, we'll need to adjust the content offset to account for
// the now mising item. Calculate what the new offset will be and store that.
var currentOffset = CollectionView.ContentOffset;
if (ScrollDirection == UICollectionViewScrollDirection.Horizontal)
_pendingOffset = new CGPoint(currentOffset.X - ItemSize.Width, currentOffset.Y);
else
_pendingOffset = new CGPoint(currentOffset.X, currentOffset.Y - ItemSize.Height);
}
public override void FinalizeCollectionViewUpdates()
{
base.FinalizeCollectionViewUpdates();
// Adjust the offset if necessary (e.g., if we've removed items from earlier in the carousel)
if (_pendingOffset.HasValue)
{
CollectionView.SetContentOffset(_pendingOffset.Value, false);
_pendingOffset = null;
}
}
}
}

Просмотреть файл

@ -0,0 +1,36 @@
using System;
using CoreGraphics;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public abstract class DefaultCell : ItemsViewCell
{
public UILabel Label { get; }
protected NSLayoutConstraint Constraint { get; set; }
[Export("initWithFrame:")]
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
protected DefaultCell(CGRect frame) : base(frame)
{
Label = new UILabel(frame)
{
TextColor = ColorExtensions.LabelColor,
Lines = 1,
Font = UIFont.PreferredBody,
TranslatesAutoresizingMaskIntoConstraints = false
};
ContentView.BackgroundColor = UIColor.Clear;
InitializeContentConstraints(Label);
}
public override void ConstrainTo(nfloat constant)
{
Constraint.Constant = constant;
}
}
}

Просмотреть файл

@ -0,0 +1,34 @@
using System;
using Foundation;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal class EmptySource : ILoopItemsViewSource
{
public int GroupCount => 0;
public int ItemCount => 0;
public bool Loop { get; set; }
public int LoopCount => 0;
public object this[NSIndexPath indexPath] => throw new IndexOutOfRangeException("IItemsViewSource is empty");
public int ItemCountInGroup(nint group) => 0;
public object Group(NSIndexPath indexPath)
{
throw new IndexOutOfRangeException("IItemsViewSource is empty");
}
public NSIndexPath GetIndexForItem(object item)
{
throw new IndexOutOfRangeException("IItemsViewSource is empty");
}
public void Dispose()
{
}
}
}

Просмотреть файл

@ -0,0 +1,309 @@
using System;
using System.ComponentModel;
using CoreGraphics;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public class GridViewLayout : ItemsViewLayout
{
readonly GridItemsLayout _itemsLayout;
public GridViewLayout(GridItemsLayout itemsLayout, ItemSizingStrategy itemSizingStrategy) : base(itemsLayout, itemSizingStrategy)
{
_itemsLayout = itemsLayout;
}
protected override void HandlePropertyChanged(PropertyChangedEventArgs propertyChanged)
{
if (propertyChanged.IsOneOf(GridItemsLayout.SpanProperty, GridItemsLayout.HorizontalItemSpacingProperty,
GridItemsLayout.VerticalItemSpacingProperty))
{
// Update the constraints; ConstrainTo will pick up the new span
ConstrainTo(CollectionView.Frame.Size);
// And force the UICollectionView to reload everything with the new span
CollectionView.ReloadData();
}
base.HandlePropertyChanged(propertyChanged);
}
public override void ConstrainTo(CGSize size)
{
var availableSpace = ScrollDirection == UICollectionViewScrollDirection.Vertical
? size.Width : size.Height;
var spacing = (nfloat)(ScrollDirection == UICollectionViewScrollDirection.Vertical
? _itemsLayout.HorizontalItemSpacing
: _itemsLayout.VerticalItemSpacing);
spacing = ReduceSpacingToFitIfNeeded(availableSpace, spacing, _itemsLayout.Span);
spacing *= (_itemsLayout.Span - 1);
ConstrainedDimension = (availableSpace - spacing) / _itemsLayout.Span;
// We need to truncate the decimal part of ConstrainedDimension
// or we occasionally run into situations where the rows/columns don't fit
// But this can run into situations where we have an extra gap because we're cutting off too much
// and we have a small gap; need to determine where the cut-off is that leads to layout dropping a whole row/column
// and see if we can adjust for that
// E.G.: We have a CollectionView that's 532 units tall, and we have a span of 3
// So we end up with ConstrainedDimension of 177.3333333333333...
// When UICollectionView lays that out, it can't fit all the rows in so it just gives us two rows.
// Truncating to 177 means the rows fit, but there's a very slight gap
// There may not be anything we can do about this.
// Possibly the solution is to round to the tenths or hundredths place, we should look into that.
// But for the moment, we need a special case for dimensions < 1, because upon transition from invisible to visible,
// Forms will briefly layout the CollectionView at a size of 1,1. For a spanned collectionview, that means we
// need to accept a constrained dimension of 1/span. If we don't, autolayout will start throwing a flurry of
// exceptions (which we can't catch) and either crash the app or spin until we kill the app.
if (ConstrainedDimension > 1)
{
ConstrainedDimension = (int)ConstrainedDimension;
}
DetermineCellSize();
}
/* `CollectionViewContentSize` and `LayoutAttributesForElementsInRect` are overridden here to work around what
* appears to be a bug in the UICollectionViewFlowLayout implementation: for horizontally scrolling grid
* layouts with auto-sized cells, trailing items which don't fill out a column are never displayed.
* For example, with a span of 3 and either 4 or 5 items, the resulting layout looks like
*
* Item1
* Item2
* Item3
*
* But with 6 items, it looks like
*
* Item1 Item4
* Item2 Item5
* Item3 Item6
*
* IOW, if there are not enough items to fill out the last column, the last column is ignored.
*
* These overrides detect and correct that situation.
*/
public override CGSize CollectionViewContentSize
{
get
{
if (!NeedsPartialColumnAdjustment())
{
return base.CollectionViewContentSize;
}
var contentSize = base.CollectionViewContentSize;
// Add space for the missing column at the end
var correctedSize = new CGSize(contentSize.Width + EstimatedItemSize.Width, contentSize.Height);
return correctedSize;
}
}
public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect(CGRect rect)
{
var layoutAttributesForRectElements = base.LayoutAttributesForElementsInRect(rect);
if (NeedsSingleItemHorizontalAlignmentAdjustment(layoutAttributesForRectElements))
{
// If there's exactly one item in a vertically scrolling grid, for some reason UICollectionViewFlowLayout
// tries to center it. This corrects that issue.
var currentFrame = layoutAttributesForRectElements[0].Frame;
var newFrame = new CGRect(CollectionView.Frame.Left + CollectionView.ContentInset.Right,
currentFrame.Top, currentFrame.Width, currentFrame.Height);
layoutAttributesForRectElements[0].Frame = newFrame;
}
if (!NeedsPartialColumnAdjustment())
{
return layoutAttributesForRectElements;
}
// When we implement Groups, we'll have to iterate over all of them to adjust and this will
// be a lot more complicated. But until then, we only have to worry about section 0
var section = 0;
var itemCount = CollectionView.NumberOfItemsInSection(section);
if (layoutAttributesForRectElements.Length == itemCount)
{
return layoutAttributesForRectElements;
}
var layoutAttributesForAllCells = new UICollectionViewLayoutAttributes[itemCount];
layoutAttributesForRectElements.CopyTo(layoutAttributesForAllCells, 0);
for (int i = layoutAttributesForRectElements.Length; i < layoutAttributesForAllCells.Length; i++)
{
layoutAttributesForAllCells[i] = LayoutAttributesForItem(NSIndexPath.FromItemSection(i, section));
}
return layoutAttributesForAllCells;
}
public override UICollectionViewLayoutInvalidationContext GetInvalidationContext(UICollectionViewLayoutAttributes preferredAttributes, UICollectionViewLayoutAttributes originalAttributes)
{
var invalidationContext = base.GetInvalidationContext(preferredAttributes, originalAttributes);
if (invalidationContext.InvalidatedItemIndexPaths == null)
{
return invalidationContext;
}
if (invalidationContext.InvalidatedItemIndexPaths.Length == 0)
{
return invalidationContext;
}
if (ScrollDirection == UICollectionViewScrollDirection.Horizontal
&& preferredAttributes.Frame.Width - originalAttributes.Frame.Width > 1)
{
// If this is a horizontal grid and we're laying out or adjusting a cell
// and we've decided it needs to be wider, then this might throw off the alignment of
// any cells above it in the layout. We'll need to recenter those cells
CenterAlignCellsInColumn(preferredAttributes);
// (Technically speaking, we _could_ simply add the cells above the current cell to the invalidationContext;
// after invalidation, they would be realigned correctly. But doing that causes subsequent calls to
// GetInvalidationContext to happen every time a new column needs layout, and those calls will include
// _every single subsequent cell in the collection_ in the invalidation list. For very large collections,
// this gets really slow and the scrolling becomes jerky. This one-time realignment is much faster.
}
return invalidationContext;
}
public override nfloat GetMinimumInteritemSpacingForSection(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
{
var requestedSpacing = ScrollDirection == UICollectionViewScrollDirection.Horizontal
? (nfloat)_itemsLayout.VerticalItemSpacing
: (nfloat)_itemsLayout.HorizontalItemSpacing;
var availableSpace = ScrollDirection == UICollectionViewScrollDirection.Horizontal
? collectionView.Frame.Height
: collectionView.Frame.Width;
return ReduceSpacingToFitIfNeeded(availableSpace, requestedSpacing, _itemsLayout.Span);
}
void CenterAlignCellsInColumn(UICollectionViewLayoutAttributes preferredAttributes)
{
// Determine the set of cells above this one
var index = preferredAttributes.IndexPath;
var span = _itemsLayout.Span;
var column = index.Item / span;
var start = (int)column * span;
// If this is the first cell in the column, we don't need to adjust
if (index.Item > start)
{
var currentCenter = preferredAttributes.Frame.GetMidX();
// Work our way through the column
for (int n = start; n < index.Item; n++)
{
// Get the layout attributes for each cell
var path = NSIndexPath.FromItemSection(n, index.Section);
var attr = LayoutAttributesForItem(path);
// And see if the midpoints line up with the new layout attributes for the current cell
var center = attr.Frame.GetMidX();
if (currentCenter - center > 1)
{
// If the midpoints don't line up (withing a tolerance), adjust the cell's frame
var cell = CollectionView.CellForItem(path);
cell.Frame = new CGRect(currentCenter - cell.Frame.Width / 2, cell.Frame.Top, cell.Frame.Width, cell.Frame.Height);
}
}
}
}
bool NeedsSingleItemHorizontalAlignmentAdjustment(UICollectionViewLayoutAttributes[] layoutAttributesForRectElements)
{
if (ScrollDirection == UICollectionViewScrollDirection.Horizontal)
{
return false;
}
if (layoutAttributesForRectElements.Length != 1)
{
return false;
}
if (layoutAttributesForRectElements[0].Frame.Top != CollectionView.Frame.Top + CollectionView.ContentInset.Bottom)
{
return false;
}
return true;
}
bool NeedsPartialColumnAdjustment(int section = 0)
{
if (ScrollDirection == UICollectionViewScrollDirection.Vertical)
{
// The bug only occurs with Horizontal scrolling
return false;
}
if (CollectionView.NumberOfSections() == 0)
{
// And it only happens if there are items
return false;
}
if (EstimatedItemSize.IsEmpty)
{
// The bug only occurs when using Autolayout; with a set ItemSize, we don't have to worry about it
return false;
}
if (CollectionView.NumberOfSections() == 0)
return false;
var itemCount = CollectionView.NumberOfItemsInSection(section);
if (itemCount < _itemsLayout.Span)
{
// If there is just one partial column, no problem; UICollectionViewFlowLayout gets it right
return false;
}
if (itemCount % _itemsLayout.Span == 0)
{
// All of the columns are full; the bug only occurs when we have a partial column
return false;
}
return true;
}
static nfloat ReduceSpacingToFitIfNeeded(nfloat available, nfloat requestedSpacing, int span)
{
if (span == 1)
{
return requestedSpacing;
}
var maxSpacing = (available - span) / (span - 1);
if (maxSpacing < 0)
{
return 0;
}
return (nfloat)Math.Min(requestedSpacing, maxSpacing);
}
}
}

Просмотреть файл

@ -0,0 +1,303 @@
using System;
using CoreGraphics;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public class GroupableItemsViewController<TItemsView> : SelectableItemsViewController<TItemsView>
where TItemsView : GroupableItemsView
{
// Keep a cached value for the current state of grouping around so we can avoid hitting the
// BindableProperty all the time
bool _isGrouped;
// Keep out header measurement cells for iOS handy so we don't have to
// create new ones all the time. For other versions, the reusable cells
// queueing mechanism does this for us.
TemplatedCell _measurementCellTemplated;
DefaultCell _measurementCellDefault;
Action _scrollAnimationEndedCallback;
public GroupableItemsViewController(TItemsView groupableItemsView, ItemsViewLayout layout)
: base(groupableItemsView, layout)
{
_isGrouped = ItemsView.IsGrouped;
}
protected override UICollectionViewDelegateFlowLayout CreateDelegator()
{
return new GroupableItemsViewDelegator<TItemsView, GroupableItemsViewController<TItemsView>>(ItemsViewLayout, this);
}
protected override IItemsViewSource CreateItemsViewSource()
{
// Use the BindableProperty here (instead of _isGroupingEnabled) because the cached value might not be set yet
if (ItemsView.IsGrouped)
{
return ItemsSourceFactory.CreateGrouped(ItemsView.ItemsSource, this);
}
return base.CreateItemsViewSource();
}
public override void UpdateItemsSource()
{
_isGrouped = ItemsView.IsGrouped;
base.UpdateItemsSource();
}
protected override void RegisterViewTypes()
{
base.RegisterViewTypes();
RegisterSupplementaryViews(UICollectionElementKindSection.Header);
RegisterSupplementaryViews(UICollectionElementKindSection.Footer);
}
void RegisterSupplementaryViews(UICollectionElementKindSection kind)
{
CollectionView.RegisterClassForSupplementaryView(typeof(HorizontalSupplementaryView),
kind, HorizontalSupplementaryView.ReuseId);
CollectionView.RegisterClassForSupplementaryView(typeof(VerticalSupplementaryView),
kind, VerticalSupplementaryView.ReuseId);
CollectionView.RegisterClassForSupplementaryView(typeof(HorizontalDefaultSupplementalView),
kind, HorizontalDefaultSupplementalView.ReuseId);
CollectionView.RegisterClassForSupplementaryView(typeof(VerticalDefaultSupplementalView),
kind, VerticalDefaultSupplementalView.ReuseId);
}
public override UICollectionReusableView GetViewForSupplementaryElement(UICollectionView collectionView,
NSString elementKind, NSIndexPath indexPath)
{
var reuseId = DetermineViewReuseId(elementKind);
var view = collectionView.DequeueReusableSupplementaryView(elementKind, reuseId, indexPath) as UICollectionReusableView;
switch (view)
{
case DefaultCell defaultCell:
UpdateDefaultSupplementaryView(defaultCell, elementKind, indexPath);
break;
case TemplatedCell templatedCell:
UpdateTemplatedSupplementaryView(templatedCell, elementKind, indexPath);
break;
}
return view;
}
void UpdateDefaultSupplementaryView(DefaultCell cell, NSString elementKind, NSIndexPath indexPath)
{
cell.Label.Text = ItemsSource.Group(indexPath).ToString();
if (cell is ItemsViewCell)
{
cell.ConstrainTo(GetLayoutSpanCount() * ItemsViewLayout.ConstrainedDimension);
}
}
void UpdateTemplatedSupplementaryView(TemplatedCell cell, NSString elementKind, NSIndexPath indexPath)
{
DataTemplate template = elementKind == UICollectionElementKindSectionKey.Header
? ItemsView.GroupHeaderTemplate
: ItemsView.GroupFooterTemplate;
var bindingContext = ItemsSource.Group(indexPath);
cell.Bind(template, bindingContext, ItemsView);
if (cell is ItemsViewCell)
{
cell.ConstrainTo(GetLayoutSpanCount() * ItemsViewLayout.ConstrainedDimension);
}
}
string DetermineViewReuseId(NSString elementKind)
{
return DetermineViewReuseId(elementKind == UICollectionElementKindSectionKey.Header
? ItemsView.GroupHeaderTemplate
: ItemsView.GroupFooterTemplate);
}
string DetermineViewReuseId(DataTemplate template)
{
if (template == null)
{
// No template, fall back the the default supplemental views
return ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
? HorizontalDefaultSupplementalView.ReuseId
: VerticalDefaultSupplementalView.ReuseId;
}
return ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
? HorizontalSupplementaryView.ReuseId
: VerticalSupplementaryView.ReuseId;
}
internal CGSize GetReferenceSizeForHeader(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
{
if (!_isGrouped)
{
return CGSize.Empty;
}
// Currently we explicitly measure all of the headers/footers
// Long-term, we might want to look at performance hints (similar to ItemSizingStrategy) for
// headers/footers (if the dev knows for sure they'll all the be the same size)
return GetReferenceSizeForheaderOrFooter(collectionView, ItemsView.GroupHeaderTemplate, UICollectionElementKindSectionKey.Header, section);
}
internal CGSize GetReferenceSizeForFooter(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
{
if (!_isGrouped)
{
return CGSize.Empty;
}
return GetReferenceSizeForheaderOrFooter(collectionView, ItemsView.GroupFooterTemplate, UICollectionElementKindSectionKey.Footer, section);
}
internal CGSize GetReferenceSizeForheaderOrFooter(UICollectionView collectionView, DataTemplate template, NSString elementKind, nint section)
{
if (!_isGrouped || template == null)
{
return CGSize.Empty;
}
if (ItemsSource.GroupCount < 1 || section > ItemsSource.GroupCount - 1)
{
return CGSize.Empty;
}
if (!NativeVersion.IsAtLeast(11))
{
// iOS 10 crashes if we try to dequeue a cell for measurement
// so we'll use an alternate method
return MeasureSupplementaryView(elementKind, section);
}
var cell = GetViewForSupplementaryElement(collectionView, elementKind,
NSIndexPath.FromItemSection(0, section)) as ItemsViewCell;
return cell.Measure();
}
internal void SetScrollAnimationEndedCallback(Action callback)
{
_scrollAnimationEndedCallback = callback;
}
internal void HandleScrollAnimationEnded()
{
_scrollAnimationEndedCallback?.Invoke();
_scrollAnimationEndedCallback = null;
}
int GetLayoutSpanCount()
{
var span = 1;
if (ItemsView?.ItemsLayout is GridItemsLayout gridItemsLayout)
{
span = gridItemsLayout.Span;
}
return span;
}
internal UIEdgeInsets GetInsetForSection(ItemsViewLayout itemsViewLayout,
UICollectionView collectionView, nint section)
{
var uIEdgeInsets = ItemsViewLayout.GetInsetForSection(collectionView, itemsViewLayout, section);
if (!ItemsView.IsGrouped)
{
return uIEdgeInsets;
}
// If we're grouping, we'll need to inset the sections to maintain the spacing between the
// groups and their group headers/footers
nfloat spacing = itemsViewLayout.GetMinimumLineSpacingForSection(collectionView, itemsViewLayout, section);
var top = uIEdgeInsets.Top;
var left = uIEdgeInsets.Left;
if (itemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal)
{
left += spacing;
}
else
{
top += spacing;
}
return new UIEdgeInsets(top, left,
uIEdgeInsets.Bottom, uIEdgeInsets.Right);
}
// These measurement methods are only necessary for iOS 10 and lower
CGSize MeasureTemplatedSupplementaryCell(NSString elementKind, nint section, NSString reuseId)
{
if (_measurementCellTemplated == null)
{
if (reuseId == HorizontalSupplementaryView.ReuseId)
{
_measurementCellTemplated = new HorizontalSupplementaryView(CGRect.Empty);
}
else if (reuseId == VerticalSupplementaryView.ReuseId)
{
_measurementCellTemplated = new VerticalSupplementaryView(CGRect.Empty);
}
}
if (_measurementCellTemplated == null)
{
return CGSize.Empty;
}
UpdateTemplatedSupplementaryView(_measurementCellTemplated, elementKind, NSIndexPath.FromItemSection(0, section));
return _measurementCellTemplated.Measure();
}
CGSize MeasureDefaultSupplementaryCell(NSString elementKind, nint section, NSString reuseId)
{
if (_measurementCellDefault == null)
{
if (reuseId == HorizontalDefaultSupplementalView.ReuseId)
{
_measurementCellDefault = new HorizontalDefaultSupplementalView(CGRect.Empty);
}
else if (reuseId == VerticalDefaultSupplementalView.ReuseId)
{
_measurementCellDefault = new VerticalDefaultSupplementalView(CGRect.Empty);
}
}
if (_measurementCellDefault == null)
{
return CGSize.Empty;
}
UpdateDefaultSupplementaryView(_measurementCellDefault, elementKind, NSIndexPath.FromItemSection(0, section));
return _measurementCellDefault.Measure();
}
CGSize MeasureSupplementaryView(NSString elementKind, nint section)
{
var reuseId = (NSString)DetermineViewReuseId(elementKind);
if (reuseId == HorizontalDefaultSupplementalView.ReuseId
|| reuseId == VerticalDefaultSupplementalView.ReuseId)
{
return MeasureDefaultSupplementaryCell(elementKind, section, reuseId);
}
return MeasureTemplatedSupplementaryCell(elementKind, section, reuseId);
}
// end of iOS 10 workaround stuff
}
}

Просмотреть файл

@ -0,0 +1,41 @@
using System;
using CoreGraphics;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public class GroupableItemsViewDelegator<TItemsView, TViewController> : SelectableItemsViewDelegator<TItemsView, TViewController>
where TItemsView : GroupableItemsView
where TViewController : GroupableItemsViewController<TItemsView>
{
public GroupableItemsViewDelegator(ItemsViewLayout itemsViewLayout, TViewController itemsViewController)
: base(itemsViewLayout, itemsViewController)
{
}
public override CGSize GetReferenceSizeForHeader(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
{
return ViewController.GetReferenceSizeForHeader(collectionView, layout, section);
}
public override CGSize GetReferenceSizeForFooter(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
{
return ViewController.GetReferenceSizeForFooter(collectionView, layout, section);
}
public override void ScrollAnimationEnded(UIScrollView scrollView)
{
ViewController?.HandleScrollAnimationEnded();
}
public override UIEdgeInsets GetInsetForSection(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
{
if (ItemsViewLayout == null)
{
return default;
}
return ViewController.GetInsetForSection(ItemsViewLayout, collectionView, section);
}
}
}

Просмотреть файл

@ -0,0 +1,48 @@
using CoreGraphics;
using Foundation;
using Microsoft.Maui.Graphics;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal abstract partial class HeightConstrainedTemplatedCell : TemplatedCell
{
[Export("initWithFrame:")]
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
public HeightConstrainedTemplatedCell(CGRect frame) : base(frame)
{
}
public override void ConstrainTo(CGSize constraint)
{
ClearConstraints();
ConstrainedDimension = constraint.Height;
}
protected override (bool, Size) NeedsContentSizeUpdate(Size currentSize)
{
var size = Size.Zero;
if (NativeHandler?.VirtualView == null)
{
return (false, size);
}
var bounds = NativeHandler.VirtualView.Frame;
if (bounds.Width <= 0 || bounds.Height <= 0)
{
return (false, size);
}
var desiredBounds = NativeHandler.VirtualView.Measure(double.PositiveInfinity, bounds.Height);
if (desiredBounds.Width == currentSize.Width)
{
// Nothing in the cell needs more room, so leave it as it is
return (false, size);
}
return (true, desiredBounds);
}
}
}

Просмотреть файл

@ -0,0 +1,29 @@
using CoreGraphics;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal sealed class HorizontalCell : HeightConstrainedTemplatedCell
{
public static NSString ReuseId = new NSString("Microsoft.Maui.Controls.Compatibility.Platform.iOS.HorizontalCell");
[Export("initWithFrame:")]
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
public HorizontalCell(CGRect frame) : base(frame)
{
}
public override CGSize Measure()
{
var measure = NativeHandler.VirtualView.Measure(double.PositiveInfinity, ConstrainedDimension);
return new CGSize(measure.Width, ConstrainedDimension);
}
protected override bool AttributesConsistentWithConstrainedDimension(UICollectionViewLayoutAttributes attributes)
{
return attributes.Frame.Width == ConstrainedDimension;
}
}
}

Просмотреть файл

@ -0,0 +1,30 @@
using CoreGraphics;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal sealed class HorizontalDefaultCell : DefaultCell
{
public static NSString ReuseId = new NSString("Microsoft.Maui.Controls.Compatibility.Platform.iOS.HorizontalDefaultCell");
[Export("initWithFrame:")]
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
public HorizontalDefaultCell(CGRect frame) : base(frame)
{
Constraint = Label.HeightAnchor.ConstraintEqualTo(Frame.Height);
Constraint.Priority = (float)UILayoutPriority.DefaultHigh;
Constraint.Active = true;
}
public override void ConstrainTo(CGSize constraint)
{
Constraint.Constant = constraint.Height;
}
public override CGSize Measure()
{
return new CGSize(Label.IntrinsicContentSize.Width, Constraint.Constant);
}
}
}

Просмотреть файл

@ -0,0 +1,32 @@
using CoreGraphics;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal sealed class HorizontalDefaultSupplementalView : DefaultCell
{
public static NSString ReuseId = new NSString("Microsoft.Maui.Controls.Compatibility.Platform.iOS.HorizontalDefaultSupplementalView");
[Export("initWithFrame:")]
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
public HorizontalDefaultSupplementalView(CGRect frame) : base(frame)
{
Label.Font = UIFont.PreferredHeadline;
Constraint = Label.HeightAnchor.ConstraintEqualTo(Frame.Height);
Constraint.Priority = (float)UILayoutPriority.DefaultHigh;
Constraint.Active = true;
}
public override void ConstrainTo(CGSize constraint)
{
Constraint.Constant = constraint.Height;
}
public override CGSize Measure()
{
return new CGSize(Label.IntrinsicContentSize.Width, Constraint.Constant);
}
}
}

Просмотреть файл

@ -0,0 +1,38 @@
using CoreGraphics;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal sealed class HorizontalSupplementaryView : HeightConstrainedTemplatedCell
{
public static NSString ReuseId = new NSString("Microsoft.Maui.Controls.Compatibility.Platform.iOS.HorizontalSupplementaryView");
[Export("initWithFrame:")]
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
public HorizontalSupplementaryView(CGRect frame) : base(frame)
{
}
public override CGSize Measure()
{
if (NativeHandler?.VirtualView == null)
{
return CGSize.Empty;
}
var measure = NativeHandler.VirtualView.Measure(double.PositiveInfinity, ConstrainedDimension);
var width = NativeHandler.VirtualView.Width > 0
? NativeHandler.VirtualView.Width : measure.Width;
return new CGSize(width, ConstrainedDimension);
}
protected override bool AttributesConsistentWithConstrainedDimension(UICollectionViewLayoutAttributes attributes)
{
return attributes.Frame.Height == ConstrainedDimension;
}
}
}

Просмотреть файл

@ -0,0 +1,14 @@
using System;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public interface IItemsViewSource : IDisposable
{
int ItemCount { get; }
int ItemCountInGroup(nint group);
int GroupCount { get; }
object this[Foundation.NSIndexPath indexPath] { get; }
object Group(Foundation.NSIndexPath indexPath);
Foundation.NSIndexPath GetIndexForItem(object item);
}
}

Просмотреть файл

@ -0,0 +1,9 @@
namespace Microsoft.Maui.Controls.Handlers.Items
{
public interface ILoopItemsViewSource : IItemsViewSource
{
bool Loop { get; set; }
int LoopCount { get; }
}
}

Просмотреть файл

@ -0,0 +1,42 @@
using Foundation;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal static class IndexPathExtensions
{
public static bool IsLessThanOrEqualToPath(this NSIndexPath path, NSIndexPath otherPath)
{
if (path.Section < otherPath.Section)
{
return true;
}
if (path.Section == otherPath.Section)
{
return path.Item <= otherPath.Item;
}
return false;
}
public static NSIndexPath FindFirst(this NSIndexPath[] paths)
{
NSIndexPath firstPath = null;
foreach (var path in paths)
{
if (firstPath == null)
{
firstPath = path;
continue;
}
if (path.IsLessThanOrEqualToPath(firstPath))
{
firstPath = path;
}
}
return firstPath;
}
}
}

Просмотреть файл

@ -0,0 +1,51 @@
using Foundation;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public static class IndexPathHelpers
{
public static NSIndexPath[] GenerateIndexPathRange(int section, int startIndex, int count)
{
var result = new NSIndexPath[count];
for (int n = 0; n < count; n++)
{
result[n] = NSIndexPath.Create(section, startIndex + n);
}
return result;
}
public static NSIndexPath[] GenerateLoopedIndexPathRange(int section, int sectionCount, int iterations, int startIndex, int count)
{
var result = new NSIndexPath[iterations * count];
var step = sectionCount / iterations;
for (int r = 0; r < iterations; r++)
{
for (int n = 0; n < count; n++)
{
var index = startIndex + (r * step) + n;
result[(r * count) + n] = NSIndexPath.Create(section, index);
}
}
return result;
}
public static bool IsIndexPathValid(this IItemsViewSource source, NSIndexPath indexPath)
{
if (indexPath.Section >= source.GroupCount)
{
return false;
}
if (indexPath.Item >= source.ItemCountInGroup(indexPath.Section))
{
return false;
}
return true;
}
}
}

Просмотреть файл

@ -0,0 +1,61 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal static class ItemsSourceFactory
{
public static IItemsViewSource Create(IEnumerable itemsSource, UICollectionViewController collectionViewController)
{
if (itemsSource == null)
{
return new EmptySource();
}
switch (itemsSource)
{
case IList _ when itemsSource is INotifyCollectionChanged:
return new ObservableItemsSource(itemsSource as IList, collectionViewController);
case IEnumerable _ when itemsSource is INotifyCollectionChanged:
return new ObservableItemsSource(itemsSource as IEnumerable, collectionViewController);
case IEnumerable<object> generic:
return new ListSource(generic);
}
return new ListSource(itemsSource);
}
public static IItemsViewSource CreateGrouped(IEnumerable itemsSource, UICollectionViewController collectionViewController)
{
if (itemsSource == null)
{
return new EmptySource();
}
return new ObservableGroupedSource(itemsSource, collectionViewController);
}
public static ILoopItemsViewSource CreateForCarouselView(IEnumerable itemsSource, UICollectionViewController collectionViewController, bool loop)
{
if (itemsSource == null)
{
return new EmptySource();
}
switch (itemsSource)
{
case IList _ when itemsSource is INotifyCollectionChanged:
return new LoopObservableItemsSource(itemsSource as IList, collectionViewController, loop);
case IEnumerable _ when itemsSource is INotifyCollectionChanged:
return new LoopObservableItemsSource(itemsSource as IEnumerable, collectionViewController, loop);
case IEnumerable<object> generic:
return new LoopListSource(generic, loop);
}
return new LoopListSource(itemsSource, loop);
}
}
}

Просмотреть файл

@ -0,0 +1,48 @@
using System;
using CoreGraphics;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public abstract class ItemsViewCell : UICollectionViewCell
{
[Export("initWithFrame:")]
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
protected ItemsViewCell(CGRect frame) : base(frame)
{
ContentView.BackgroundColor = UIColor.Clear;
var selectedBackgroundView = new UIView
{
BackgroundColor = ColorExtensions.Gray
};
SelectedBackgroundView = selectedBackgroundView;
}
protected void InitializeContentConstraints(UIView nativeView)
{
ContentView.TranslatesAutoresizingMaskIntoConstraints = false;
nativeView.TranslatesAutoresizingMaskIntoConstraints = false;
ContentView.AddSubview(nativeView);
// We want the cell to be the same size as the ContentView
ContentView.TopAnchor.ConstraintEqualTo(TopAnchor).Active = true;
ContentView.BottomAnchor.ConstraintEqualTo(BottomAnchor).Active = true;
ContentView.LeadingAnchor.ConstraintEqualTo(LeadingAnchor).Active = true;
ContentView.TrailingAnchor.ConstraintEqualTo(TrailingAnchor).Active = true;
// And we want the ContentView to be the same size as the root renderer for the Forms element
ContentView.TopAnchor.ConstraintEqualTo(nativeView.TopAnchor).Active = true;
ContentView.BottomAnchor.ConstraintEqualTo(nativeView.BottomAnchor).Active = true;
ContentView.LeadingAnchor.ConstraintEqualTo(nativeView.LeadingAnchor).Active = true;
ContentView.TrailingAnchor.ConstraintEqualTo(nativeView.TrailingAnchor).Active = true;
}
public abstract void ConstrainTo(nfloat constant);
public abstract void ConstrainTo(CGSize constraint);
public abstract CGSize Measure();
}
}

Просмотреть файл

@ -0,0 +1,636 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using CoreGraphics;
using Foundation;
using Microsoft.Maui.Graphics;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public abstract class ItemsViewController<TItemsView> : UICollectionViewController
where TItemsView : ItemsView
{
public const int EmptyTag = 333;
public IItemsViewSource ItemsSource { get; protected set; }
public TItemsView ItemsView { get; }
protected ItemsViewLayout ItemsViewLayout { get; set; }
bool _initialized;
bool _isEmpty;
bool _emptyViewDisplayed;
bool _disposed;
UIView _emptyUIView;
VisualElement _emptyViewFormsElement;
Dictionary<object, TemplatedCell> _measurementCells = new Dictionary<object, TemplatedCell>();
protected UICollectionViewDelegateFlowLayout Delegator { get; set; }
protected ItemsViewController(TItemsView itemsView, ItemsViewLayout layout) : base(layout)
{
ItemsView = itemsView;
ItemsViewLayout = layout;
}
public void UpdateLayout(ItemsViewLayout newLayout)
{
// Ignore calls to this method if the new layout is the same as the old one
if (CollectionView.CollectionViewLayout == newLayout)
return;
ItemsViewLayout = newLayout;
_initialized = false;
EnsureLayoutInitialized();
if (_initialized)
{
// Reload the data so the currently visible cells get laid out according to the new layout
CollectionView.ReloadData();
}
}
protected override void Dispose(bool disposing)
{
if (_disposed)
return;
_disposed = true;
if (disposing)
{
ItemsSource?.Dispose();
CollectionView.Delegate = null;
Delegator?.Dispose();
_emptyUIView?.Dispose();
_emptyUIView = null;
_emptyViewFormsElement = null;
ItemsViewLayout?.Dispose();
CollectionView?.Dispose();
}
base.Dispose(disposing);
}
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
var cell = collectionView.DequeueReusableCell(DetermineCellReuseId(), indexPath) as UICollectionViewCell;
switch (cell)
{
case DefaultCell defaultCell:
UpdateDefaultCell(defaultCell, indexPath);
break;
case TemplatedCell templatedCell:
UpdateTemplatedCell(templatedCell, indexPath);
break;
}
return cell;
}
public override nint GetItemsCount(UICollectionView collectionView, nint section)
{
CheckForEmptySource();
return ItemsSource.ItemCountInGroup(section);
}
void CheckForEmptySource()
{
var wasEmpty = _isEmpty;
_isEmpty = ItemsSource.ItemCount == 0;
if (_isEmpty)
{
_measurementCells.Clear();
ItemsViewLayout?.ClearCellSizeCache();
}
if (wasEmpty != _isEmpty)
{
UpdateEmptyViewVisibility(_isEmpty);
}
if (wasEmpty && !_isEmpty)
{
// If we're going from empty to having stuff, it's possible that we've never actually measured
// a prototype cell and our itemSize or estimatedItemSize are wrong/unset
// So trigger a constraint update; if we need a measurement, that will make it happen
ItemsViewLayout.ConstrainTo(CollectionView.Bounds.Size);
}
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
ItemsSource = CreateItemsViewSource();
if (!NativeVersion.IsAtLeast(11))
AutomaticallyAdjustsScrollViewInsets = false;
else
{
// We set this property to keep iOS from trying to be helpful about insetting all the
// CollectionView content when we're in landscape mode (to avoid the notch)
// The SetUseSafeArea Platform Specific is already taking care of this for us
// That said, at some point it's possible folks will want a PS for controlling this behavior
CollectionView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Never;
}
RegisterViewTypes();
EnsureLayoutInitialized();
}
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
ConstrainToItemsView();
}
public override void ViewWillLayoutSubviews()
{
ConstrainToItemsView();
base.ViewWillLayoutSubviews();
LayoutEmptyView();
}
void ConstrainToItemsView()
{
var itemsViewWidth = ItemsView.Width;
var itemsViewHeight = ItemsView.Height;
if (itemsViewHeight < 0 || itemsViewWidth < 0)
{
ItemsViewLayout.UpdateConstraints(CollectionView.Bounds.Size);
return;
}
ItemsViewLayout.UpdateConstraints(new CGSize(itemsViewWidth, itemsViewHeight));
}
void EnsureLayoutInitialized()
{
if (_initialized)
{
return;
}
_initialized = true;
ItemsViewLayout.GetPrototype = GetPrototype;
Delegator = CreateDelegator();
CollectionView.Delegate = Delegator;
ItemsViewLayout.SetInitialConstraints(CollectionView.Bounds.Size);
CollectionView.SetCollectionViewLayout(ItemsViewLayout, false);
UpdateEmptyView();
}
protected virtual UICollectionViewDelegateFlowLayout CreateDelegator()
{
return new ItemsViewDelegator<TItemsView, ItemsViewController<TItemsView>>(ItemsViewLayout, this);
}
protected virtual IItemsViewSource CreateItemsViewSource()
{
return ItemsSourceFactory.Create(ItemsView.ItemsSource, this);
}
public virtual void UpdateItemsSource()
{
_measurementCells.Clear();
ItemsViewLayout?.ClearCellSizeCache();
ItemsSource = CreateItemsViewSource();
CollectionView.ReloadData();
CollectionView.CollectionViewLayout.InvalidateLayout();
}
public virtual void UpdateFlowDirection()
{
//CollectionView.UpdateFlowDirection(ItemsView);
if (_emptyViewDisplayed)
{
AlignEmptyView();
}
Layout.InvalidateLayout();
}
public override nint NumberOfSections(UICollectionView collectionView)
{
CheckForEmptySource();
return ItemsSource.GroupCount;
}
protected virtual void UpdateDefaultCell(DefaultCell cell, NSIndexPath indexPath)
{
cell.Label.Text = ItemsSource[indexPath].ToString();
if (cell is ItemsViewCell constrainedCell)
{
ItemsViewLayout.PrepareCellForLayout(constrainedCell);
}
}
protected virtual void UpdateTemplatedCell(TemplatedCell cell, NSIndexPath indexPath)
{
cell.ContentSizeChanged -= CellContentSizeChanged;
cell.LayoutAttributesChanged -= CellLayoutAttributesChanged;
var bindingContext = ItemsSource[indexPath];
// If we've already created a cell for this index path (for measurement), re-use the content
if (_measurementCells.TryGetValue(bindingContext, out TemplatedCell measurementCell))
{
_measurementCells.Remove(bindingContext);
measurementCell.ContentSizeChanged -= CellContentSizeChanged;
measurementCell.LayoutAttributesChanged -= CellLayoutAttributesChanged;
cell.UseContent(measurementCell);
}
else
{
cell.Bind(ItemsView.ItemTemplate, ItemsSource[indexPath], ItemsView);
}
cell.ContentSizeChanged += CellContentSizeChanged;
cell.LayoutAttributesChanged += CellLayoutAttributesChanged;
ItemsViewLayout.PrepareCellForLayout(cell);
}
public virtual NSIndexPath GetIndexForItem(object item)
{
return ItemsSource.GetIndexForItem(item);
}
protected object GetItemAtIndex(NSIndexPath index)
{
return ItemsSource[index];
}
void CellContentSizeChanged(object sender, EventArgs e)
{
if (_disposed)
return;
if (!(sender is TemplatedCell cell))
{
return;
}
var visibleCells = CollectionView.VisibleCells;
for (int n = 0; n < visibleCells.Length; n++)
{
if (cell == visibleCells[n])
{
Layout?.InvalidateLayout();
return;
}
}
}
void CellLayoutAttributesChanged(object sender, LayoutAttributesChangedEventArgs args)
{
CacheCellAttributes(args.NewAttributes.IndexPath, args.NewAttributes.Size);
}
protected virtual void CacheCellAttributes(NSIndexPath indexPath, CGSize size)
{
if (!ItemsSource.IsIndexPathValid(indexPath))
{
// The upate might be coming from a cell that's being removed; don't cache it.
return;
}
var item = ItemsSource[indexPath];
if (item != null)
{
ItemsViewLayout.CacheCellSize(item, size);
}
}
protected virtual string DetermineCellReuseId()
{
if (ItemsView.ItemTemplate != null)
{
return ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
? HorizontalCell.ReuseId
: VerticalCell.ReuseId;
}
return ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
? HorizontalDefaultCell.ReuseId
: VerticalDefaultCell.ReuseId;
}
UICollectionViewCell GetPrototype()
{
if (ItemsSource.ItemCount == 0)
{
return null;
}
var group = 0;
if (ItemsSource.GroupCount > 1)
{
// If we're in a grouping situation, then we need to make sure we find an actual data item
// to use for our prototype cell. It's possible that we have empty groups.
for (int n = 0; n < ItemsSource.GroupCount; n++)
{
if (ItemsSource.ItemCountInGroup(n) > 0)
{
group = n;
break;
}
}
}
var indexPath = NSIndexPath.Create(group, 0);
return CreateMeasurementCell(indexPath);
}
protected virtual void RegisterViewTypes()
{
CollectionView.RegisterClassForCell(typeof(HorizontalDefaultCell), HorizontalDefaultCell.ReuseId);
CollectionView.RegisterClassForCell(typeof(VerticalDefaultCell), VerticalDefaultCell.ReuseId);
CollectionView.RegisterClassForCell(typeof(HorizontalCell), HorizontalCell.ReuseId);
CollectionView.RegisterClassForCell(typeof(VerticalCell), VerticalCell.ReuseId);
}
protected abstract bool IsHorizontal { get; }
protected virtual CGRect DetermineEmptyViewFrame()
{
return new CGRect(CollectionView.Frame.X, CollectionView.Frame.Y,
CollectionView.Frame.Width, CollectionView.Frame.Height);
}
protected void RemeasureLayout(VisualElement formsElement)
{
if (IsHorizontal)
{
var request = formsElement.Measure(double.PositiveInfinity, CollectionView.Frame.Height, MeasureFlags.IncludeMargins);
Controls.Compatibility.Layout.LayoutChildIntoBoundingRegion(formsElement, new Rectangle(0, 0, request.Request.Width, CollectionView.Frame.Height));
}
else
{
var request = formsElement.Measure(CollectionView.Frame.Width, double.PositiveInfinity, MeasureFlags.IncludeMargins);
Controls.Compatibility.Layout.LayoutChildIntoBoundingRegion(formsElement, new Rectangle(0, 0, CollectionView.Frame.Width, request.Request.Height));
}
}
protected void OnFormsElementMeasureInvalidated(object sender, EventArgs e)
{
if (sender is VisualElement formsElement)
{
HandleFormsElementMeasureInvalidated(formsElement);
}
}
protected virtual void HandleFormsElementMeasureInvalidated(VisualElement formsElement)
{
RemeasureLayout(formsElement);
}
internal void UpdateView(object view, DataTemplate viewTemplate, ref UIView uiView, ref VisualElement formsElement)
{
// Is view set on the ItemsView?
if (view == null)
{
if (formsElement != null)
{
//Platform.GetRenderer(formsElement)?.DisposeRendererAndChildren();
}
uiView?.Dispose();
uiView = null;
formsElement = null;
}
else
{
// Create the native renderer for the view, and keep the actual Forms element (if any)
// around for updating the layout later
(uiView, formsElement) = TemplateHelpers.RealizeView(view, viewTemplate, ItemsView);
}
}
internal void UpdateEmptyView()
{
if (!_initialized)
{
return;
}
// Get rid of the old view
TearDownEmptyView();
// Set up the new empty view
UpdateView(ItemsView?.EmptyView, ItemsView?.EmptyViewTemplate, ref _emptyUIView, ref _emptyViewFormsElement);
// We may need to show the updated empty view
UpdateEmptyViewVisibility(ItemsSource?.ItemCount == 0);
}
void UpdateEmptyViewVisibility(bool isEmpty)
{
if (!_initialized)
{
return;
}
if (isEmpty)
{
ShowEmptyView();
}
else
{
HideEmptyView();
}
}
void AlignEmptyView()
{
if (_emptyUIView == null)
{
return;
}
if (CollectionView.EffectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirection.RightToLeft)
{
if (_emptyUIView.Transform.xx == -1)
{
return;
}
FlipEmptyView();
}
else
{
if (_emptyUIView.Transform.xx == -1)
{
FlipEmptyView();
}
}
}
void FlipEmptyView()
{
// Flip the empty view 180 degrees around the X axis
_emptyUIView.Transform = CGAffineTransform.Scale(_emptyUIView.Transform, -1, 1);
}
void ShowEmptyView()
{
if (_emptyViewDisplayed || _emptyUIView == null)
{
return;
}
_emptyUIView.Tag = EmptyTag;
CollectionView.AddSubview(_emptyUIView);
if (((IElementController)ItemsView).LogicalChildren.IndexOf(_emptyViewFormsElement) == -1)
{
ItemsView.AddLogicalChild(_emptyViewFormsElement);
}
LayoutEmptyView();
AlignEmptyView();
_emptyViewDisplayed = true;
}
void HideEmptyView()
{
if (!_emptyViewDisplayed || _emptyUIView == null)
{
return;
}
_emptyUIView.RemoveFromSuperview();
_emptyViewDisplayed = false;
}
void TearDownEmptyView()
{
HideEmptyView();
// RemoveLogicalChild will trigger a disposal of the native view and its content
ItemsView.RemoveLogicalChild(_emptyViewFormsElement);
_emptyUIView = null;
_emptyViewFormsElement = null;
}
void LayoutEmptyView()
{
if (!_initialized || _emptyUIView == null || _emptyUIView.Superview == null)
{
return;
}
var frame = DetermineEmptyViewFrame();
_emptyUIView.Frame = frame;
if (_emptyViewFormsElement != null && ((IElementController)ItemsView).LogicalChildren.IndexOf(_emptyViewFormsElement) != -1)
_emptyViewFormsElement.Layout(frame.ToRectangle());
}
TemplatedCell CreateAppropriateCellForLayout()
{
var frame = new CGRect(0, 0, ItemsViewLayout.EstimatedItemSize.Width, ItemsViewLayout.EstimatedItemSize.Height);
if (ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal)
{
return new HorizontalCell(frame);
}
return new VerticalCell(frame);
}
public UICollectionViewCell CreateMeasurementCell(NSIndexPath indexPath)
{
if (ItemsView.ItemTemplate == null)
{
var frame = new CGRect(0, 0, ItemsViewLayout.EstimatedItemSize.Width, ItemsViewLayout.EstimatedItemSize.Height);
DefaultCell cell;
if (ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal)
{
cell = new HorizontalDefaultCell(frame);
}
else
{
cell = new VerticalDefaultCell(frame);
}
UpdateDefaultCell(cell, indexPath);
return cell;
}
TemplatedCell templatedCell = CreateAppropriateCellForLayout();
UpdateTemplatedCell(templatedCell, indexPath);
// Keep this cell around, we can transfer the contents to the actual cell when the UICollectionView creates it
_measurementCells[ItemsSource[indexPath]] = templatedCell;
return templatedCell;
}
internal CGSize GetSizeForItem(NSIndexPath indexPath)
{
if (ItemsViewLayout.EstimatedItemSize.IsEmpty)
{
return ItemsViewLayout.ItemSize;
}
if (ItemsSource.IsIndexPathValid(indexPath))
{
var item = ItemsSource[indexPath];
if (item != null && ItemsViewLayout.TryGetCachedCellSize(item, out CGSize size))
{
return size;
}
}
return ItemsViewLayout.EstimatedItemSize;
}
internal protected virtual void UpdateVisibility()
{
if (ItemsView.IsVisible)
{
if (CollectionView.Hidden)
{
CollectionView.Hidden = false;
Layout.InvalidateLayout();
CollectionView.LayoutIfNeeded();
}
}
else
{
CollectionView.Hidden = true;
}
}
}
}

Просмотреть файл

@ -0,0 +1,170 @@
using System;
using System.Linq;
using CoreGraphics;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public class ItemsViewDelegator<TItemsView, TViewController> : UICollectionViewDelegateFlowLayout
where TItemsView : ItemsView
where TViewController : ItemsViewController<TItemsView>
{
public ItemsViewLayout ItemsViewLayout { get; }
public TViewController ViewController { get; }
protected float PreviousHorizontalOffset, PreviousVerticalOffset;
public ItemsViewDelegator(ItemsViewLayout itemsViewLayout, TViewController itemsViewController)
{
ItemsViewLayout = itemsViewLayout;
ViewController = itemsViewController;
}
public override void Scrolled(UIScrollView scrollView)
{
var (visibleItems, firstVisibleItemIndex, centerItemIndex, lastVisibleItemIndex) = GetVisibleItemsIndex();
if (!visibleItems)
return;
var contentInset = scrollView.ContentInset;
var contentOffsetX = scrollView.ContentOffset.X + contentInset.Left;
var contentOffsetY = scrollView.ContentOffset.Y + contentInset.Top;
var itemsViewScrolledEventArgs = new ItemsViewScrolledEventArgs
{
HorizontalDelta = contentOffsetX - PreviousHorizontalOffset,
VerticalDelta = contentOffsetY - PreviousVerticalOffset,
HorizontalOffset = contentOffsetX,
VerticalOffset = contentOffsetY,
FirstVisibleItemIndex = firstVisibleItemIndex,
CenterItemIndex = centerItemIndex,
LastVisibleItemIndex = lastVisibleItemIndex
};
var itemsView = ViewController.ItemsView;
var source = ViewController.ItemsSource;
itemsView.SendScrolled(itemsViewScrolledEventArgs);
PreviousHorizontalOffset = (float)contentOffsetX;
PreviousVerticalOffset = (float)contentOffsetY;
switch (itemsView.RemainingItemsThreshold)
{
case -1:
return;
case 0:
if (lastVisibleItemIndex == source.ItemCount - 1)
itemsView.SendRemainingItemsThresholdReached();
break;
default:
if (source.ItemCount - 1 - lastVisibleItemIndex <= itemsView.RemainingItemsThreshold)
itemsView.SendRemainingItemsThresholdReached();
break;
}
}
public override UIEdgeInsets GetInsetForSection(UICollectionView collectionView, UICollectionViewLayout layout,
nint section)
{
if (ItemsViewLayout == null)
{
return default;
}
return ItemsViewLayout.GetInsetForSection(collectionView, layout, section);
}
public override nfloat GetMinimumInteritemSpacingForSection(UICollectionView collectionView,
UICollectionViewLayout layout, nint section)
{
if (ItemsViewLayout == null)
{
return default;
}
return ItemsViewLayout.GetMinimumInteritemSpacingForSection(collectionView, layout, section);
}
public override nfloat GetMinimumLineSpacingForSection(UICollectionView collectionView,
UICollectionViewLayout layout, nint section)
{
if (ItemsViewLayout == null)
{
return default;
}
return ItemsViewLayout.GetMinimumLineSpacingForSection(collectionView, layout, section);
}
public override void CellDisplayingEnded(UICollectionView collectionView, UICollectionViewCell cell, NSIndexPath indexPath)
{
if (ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal)
{
var actualWidth = collectionView.ContentSize.Width - collectionView.Bounds.Size.Width;
if (collectionView.ContentOffset.X >= actualWidth || collectionView.ContentOffset.X < 0)
return;
}
else
{
var actualHeight = collectionView.ContentSize.Height - collectionView.Bounds.Size.Height;
if (collectionView.ContentOffset.Y >= actualHeight || collectionView.ContentOffset.Y < 0)
return;
}
}
protected virtual (bool VisibleItems, NSIndexPath First, NSIndexPath Center, NSIndexPath Last) GetVisibleItemsIndexPath()
{
var indexPathsForVisibleItems = ViewController.CollectionView.IndexPathsForVisibleItems.OrderBy(x => x.Row).ToList();
var visibleItems = indexPathsForVisibleItems.Count > 0;
NSIndexPath firstVisibleItemIndex = null, centerItemIndex = null, lastVisibleItemIndex = null;
if (visibleItems)
{
firstVisibleItemIndex = indexPathsForVisibleItems.First();
centerItemIndex = GetCenteredIndexPath(ViewController.CollectionView);
lastVisibleItemIndex = indexPathsForVisibleItems.Last();
}
return (visibleItems, firstVisibleItemIndex, centerItemIndex, lastVisibleItemIndex);
}
protected virtual (bool VisibleItems, int First, int Center, int Last) GetVisibleItemsIndex()
{
var (VisibleItems, First, Center, Last) = GetVisibleItemsIndexPath();
int firstVisibleItemIndex = -1, centerItemIndex = -1, lastVisibleItemIndex = -1;
if (VisibleItems)
{
firstVisibleItemIndex = (int)First.Item;
centerItemIndex = (int)Center.Item;
lastVisibleItemIndex = (int)Last.Item;
}
return (VisibleItems, firstVisibleItemIndex, centerItemIndex, lastVisibleItemIndex);
}
static NSIndexPath GetCenteredIndexPath(UICollectionView collectionView)
{
NSIndexPath centerItemIndex = null;
var indexPathsForVisibleItems = collectionView.IndexPathsForVisibleItems.OrderBy(x => x.Row).ToList();
if (indexPathsForVisibleItems.Count == 0)
return centerItemIndex;
var firstVisibleItemIndex = indexPathsForVisibleItems.First();
var centerPoint = new CGPoint(collectionView.Center.X + collectionView.ContentOffset.X, collectionView.Center.Y + collectionView.ContentOffset.Y);
var centerIndexPath = collectionView.IndexPathForItemAtPoint(centerPoint);
centerItemIndex = centerIndexPath ?? firstVisibleItemIndex;
return centerItemIndex;
}
public override CGSize GetSizeForItem(UICollectionView collectionView, UICollectionViewLayout layout, NSIndexPath indexPath)
{
return ViewController.GetSizeForItem(indexPath);
}
}
}

Просмотреть файл

@ -0,0 +1,609 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using CoreGraphics;
using Foundation;
using Microsoft.Maui.Controls.Internals;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public abstract class ItemsViewLayout : UICollectionViewFlowLayout
{
readonly ItemsLayout _itemsLayout;
bool _disposed;
bool _adjustContentOffset;
CGSize _adjustmentSize0;
CGSize _adjustmentSize1;
CGSize _currentSize;
const double ConstraintSizeTolerance = 0.00001;
Dictionary<object, CGSize> _cellSizeCache = new Dictionary<object, CGSize>();
public ItemsUpdatingScrollMode ItemsUpdatingScrollMode { get; set; }
public nfloat ConstrainedDimension { get; set; }
public Func<UICollectionViewCell> GetPrototype { get; set; }
internal ItemSizingStrategy ItemSizingStrategy { get; private set; }
protected ItemsViewLayout(ItemsLayout itemsLayout, ItemSizingStrategy itemSizingStrategy = ItemSizingStrategy.MeasureFirstItem)
{
ItemSizingStrategy = itemSizingStrategy;
_itemsLayout = itemsLayout;
_itemsLayout.PropertyChanged += LayoutOnPropertyChanged;
var scrollDirection = itemsLayout.Orientation == ItemsLayoutOrientation.Horizontal
? UICollectionViewScrollDirection.Horizontal
: UICollectionViewScrollDirection.Vertical;
Initialize(scrollDirection);
if (NativeVersion.IsAtLeast(11))
{
// `ContentInset` is actually the default value, but I'm leaving this here as a note to
// future maintainers; it's likely that someone will want a Platform Specific to change this behavior
// (Setting it to `SafeArea` lets you do the thing where the header/footer of your UICollectionView
// fills the screen width in landscape while your items are automatically shifted to avoid the notch)
SectionInsetReference = UICollectionViewFlowLayoutSectionInsetReference.ContentInset;
}
}
public override bool FlipsHorizontallyInOppositeLayoutDirection => true;
protected override void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
if (_itemsLayout != null)
{
_itemsLayout.PropertyChanged -= LayoutOnPropertyChanged;
}
}
base.Dispose(disposing);
}
void LayoutOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChanged)
{
HandlePropertyChanged(propertyChanged);
}
protected virtual void HandlePropertyChanged(PropertyChangedEventArgs propertyChanged)
{
if (propertyChanged.IsOneOf(LinearItemsLayout.ItemSpacingProperty,
GridItemsLayout.HorizontalItemSpacingProperty, GridItemsLayout.VerticalItemSpacingProperty))
{
UpdateItemSpacing();
}
}
internal virtual void UpdateConstraints(CGSize size)
{
if (!RequiresConstraintUpdate(size, _currentSize))
{
return;
}
ClearCellSizeCache();
_currentSize = size;
var newSize = new CGSize(Math.Floor(size.Width), Math.Floor(size.Height));
ConstrainTo(newSize);
UpdateCellConstraints();
}
internal void SetInitialConstraints(CGSize size)
{
_currentSize = size;
ConstrainTo(size);
}
public abstract void ConstrainTo(CGSize size);
public virtual UIEdgeInsets GetInsetForSection(UICollectionView collectionView, UICollectionViewLayout layout,
nint section)
{
if (_itemsLayout is GridItemsLayout gridItemsLayout)
{
if (ScrollDirection == UICollectionViewScrollDirection.Horizontal)
{
return new UIEdgeInsets(0, 0, 0, (nfloat)gridItemsLayout.HorizontalItemSpacing * collectionView.NumberOfItemsInSection(section));
}
return new UIEdgeInsets(0, 0, (nfloat)gridItemsLayout.VerticalItemSpacing * collectionView.NumberOfItemsInSection(section), 0);
}
return UIEdgeInsets.Zero;
}
public virtual nfloat GetMinimumInteritemSpacingForSection(UICollectionView collectionView,
UICollectionViewLayout layout, nint section)
{
return (nfloat)0.0;
}
public virtual nfloat GetMinimumLineSpacingForSection(UICollectionView collectionView,
UICollectionViewLayout layout, nint section)
{
if (_itemsLayout is LinearItemsLayout listViewLayout)
{
return (nfloat)listViewLayout.ItemSpacing;
}
if (_itemsLayout is GridItemsLayout gridItemsLayout)
{
if (ScrollDirection == UICollectionViewScrollDirection.Horizontal)
{
return (nfloat)gridItemsLayout.HorizontalItemSpacing;
}
return (nfloat)gridItemsLayout.VerticalItemSpacing;
}
return (nfloat)0.0;
}
public void PrepareCellForLayout(ItemsViewCell cell)
{
if (EstimatedItemSize == CGSize.Empty)
{
cell.ConstrainTo(ItemSize);
}
else
{
cell.ConstrainTo(ConstrainedDimension);
}
}
public override bool ShouldInvalidateLayout(UICollectionViewLayoutAttributes preferredAttributes, UICollectionViewLayoutAttributes originalAttributes)
{
if (ItemSizingStrategy == ItemSizingStrategy.MeasureAllItems)
{
if (preferredAttributes.Bounds != originalAttributes.Bounds)
{
return true;
}
}
return base.ShouldInvalidateLayout(preferredAttributes, originalAttributes);
}
protected void DetermineCellSize()
{
if (GetPrototype == null)
{
return;
}
// We set the EstimatedItemSize here for two reasons:
// 1. If we don't set it, iOS versions below 10 will crash
// 2. If GetPrototype() cannot return a cell because the items source is empty, we need to have
// an estimate set so that when a cell _does_ become available (i.e., when the items source
// has at least one item), Autolayout will kick in for the first cell and size it correctly
// If GetPrototype() _can_ return a cell, this estimate will be updated once that cell is measured
if (EstimatedItemSize == CGSize.Empty)
{
EstimatedItemSize = new CGSize(1, 1);
}
ItemsViewCell prototype = null;
if (CollectionView?.VisibleCells.Length > 0)
{
prototype = CollectionView.VisibleCells[0] as ItemsViewCell;
}
if (prototype == null)
{
prototype = GetPrototype() as ItemsViewCell;
}
if (prototype == null)
{
return;
}
// Constrain and measure the prototype cell
prototype.ConstrainTo(ConstrainedDimension);
var measure = prototype.Measure();
if (ItemSizingStrategy == ItemSizingStrategy.MeasureFirstItem)
{
// This is the size we'll give all of our cells from here on out
ItemSize = measure;
// Make sure autolayout is disabled
EstimatedItemSize = CGSize.Empty;
}
else
{
// Autolayout is now enabled, and this is the size used to guess scrollbar size and progress
EstimatedItemSize = measure;
}
}
void Initialize(UICollectionViewScrollDirection scrollDirection)
{
ScrollDirection = scrollDirection;
}
protected void UpdateCellConstraints()
{
PrepareCellsForLayout(CollectionView.VisibleCells);
PrepareCellsForLayout(CollectionView.GetVisibleSupplementaryViews(UICollectionElementKindSectionKey.Header));
PrepareCellsForLayout(CollectionView.GetVisibleSupplementaryViews(UICollectionElementKindSectionKey.Footer));
}
void PrepareCellsForLayout(UICollectionReusableView[] cells)
{
for (int n = 0; n < cells.Length; n++)
{
if (cells[n] is ItemsViewCell constrainedCell)
{
PrepareCellForLayout(constrainedCell);
}
}
}
public override CGPoint TargetContentOffset(CGPoint proposedContentOffset, CGPoint scrollingVelocity)
{
var snapPointsType = _itemsLayout.SnapPointsType;
if (snapPointsType == SnapPointsType.None)
{
// Nothing to do here; fall back to the default
return base.TargetContentOffset(proposedContentOffset, scrollingVelocity);
}
var alignment = _itemsLayout.SnapPointsAlignment;
if (snapPointsType == SnapPointsType.MandatorySingle)
{
// Mandatory snapping, single element
return ScrollSingle(alignment, proposedContentOffset, scrollingVelocity);
}
// Get the viewport of the UICollectionView at the proposed content offset
var viewport = new CGRect(proposedContentOffset, CollectionView.Bounds.Size);
// And find all the elements currently visible in the viewport
var visibleElements = LayoutAttributesForElementsInRect(viewport);
if (visibleElements.Length == 0)
{
// Nothing to see here; fall back to the default
return base.TargetContentOffset(proposedContentOffset, scrollingVelocity);
}
if (visibleElements.Length == 1)
{
// If there is only one item in the viewport, then we need to align the viewport with it
return SnapHelpers.AdjustContentOffset(proposedContentOffset, visibleElements[0].Frame, viewport,
alignment, ScrollDirection);
}
// If there are multiple items in the viewport, we need to choose the one which is
// closest to the relevant part of the viewport while being sufficiently visible
// Find the spot in the viewport we're trying to align with
var alignmentTarget = SnapHelpers.FindAlignmentTarget(alignment, proposedContentOffset,
CollectionView, ScrollDirection);
// Find the closest sufficiently visible candidate
var bestCandidate = SnapHelpers.FindBestSnapCandidate(visibleElements, viewport, alignmentTarget);
if (bestCandidate != null)
{
return SnapHelpers.AdjustContentOffset(proposedContentOffset, bestCandidate.Frame, viewport, alignment,
ScrollDirection);
}
// If we got this far an nothing matched, it means that we have multiple items but somehow
// none of them fit at least half in the viewport. So just fall back to the first item
return SnapHelpers.AdjustContentOffset(proposedContentOffset, visibleElements[0].Frame, viewport, alignment,
ScrollDirection);
}
CGPoint ScrollSingle(SnapPointsAlignment alignment, CGPoint proposedContentOffset, CGPoint scrollingVelocity)
{
// Get the viewport of the UICollectionView at the current content offset
var contentOffset = CollectionView.ContentOffset;
var viewport = new CGRect(contentOffset, CollectionView.Bounds.Size);
// Find the spot in the viewport we're trying to align with
var alignmentTarget = SnapHelpers.FindAlignmentTarget(alignment, contentOffset, CollectionView, ScrollDirection);
var visibleElements = LayoutAttributesForElementsInRect(viewport);
// Find the current aligned item
var currentItem = SnapHelpers.FindBestSnapCandidate(visibleElements, viewport, alignmentTarget);
if (currentItem == null)
{
// Somehow we don't currently have an item in the viewport near the target; fall back to the
// default behavior
return base.TargetContentOffset(proposedContentOffset, scrollingVelocity);
}
// Determine the index of the current item
var currentIndex = visibleElements.IndexOf(currentItem);
// Figure out the step size when jumping to the "next" element
var span = 1;
if (_itemsLayout is GridItemsLayout gridItemsLayout)
{
span = gridItemsLayout.Span;
}
// Find the next item in the
currentItem = SnapHelpers.FindNextItem(visibleElements, ScrollDirection, span, scrollingVelocity, currentIndex);
return SnapHelpers.AdjustContentOffset(CollectionView.ContentOffset, currentItem.Frame, viewport, alignment,
ScrollDirection);
}
protected virtual void UpdateItemSpacing()
{
if (_itemsLayout == null)
{
return;
}
InvalidateLayout();
}
public override UICollectionViewLayoutInvalidationContext GetInvalidationContext(UICollectionViewLayoutAttributes preferredAttributes, UICollectionViewLayoutAttributes originalAttributes)
{
if (preferredAttributes.RepresentedElementKind != UICollectionElementKindSectionKey.Header
&& preferredAttributes.RepresentedElementKind != UICollectionElementKindSectionKey.Footer)
{
if (NativeVersion.IsAtLeast(12))
{
return base.GetInvalidationContext(preferredAttributes, originalAttributes);
}
try
{
// We only have to do this on older iOS versions; sometimes when removing a cell that's right at the edge
// of the viewport we'll run into a race condition where the invalidation context will have the removed
// indexpath. And then things crash. So
var defaultContext = base.GetInvalidationContext(preferredAttributes, originalAttributes);
return defaultContext;
}
catch (MonoTouchException ex) when (ex.Name == "NSRangeException")
{
Controls.Internals.Log.Warning("ItemsViewLayout", ex.ToString());
}
UICollectionViewFlowLayoutInvalidationContext context = new UICollectionViewFlowLayoutInvalidationContext();
return context;
}
// Ensure that if this invalidation was triggered by header/footer changes, the header/footer are being invalidated
UICollectionViewFlowLayoutInvalidationContext invalidationContext = new UICollectionViewFlowLayoutInvalidationContext();
var indexPath = preferredAttributes.IndexPath;
if (preferredAttributes.RepresentedElementKind == UICollectionElementKindSectionKey.Header)
{
invalidationContext.InvalidateSupplementaryElements(UICollectionElementKindSectionKey.Header, new[] { indexPath });
}
else if (preferredAttributes.RepresentedElementKind == UICollectionElementKindSectionKey.Footer)
{
invalidationContext.InvalidateSupplementaryElements(UICollectionElementKindSectionKey.Footer, new[] { indexPath });
}
return invalidationContext;
}
public override void PrepareLayout()
{
base.PrepareLayout();
// PrepareLayout is the only good place to consistently track the content size changes
TrackOffsetAdjustment();
}
public override void PrepareForCollectionViewUpdates(UICollectionViewUpdateItem[] updateItems)
{
base.PrepareForCollectionViewUpdates(updateItems);
if (ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepScrollOffset)
{
// This is the default behavior for iOS, no need to do anything
return;
}
if (ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepItemsInView
|| ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepLastItemInView)
{
// If this update will shift the visible items, we'll have to adjust for
// that later in TargetContentOffsetForProposedContentOffset
_adjustContentOffset = UpdateWillShiftVisibleItems(CollectionView, updateItems);
}
}
public override CGPoint TargetContentOffsetForProposedContentOffset(CGPoint proposedContentOffset)
{
if (_adjustContentOffset)
{
_adjustContentOffset = false;
// PrepareForCollectionViewUpdates detected that an item update was going to shift the viewport
// and we want to make sure it stays in place
return proposedContentOffset + ComputeOffsetAdjustment();
}
return base.TargetContentOffsetForProposedContentOffset(proposedContentOffset);
}
public override void FinalizeCollectionViewUpdates()
{
base.FinalizeCollectionViewUpdates();
if (ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepLastItemInView)
{
ForceScrollToLastItem(CollectionView, _itemsLayout);
}
}
void TrackOffsetAdjustment()
{
// Keep track of the previous sizes of the CollectionView content so we can adjust the viewport
// offsets if we're in ItemsUpdatingScrollMode.KeepItemsInView
// We keep track of the last two adjustments because the only place we can consistently track this
// is PrepareLayout, and by the time PrepareLayout has been called, the CollectionViewContentSize
// has already been updated
if (_adjustmentSize0.IsEmpty)
{
_adjustmentSize0 = CollectionViewContentSize;
}
else if (_adjustmentSize1.IsEmpty)
{
_adjustmentSize1 = CollectionViewContentSize;
}
else
{
_adjustmentSize0 = _adjustmentSize1;
_adjustmentSize1 = CollectionViewContentSize;
}
}
CGSize ComputeOffsetAdjustment()
{
return CollectionViewContentSize - _adjustmentSize0;
}
static bool UpdateWillShiftVisibleItems(UICollectionView collectionView, UICollectionViewUpdateItem[] updateItems)
{
// Find the first visible item
var firstPath = collectionView.IndexPathsForVisibleItems.FindFirst();
if (firstPath == null)
{
// No visible items to shift
return false;
}
// Determine whether any of the new items will be "before" the first visible item
foreach (var item in updateItems)
{
if (item.UpdateAction == UICollectionUpdateAction.Delete
|| item.UpdateAction == UICollectionUpdateAction.Insert
|| item.UpdateAction == UICollectionUpdateAction.Move)
{
if (item.IndexPathAfterUpdate == null)
{
continue;
}
if (item.IndexPathAfterUpdate.IsLessThanOrEqualToPath(firstPath))
{
// If any of these items will end up "before" the first visible item, then the items will shift
return true;
}
}
}
return false;
}
static void ForceScrollToLastItem(UICollectionView collectionView, ItemsLayout itemsLayout)
{
var sections = (int)collectionView.NumberOfSections();
if (sections == 0)
{
return;
}
for (int section = sections - 1; section >= 0; section--)
{
var itemCount = collectionView.NumberOfItemsInSection(section);
if (itemCount > 0)
{
var lastIndexPath = NSIndexPath.FromItemSection(itemCount - 1, section);
if (itemsLayout.Orientation == ItemsLayoutOrientation.Vertical)
collectionView.ScrollToItem(lastIndexPath, UICollectionViewScrollPosition.Bottom, true);
else
collectionView.ScrollToItem(lastIndexPath, UICollectionViewScrollPosition.Right, true);
return;
}
}
}
public override bool ShouldInvalidateLayoutForBoundsChange(CGRect newBounds)
{
if (newBounds.Size == _currentSize)
{
return base.ShouldInvalidateLayoutForBoundsChange(newBounds);
}
if (NativeVersion.IsAtLeast(11))
{
UpdateConstraints(CollectionView.AdjustedContentInset.InsetRect(newBounds).Size);
}
else
{
UpdateConstraints(CollectionView.Bounds.Size);
}
return true;
}
internal bool TryGetCachedCellSize(object item, out CGSize size)
{
if (_cellSizeCache.TryGetValue(item, out CGSize internalSize))
{
size = internalSize;
return true;
}
size = CGSize.Empty;
return false;
}
internal void CacheCellSize(object item, CGSize size)
{
_cellSizeCache[item] = size;
}
internal void ClearCellSizeCache()
{
_cellSizeCache.Clear();
}
bool RequiresConstraintUpdate(CGSize newSize, CGSize current)
{
if (Math.Abs(newSize.Width - current.Width) > ConstraintSizeTolerance)
{
return true;
}
if (Math.Abs(newSize.Height - current.Height) > ConstraintSizeTolerance)
{
return true;
}
return false;
}
}
}

Просмотреть файл

@ -0,0 +1,12 @@
using System;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public class LayoutAttributesChangedEventArgs : EventArgs
{
public UICollectionViewLayoutAttributes NewAttributes { get; }
public LayoutAttributesChangedEventArgs(UICollectionViewLayoutAttributes newAttributes) => NewAttributes = newAttributes;
}
}

Просмотреть файл

@ -0,0 +1,77 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Foundation;
namespace Microsoft.Maui.Controls.Handlers.Items
{
class ListSource : List<object>, IItemsViewSource
{
public ListSource()
{
}
public ListSource(IEnumerable<object> enumerable) : base(enumerable)
{
}
public ListSource(IEnumerable enumerable)
{
foreach (object item in enumerable)
{
Add(item);
}
}
public void Dispose()
{
}
public object this[NSIndexPath indexPath]
{
get
{
if (indexPath.Section > 0)
{
throw new ArgumentOutOfRangeException(nameof(indexPath));
}
return this[(int)indexPath.Item];
}
}
public int GroupCount => 1;
public int ItemCount => Count;
public NSIndexPath GetIndexForItem(object item)
{
for (int n = 0; n < Count; n++)
{
if (this[n] == item)
{
return NSIndexPath.Create(0, n);
}
}
return NSIndexPath.Create(-1, -1);
}
public object Group(NSIndexPath indexPath)
{
return null;
}
public int ItemCountInGroup(nint group)
{
if (group > 0)
{
throw new ArgumentOutOfRangeException(nameof(group));
}
return Count;
}
}
}

Просмотреть файл

@ -0,0 +1,19 @@
using CoreGraphics;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public class ListViewLayout : ItemsViewLayout
{
public ListViewLayout(LinearItemsLayout itemsLayout, ItemSizingStrategy itemSizingStrategy) : base(itemsLayout, itemSizingStrategy)
{
}
public override void ConstrainTo(CGSize size)
{
ConstrainedDimension =
ScrollDirection == UICollectionViewScrollDirection.Vertical ? size.Width : size.Height;
DetermineCellSize();
}
}
}

Просмотреть файл

@ -0,0 +1,30 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal class LoopListSource : ListSource, ILoopItemsViewSource
{
const int LoopBy = 3;
public LoopListSource(IEnumerable<object> enumerable, bool loop) : base(enumerable)
{
Loop = loop;
}
public LoopListSource(IEnumerable enumerable, bool loop)
{
Loop = loop;
foreach (object item in enumerable)
{
Add(item);
}
}
public bool Loop { get; set; }
public int LoopCount => Loop ? Count * LoopBy : Count;
}
}

Просмотреть файл

@ -0,0 +1,31 @@
using System.Collections;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal class LoopObservableItemsSource : ObservableItemsSource, ILoopItemsViewSource
{
const int LoopBy = 3;
public LoopObservableItemsSource(IEnumerable itemSource, UICollectionViewController collectionViewController, bool loop, int group = -1) : base(itemSource, collectionViewController, group)
{
Loop = loop;
}
public bool Loop { get; set; }
public int LoopCount => Loop ? Count * LoopBy : Count;
protected override NSIndexPath[] CreateIndexesFrom(int startIndex, int count)
{
if (!Loop)
{
return base.CreateIndexesFrom(startIndex, count);
}
return IndexPathHelpers.GenerateLoopedIndexPathRange(Section,
(int)CollectionView.NumberOfItemsInSection(Section), LoopBy, startIndex, count);
}
}
}

Просмотреть файл

@ -0,0 +1,356 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal class ObservableGroupedSource : IItemsViewSource
{
readonly UICollectionView _collectionView;
readonly UICollectionViewController _collectionViewController;
readonly IList _groupSource;
bool _disposed;
List<ObservableItemsSource> _groups = new List<ObservableItemsSource>();
public ObservableGroupedSource(IEnumerable groupSource, UICollectionViewController collectionViewController)
{
_collectionViewController = collectionViewController;
_collectionView = _collectionViewController.CollectionView;
_groupSource = groupSource as IList ?? new ListSource(groupSource);
if (_groupSource is INotifyCollectionChanged incc)
{
incc.CollectionChanged += CollectionChanged;
}
ResetGroupTracking();
}
public object this[NSIndexPath indexPath]
{
get
{
return GetGroupItemAt(indexPath.Section, (int)indexPath.Item);
}
}
public int GroupCount => _groupSource.Count;
public int ItemCount
{
get
{
var total = 0;
for (int n = 0; n < _groupSource.Count; n++)
{
total += GetGroupCount(n);
}
return total;
}
}
public NSIndexPath GetIndexForItem(object item)
{
for (int i = 0; i < _groupSource.Count; i++)
{
var j = IndexInGroup(item, _groupSource[i]);
if (j == -1)
{
continue;
}
return NSIndexPath.Create(i, j);
}
return NSIndexPath.Create(-1, -1);
}
public object Group(NSIndexPath indexPath)
{
return _groupSource[indexPath.Section];
}
public int ItemCountInGroup(nint group)
{
return GetGroupCount((int)group);
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
ClearGroupTracking();
if (_groupSource is INotifyCollectionChanged incc)
{
incc.CollectionChanged -= CollectionChanged;
}
}
}
void ClearGroupTracking()
{
for (int n = _groups.Count - 1; n >= 0; n--)
{
_groups[n].Dispose();
_groups.RemoveAt(n);
}
}
void ResetGroupTracking()
{
ClearGroupTracking();
for (int n = 0; n < _groupSource.Count; n++)
{
if (_groupSource[n] is INotifyCollectionChanged && _groupSource[n] is IEnumerable list)
{
_groups.Add(new ObservableItemsSource(list, _collectionViewController, n));
}
}
}
void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
if (Device.IsInvokeRequired)
{
Device.BeginInvokeOnMainThread(() => CollectionChanged(args));
}
else
{
CollectionChanged(args);
}
}
void CollectionChanged(NotifyCollectionChangedEventArgs args)
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
Add(args);
break;
case NotifyCollectionChangedAction.Remove:
Remove(args);
break;
case NotifyCollectionChangedAction.Replace:
Replace(args);
break;
case NotifyCollectionChangedAction.Move:
Move(args);
break;
case NotifyCollectionChangedAction.Reset:
Reload();
break;
default:
throw new ArgumentOutOfRangeException();
}
}
void Reload()
{
ResetGroupTracking();
_collectionView.ReloadData();
_collectionView.CollectionViewLayout.InvalidateLayout();
}
NSIndexSet CreateIndexSetFrom(int startIndex, int count)
{
return NSIndexSet.FromNSRange(new NSRange(startIndex, count));
}
bool NotLoadedYet()
{
// If the UICollectionView hasn't actually been loaded, then calling InsertSections or DeleteSections is
// going to crash or get in an unusable state; instead, ReloadData should be used
return !_collectionViewController.IsViewLoaded || _collectionViewController.View.Window == null;
}
void Add(NotifyCollectionChangedEventArgs args)
{
if (ReloadRequired())
{
Reload();
return;
}
var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _groupSource.IndexOf(args.NewItems[0]);
var count = args.NewItems.Count;
// Adding a group will change the section index for all subsequent groups, so the easiest thing to do
// is to reset all the group tracking to get it up-to-date
ResetGroupTracking();
// Queue up the updates to the UICollectionView
Update(() => _collectionView.InsertSections(CreateIndexSetFrom(startIndex, count)));
}
void Remove(NotifyCollectionChangedEventArgs args)
{
var startIndex = args.OldStartingIndex;
if (startIndex < 0)
{
// INCC implementation isn't giving us enough information to know where the removed items were in the
// collection. So the best we can do is a complete reload
Reload();
return;
}
if (ReloadRequired())
{
Reload();
return;
}
// Removing a group will change the section index for all subsequent groups, so the easiest thing to do
// is to reset all the group tracking to get it up-to-date
ResetGroupTracking();
// Since we have a start index, we can be more clever about removing the item(s) (and get the nifty animations)
var count = args.OldItems.Count;
// Queue up the updates to the UICollectionView
Update(() => _collectionView.DeleteSections(CreateIndexSetFrom(startIndex, count)));
}
void Replace(NotifyCollectionChangedEventArgs args)
{
var newCount = args.NewItems.Count;
if (newCount == args.OldItems.Count)
{
ResetGroupTracking();
var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _groupSource.IndexOf(args.NewItems[0]);
// We are replacing one set of items with a set of equal size; we can do a simple item range update
Update(() => _collectionView.ReloadSections(CreateIndexSetFrom(startIndex, newCount)));
return;
}
// The original and replacement sets are of unequal size; this means that everything currently in view will
// have to be updated. So we just have to use ReloadData and let the UICollectionView update everything
Reload();
}
void Move(NotifyCollectionChangedEventArgs args)
{
var count = args.NewItems.Count;
ResetGroupTracking();
if (count == 1)
{
// For a single item, we can use MoveSection and get the animation
Update(() => _collectionView.MoveSection(args.OldStartingIndex, args.NewStartingIndex));
return;
}
var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex);
var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count;
Update(() => _collectionView.ReloadSections(CreateIndexSetFrom(start, end)));
}
int GetGroupCount(int groupIndex)
{
switch (_groupSource[groupIndex])
{
case IList list:
return list.Count;
case IEnumerable enumerable:
var count = 0;
var enumerator = enumerable.GetEnumerator();
while (enumerator.MoveNext())
{
count += 1;
}
return count;
}
return 0;
}
object GetGroupItemAt(int groupIndex, int index)
{
switch (_groupSource[groupIndex])
{
case IList list:
return list[index];
case IEnumerable enumerable:
var count = -1;
var enumerator = enumerable.GetEnumerator();
do
{
enumerator.MoveNext();
count += 1;
}
while (count < index);
return enumerator.Current;
}
return null;
}
int IndexInGroup(object item, object group)
{
switch (group)
{
case IList list:
return list.IndexOf(item);
case IEnumerable enumerable:
var enumerator = enumerable.GetEnumerator();
var index = 0;
while (enumerator.MoveNext())
{
if (enumerator.Current == item)
{
return index;
}
}
return -1;
}
return -1;
}
bool ReloadRequired()
{
// If the UICollectionView has never been loaded, or doesn't yet have any sections, any insert/delete operations
// are gonna crash hard. We'll need to reload the data instead.
return NotLoadedYet()
|| _collectionView.NumberOfSections() == 0;
}
void Update(Action update)
{
if (_collectionView.Hidden)
{
return;
}
update();
}
}
}

Просмотреть файл

@ -0,0 +1,294 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal class ObservableItemsSource : IItemsViewSource
{
readonly UICollectionViewController _collectionViewController;
protected readonly UICollectionView CollectionView;
readonly bool _grouped;
readonly int _section;
readonly IEnumerable _itemsSource;
bool _disposed;
public ObservableItemsSource(IEnumerable itemSource, UICollectionViewController collectionViewController, int group = -1)
{
_collectionViewController = collectionViewController;
CollectionView = _collectionViewController.CollectionView;
_section = group < 0 ? 0 : group;
_grouped = group >= 0;
_itemsSource = itemSource;
Count = ItemsCount();
((INotifyCollectionChanged)itemSource).CollectionChanged += CollectionChanged;
}
internal event NotifyCollectionChangedEventHandler CollectionViewUpdating;
internal event NotifyCollectionChangedEventHandler CollectionViewUpdated;
public int Count { get; private set; }
public int Section => _section;
public object this[int index] => ElementAt(index);
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
((INotifyCollectionChanged)_itemsSource).CollectionChanged -= CollectionChanged;
}
_disposed = true;
}
}
public int ItemCountInGroup(nint group)
{
return Count;
}
public object Group(NSIndexPath indexPath)
{
return null;
}
public NSIndexPath GetIndexForItem(object item)
{
for (int n = 0; n < Count; n++)
{
if (this[n] == item)
{
return NSIndexPath.Create(_section, n);
}
}
return NSIndexPath.Create(-1, -1);
}
public int GroupCount => 1;
public int ItemCount => Count;
public object this[NSIndexPath indexPath]
{
get
{
if (indexPath.Section != _section)
{
throw new ArgumentOutOfRangeException(nameof(indexPath));
}
return this[(int)indexPath.Item];
}
}
void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
if (Device.IsInvokeRequired)
{
Device.BeginInvokeOnMainThread(() => CollectionChanged(args));
}
else
{
CollectionChanged(args);
}
}
void CollectionChanged(NotifyCollectionChangedEventArgs args)
{
// Force UICollectionView to get the internal accounting straight
CollectionView.NumberOfItemsInSection(_section);
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
Add(args);
break;
case NotifyCollectionChangedAction.Remove:
Remove(args);
break;
case NotifyCollectionChangedAction.Replace:
Replace(args);
break;
case NotifyCollectionChangedAction.Move:
Move(args);
break;
case NotifyCollectionChangedAction.Reset:
Reload();
break;
default:
throw new ArgumentOutOfRangeException();
}
}
void Reload()
{
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
Count = ItemsCount();
OnCollectionViewUpdating(args);
CollectionView.ReloadData();
CollectionView.CollectionViewLayout.InvalidateLayout();
OnCollectionViewUpdated(args);
}
protected virtual NSIndexPath[] CreateIndexesFrom(int startIndex, int count)
{
return IndexPathHelpers.GenerateIndexPathRange(_section, startIndex, count);
}
void Add(NotifyCollectionChangedEventArgs args)
{
var count = args.NewItems.Count;
Count += count;
var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : IndexOf(args.NewItems[0]);
// Queue up the updates to the UICollectionView
Update(() => CollectionView.InsertItems(CreateIndexesFrom(startIndex, count)), args);
}
void Remove(NotifyCollectionChangedEventArgs args)
{
var startIndex = args.OldStartingIndex;
if (startIndex < 0)
{
// INCC implementation isn't giving us enough information to know where the removed items were in the
// collection. So the best we can do is a ReloadData()
Reload();
return;
}
// If we have a start index, we can be more clever about removing the item(s) (and get the nifty animations)
var count = args.OldItems.Count;
Count -= count;
Update(() => CollectionView.DeleteItems(CreateIndexesFrom(startIndex, count)), args);
}
void Replace(NotifyCollectionChangedEventArgs args)
{
var newCount = args.NewItems.Count;
if (newCount == args.OldItems.Count)
{
var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : IndexOf(args.NewItems[0]);
// We are replacing one set of items with a set of equal size; we can do a simple item range update
Update(() => CollectionView.ReloadItems(CreateIndexesFrom(startIndex, newCount)), args);
return;
}
// The original and replacement sets are of unequal size; this means that everything currently in view will
// have to be updated. So we just have to use ReloadData and let the UICollectionView update everything
Reload();
}
void Move(NotifyCollectionChangedEventArgs args)
{
var count = args.NewItems.Count;
if (count == 1)
{
// For a single item, we can use MoveItem and get the animation
var oldPath = NSIndexPath.Create(_section, args.OldStartingIndex);
var newPath = NSIndexPath.Create(_section, args.NewStartingIndex);
Update(() => CollectionView.MoveItem(oldPath, newPath), args);
return;
}
var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex);
var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count;
Update(() => CollectionView.ReloadItems(CreateIndexesFrom(start, end)), args);
}
internal int ItemsCount()
{
if (_itemsSource is IList list)
return list.Count;
int count = 0;
foreach (var item in _itemsSource)
count++;
return count;
}
internal object ElementAt(int index)
{
if (_itemsSource is IList list)
return list[index];
int count = 0;
foreach (var item in _itemsSource)
{
if (count == index)
return item;
count++;
}
return -1;
}
internal int IndexOf(object item)
{
if (_itemsSource is IList list)
return list.IndexOf(item);
int count = 0;
foreach (var i in _itemsSource)
{
if (i == item)
return count;
count++;
}
return -1;
}
void Update(Action update, NotifyCollectionChangedEventArgs args)
{
if (CollectionView.Hidden)
{
return;
}
OnCollectionViewUpdating(args);
update();
OnCollectionViewUpdated(args);
}
void OnCollectionViewUpdating(NotifyCollectionChangedEventArgs args)
{
CollectionViewUpdating?.Invoke(this, args);
}
void OnCollectionViewUpdated(NotifyCollectionChangedEventArgs args)
{
Device.BeginInvokeOnMainThread(() =>
{
CollectionViewUpdated?.Invoke(this, args);
});
}
}
}

Просмотреть файл

@ -0,0 +1,48 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal static class PropertyChangedEventArgsExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Is(this PropertyChangedEventArgs args, BindableProperty property)
{
return args.PropertyName == property.PropertyName;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsOneOf(this PropertyChangedEventArgs args, BindableProperty p0, BindableProperty p1)
{
return args.PropertyName == p0.PropertyName ||
args.PropertyName == p1.PropertyName;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsOneOf(this PropertyChangedEventArgs args, BindableProperty p0, BindableProperty p1, BindableProperty p2)
{
return args.PropertyName == p0.PropertyName ||
args.PropertyName == p1.PropertyName ||
args.PropertyName == p2.PropertyName;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsOneOf(this PropertyChangedEventArgs args, BindableProperty p0, BindableProperty p1, BindableProperty p2, BindableProperty p3)
{
return args.PropertyName == p0.PropertyName ||
args.PropertyName == p1.PropertyName ||
args.PropertyName == p2.PropertyName ||
args.PropertyName == p3.PropertyName;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsOneOf(this PropertyChangedEventArgs args, BindableProperty p0, BindableProperty p1, BindableProperty p2, BindableProperty p3, BindableProperty p4)
{
return args.PropertyName == p0.PropertyName ||
args.PropertyName == p1.PropertyName ||
args.PropertyName == p2.PropertyName ||
args.PropertyName == p3.PropertyName ||
args.PropertyName == p4.PropertyName;
}
}
}

Просмотреть файл

@ -0,0 +1,50 @@
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public static class ScrollToPositionExtensions
{
public static UICollectionViewScrollPosition ToCollectionViewScrollPosition(this ScrollToPosition scrollToPosition,
UICollectionViewScrollDirection scrollDirection = UICollectionViewScrollDirection.Vertical, bool isLtr = false)
{
if (scrollDirection == UICollectionViewScrollDirection.Horizontal)
{
return scrollToPosition.ToHorizontalCollectionViewScrollPosition(isLtr);
}
return scrollToPosition.ToVerticalCollectionViewScrollPosition();
}
public static UICollectionViewScrollPosition ToHorizontalCollectionViewScrollPosition(this ScrollToPosition scrollToPosition, bool isLtr)
{
switch (scrollToPosition)
{
case ScrollToPosition.MakeVisible:
return UICollectionViewScrollPosition.None;
case ScrollToPosition.Start:
return isLtr ? UICollectionViewScrollPosition.Right : UICollectionViewScrollPosition.Left;
case ScrollToPosition.End:
return isLtr ? UICollectionViewScrollPosition.Left : UICollectionViewScrollPosition.Right;
case ScrollToPosition.Center:
default:
return UICollectionViewScrollPosition.CenteredHorizontally;
}
}
public static UICollectionViewScrollPosition ToVerticalCollectionViewScrollPosition(this ScrollToPosition scrollToPosition)
{
switch (scrollToPosition)
{
case ScrollToPosition.MakeVisible:
return UICollectionViewScrollPosition.None;
case ScrollToPosition.Start:
return UICollectionViewScrollPosition.Top;
case ScrollToPosition.End:
return UICollectionViewScrollPosition.Bottom;
case ScrollToPosition.Center:
default:
return UICollectionViewScrollPosition.CenteredVertically;
}
}
}
}

Просмотреть файл

@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public class SelectableItemsViewController<TItemsView> : StructuredItemsViewController<TItemsView>
where TItemsView : SelectableItemsView
{
public SelectableItemsViewController(TItemsView selectableItemsView, ItemsViewLayout layout)
: base(selectableItemsView, layout)
{
}
protected override UICollectionViewDelegateFlowLayout CreateDelegator()
{
return new SelectableItemsViewDelegator<TItemsView, SelectableItemsViewController<TItemsView>>(ItemsViewLayout, this);
}
// _Only_ called if the user initiates the selection change; will not be called for programmatic selection
public override void ItemSelected(UICollectionView collectionView, NSIndexPath indexPath)
{
FormsSelectItem(indexPath);
}
// _Only_ called if the user initiates the selection change; will not be called for programmatic selection
public override void ItemDeselected(UICollectionView collectionView, NSIndexPath indexPath)
{
FormsDeselectItem(indexPath);
}
// Called by Forms to mark an item selected
internal void SelectItem(object selectedItem)
{
var index = GetIndexForItem(selectedItem);
if (index.Section > -1 && index.Item > -1)
{
CollectionView.SelectItem(index, true, UICollectionViewScrollPosition.None);
}
}
// Called by Forms to clear the native selection
internal void ClearSelection()
{
var selectedItemIndexes = CollectionView.GetIndexPathsForSelectedItems();
foreach (var index in selectedItemIndexes)
{
CollectionView.DeselectItem(index, true);
}
}
void FormsSelectItem(NSIndexPath indexPath)
{
var mode = ItemsView.SelectionMode;
switch (mode)
{
case SelectionMode.None:
break;
case SelectionMode.Single:
ItemsView.SelectedItem = GetItemAtIndex(indexPath);
break;
case SelectionMode.Multiple:
ItemsView.SelectedItems.Add(GetItemAtIndex(indexPath));
break;
}
}
void FormsDeselectItem(NSIndexPath indexPath)
{
var mode = ItemsView.SelectionMode;
switch (mode)
{
case SelectionMode.None:
break;
case SelectionMode.Single:
break;
case SelectionMode.Multiple:
ItemsView.SelectedItems.Remove(GetItemAtIndex(indexPath));
break;
}
}
internal void UpdateNativeSelection()
{
if (ItemsView == null)
{
return;
}
var mode = ItemsView.SelectionMode;
switch (mode)
{
case SelectionMode.None:
return;
case SelectionMode.Single:
var selectedItem = ItemsView.SelectedItem;
if (selectedItem != null)
{
SelectItem(selectedItem);
}
else
{
// SelectedItem has been set to null; if an item is selected, we need to de-select it
ClearSelection();
}
return;
case SelectionMode.Multiple:
SynchronizeNativeSelectionWithSelectedItems();
break;
}
}
internal void UpdateSelectionMode()
{
var mode = ItemsView.SelectionMode;
switch (mode)
{
case SelectionMode.None:
CollectionView.AllowsSelection = false;
CollectionView.AllowsMultipleSelection = false;
break;
case SelectionMode.Single:
CollectionView.AllowsSelection = true;
CollectionView.AllowsMultipleSelection = false;
break;
case SelectionMode.Multiple:
CollectionView.AllowsSelection = true;
CollectionView.AllowsMultipleSelection = true;
break;
}
UpdateNativeSelection();
}
void SynchronizeNativeSelectionWithSelectedItems()
{
var selectedItems = ItemsView.SelectedItems;
var selectedIndexPaths = CollectionView.GetIndexPathsForSelectedItems();
foreach (var path in selectedIndexPaths)
{
var itemAtPath = GetItemAtIndex(path);
if (ShouldNotBeSelected(itemAtPath, selectedItems))
{
CollectionView.DeselectItem(path, true);
}
}
foreach (var item in selectedItems)
{
SelectItem(item);
}
}
bool ShouldNotBeSelected(object item, IList<object> selectedItems)
{
for (int n = 0; n < selectedItems.Count; n++)
{
if (selectedItems[n] == item)
{
return false;
}
}
return true;
}
}
}

Просмотреть файл

@ -0,0 +1,25 @@
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public class SelectableItemsViewDelegator<TItemsView, TViewController> : ItemsViewDelegator<TItemsView, TViewController>
where TItemsView : SelectableItemsView
where TViewController : SelectableItemsViewController<TItemsView>
{
public SelectableItemsViewDelegator(ItemsViewLayout itemsViewLayout, TViewController itemsViewController)
: base(itemsViewLayout, itemsViewController)
{
}
public override void ItemSelected(UICollectionView collectionView, NSIndexPath indexPath)
{
ViewController?.ItemSelected(collectionView, indexPath);
}
public override void ItemDeselected(UICollectionView collectionView, NSIndexPath indexPath)
{
ViewController?.ItemDeselected(collectionView, indexPath);
}
}
}

Просмотреть файл

@ -0,0 +1,198 @@
using System;
using CoreGraphics;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal class SnapHelpers
{
public static CGPoint AdjustContentOffset(CGPoint proposedContentOffset, CGRect itemFrame,
CGRect viewport, SnapPointsAlignment alignment, UICollectionViewScrollDirection scrollDirection)
{
var offset = GetViewportOffset(itemFrame, viewport, alignment, scrollDirection);
return new CGPoint(proposedContentOffset.X - offset.X, proposedContentOffset.Y - offset.Y);
}
public static CGPoint FindAlignmentTarget(SnapPointsAlignment snapPointsAlignment,
CGPoint contentOffset, UICollectionView collectionView, UICollectionViewScrollDirection scrollDirection)
{
var inset = collectionView.ContentInset;
var bounds = collectionView.Bounds;
switch (scrollDirection)
{
case UICollectionViewScrollDirection.Vertical:
var y = FindAlignmentTarget(snapPointsAlignment, contentOffset.Y, inset.Top,
contentOffset.Y + bounds.Height, inset.Bottom);
return new CGPoint(contentOffset.X, y);
case UICollectionViewScrollDirection.Horizontal:
var x = FindAlignmentTarget(snapPointsAlignment, contentOffset.X, inset.Left,
contentOffset.X + bounds.Width, inset.Right);
return new CGPoint(x, contentOffset.Y);
default:
throw new ArgumentOutOfRangeException();
}
}
public static UICollectionViewLayoutAttributes FindBestSnapCandidate(UICollectionViewLayoutAttributes[] items,
CGRect viewport, CGPoint alignmentTarget)
{
UICollectionViewLayoutAttributes bestCandidate = null;
foreach (UICollectionViewLayoutAttributes item in items)
{
if (!IsAtLeastHalfVisible(item, viewport))
{
continue;
}
bestCandidate = bestCandidate == null ? item : Nearer(bestCandidate, item, alignmentTarget);
}
return bestCandidate;
}
static nfloat Area(CGRect rect)
{
return rect.Height * rect.Width;
}
static CGPoint Center(CGRect rect)
{
return new CGPoint(rect.X + rect.Width / 2, rect.Y + rect.Height / 2);
}
static nfloat DistanceSquared(CGRect rect, CGPoint target)
{
var rectCenter = Center(rect);
return (target.X - rectCenter.X) * (target.X - rectCenter.X) +
(target.Y - rectCenter.Y) * (target.Y - rectCenter.Y);
}
static int Clamp(int n, int min, int max)
{
if (n < min)
{
return min;
}
if (n > max)
{
return max;
}
return n;
}
static nfloat FindAlignmentTarget(SnapPointsAlignment snapPointsAlignment, nfloat start, nfloat startInset,
nfloat end, nfloat endInset)
{
switch (snapPointsAlignment)
{
case SnapPointsAlignment.Center:
var viewPortStart = start + startInset;
var viewPortEnd = end - endInset;
var viewPortSize = viewPortEnd - viewPortStart;
return viewPortStart + (viewPortSize / 2);
case SnapPointsAlignment.End:
return end - endInset;
case SnapPointsAlignment.Start:
default:
return start + startInset;
}
}
static CGPoint GetViewportOffset(CGRect itemFrame, CGRect viewport, SnapPointsAlignment snapPointsAlignment,
UICollectionViewScrollDirection scrollDirection)
{
if (scrollDirection == UICollectionViewScrollDirection.Horizontal)
{
if (snapPointsAlignment == SnapPointsAlignment.Start)
{
return new CGPoint(viewport.Left - itemFrame.Left, 0);
}
if (snapPointsAlignment == SnapPointsAlignment.End)
{
return new CGPoint(viewport.Right - itemFrame.Right, 0);
}
var centerViewport = Center(viewport);
var centerItem = Center(itemFrame);
return new CGPoint(centerViewport.X - centerItem.X, 0);
}
if (snapPointsAlignment == SnapPointsAlignment.Start)
{
return new CGPoint(0, viewport.Top - itemFrame.Top);
}
if (snapPointsAlignment == SnapPointsAlignment.End)
{
return new CGPoint(0, viewport.Bottom - itemFrame.Bottom);
}
var centerViewport1 = Center(viewport);
var centerItem1 = Center(itemFrame);
return new CGPoint(0, centerViewport1.Y - centerItem1.Y);
}
static bool IsAtLeastHalfVisible(UICollectionViewLayoutAttributes item, CGRect viewport)
{
var itemFrame = item.Frame;
var visibleArea = Area(CGRect.Intersect(itemFrame, viewport));
return visibleArea >= Area(itemFrame) / 2;
}
static UICollectionViewLayoutAttributes Nearer(UICollectionViewLayoutAttributes a,
UICollectionViewLayoutAttributes b,
CGPoint target)
{
var dA = DistanceSquared(a.Frame, target);
var dB = DistanceSquared(b.Frame, target);
if (dA < dB)
{
return a;
}
return b;
}
public static UICollectionViewLayoutAttributes FindNextItem(UICollectionViewLayoutAttributes[] items,
UICollectionViewScrollDirection direction, int step, CGPoint scrollingVelocity, int currentIndex)
{
var velocity = direction == UICollectionViewScrollDirection.Horizontal
? scrollingVelocity.X
: scrollingVelocity.Y;
if (velocity == 0)
{
// The user isn't scrolling at all, just stay where we are
return items[currentIndex];
}
// Move the index up or down by increment, depending on the velocity
if (velocity > 0)
{
currentIndex = currentIndex + step;
}
else if (velocity < 0)
{
currentIndex = currentIndex - step;
}
// Make sure we're not out of bounds
currentIndex = Clamp(currentIndex, 0, items.Length - 1);
return items[currentIndex];
}
}
}

Просмотреть файл

@ -0,0 +1,237 @@
using System;
using CoreGraphics;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public class StructuredItemsViewController<TItemsView> : ItemsViewController<TItemsView>
where TItemsView : StructuredItemsView
{
public const int HeaderTag = 111;
public const int FooterTag = 222;
bool _disposed;
UIView _headerUIView;
VisualElement _headerViewFormsElement;
UIView _footerUIView;
VisualElement _footerViewFormsElement;
public StructuredItemsViewController(TItemsView structuredItemsView, ItemsViewLayout layout)
: base(structuredItemsView, layout)
{
}
protected override void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
if (_headerViewFormsElement != null)
{
_headerViewFormsElement.MeasureInvalidated -= OnFormsElementMeasureInvalidated;
}
if (_footerViewFormsElement != null)
{
_footerViewFormsElement.MeasureInvalidated -= OnFormsElementMeasureInvalidated;
}
_headerUIView = null;
_headerViewFormsElement = null;
_footerUIView = null;
_footerViewFormsElement = null;
}
base.Dispose(disposing);
}
protected override bool IsHorizontal => (ItemsView?.ItemsLayout as ItemsLayout)?.Orientation == ItemsLayoutOrientation.Horizontal;
protected override CGRect DetermineEmptyViewFrame()
{
nfloat headerHeight = 0;
var headerView = CollectionView.ViewWithTag(HeaderTag);
if (headerView != null)
headerHeight = headerView.Frame.Height;
nfloat footerHeight = 0;
var footerView = CollectionView.ViewWithTag(FooterTag);
if (footerView != null)
footerHeight = footerView.Frame.Height;
return new CGRect(CollectionView.Frame.X, CollectionView.Frame.Y, CollectionView.Frame.Width,
Math.Abs(CollectionView.Frame.Height - (headerHeight + footerHeight)));
}
public override void ViewWillLayoutSubviews()
{
base.ViewWillLayoutSubviews();
// This update is only relevant if you have a footer view because it's used to place the footer view
// based on the ContentSize so we just update the positions if the ContentSize has changed
if (_footerUIView != null)
{
var emptyView = CollectionView.ViewWithTag(EmptyTag);
if (IsHorizontal)
{
if (_footerUIView.Frame.X != ItemsViewLayout.CollectionViewContentSize.Width ||
_footerUIView.Frame.X < emptyView?.Frame.X)
UpdateHeaderFooterPosition();
}
else
{
if (_footerUIView.Frame.Y != ItemsViewLayout.CollectionViewContentSize.Height ||
_footerUIView.Frame.Y < (emptyView?.Frame.Y + emptyView?.Frame.Height))
UpdateHeaderFooterPosition();
}
}
}
internal void UpdateFooterView()
{
UpdateSubview(ItemsView?.Footer, ItemsView?.FooterTemplate, FooterTag,
ref _footerUIView, ref _footerViewFormsElement);
UpdateHeaderFooterPosition();
}
internal void UpdateHeaderView()
{
UpdateSubview(ItemsView?.Header, ItemsView?.HeaderTemplate, HeaderTag,
ref _headerUIView, ref _headerViewFormsElement);
UpdateHeaderFooterPosition();
}
internal void UpdateSubview(object view, DataTemplate viewTemplate, nint viewTag, ref UIView uiView, ref VisualElement formsElement)
{
uiView?.RemoveFromSuperview();
if (formsElement != null)
{
ItemsView.RemoveLogicalChild(formsElement);
formsElement.MeasureInvalidated -= OnFormsElementMeasureInvalidated;
}
UpdateView(view, viewTemplate, ref uiView, ref formsElement);
if (uiView != null)
{
uiView.Tag = viewTag;
CollectionView.AddSubview(uiView);
}
if (formsElement != null)
ItemsView.AddLogicalChild(formsElement);
if (formsElement != null)
{
RemeasureLayout(formsElement);
formsElement.MeasureInvalidated += OnFormsElementMeasureInvalidated;
}
else if (uiView != null)
{
uiView.SizeToFit();
}
}
void UpdateHeaderFooterPosition()
{
var emptyView = CollectionView.ViewWithTag(EmptyTag);
if (IsHorizontal)
{
var currentInset = CollectionView.ContentInset;
nfloat headerWidth = _headerUIView?.Frame.Width ?? 0f;
nfloat footerWidth = _footerUIView?.Frame.Width ?? 0f;
nfloat emptyWidth = emptyView?.Frame.Width ?? 0f;
if (_headerUIView != null && _headerUIView.Frame.X != headerWidth)
_headerUIView.Frame = new CoreGraphics.CGRect(-headerWidth, 0, headerWidth, CollectionView.Frame.Height);
if (_footerUIView != null && (_footerUIView.Frame.X != ItemsViewLayout.CollectionViewContentSize.Width || emptyWidth > 0))
_footerUIView.Frame = new CoreGraphics.CGRect(ItemsViewLayout.CollectionViewContentSize.Width + emptyWidth, 0, footerWidth, CollectionView.Frame.Height);
if (CollectionView.ContentInset.Left != headerWidth || CollectionView.ContentInset.Right != footerWidth)
{
var currentOffset = CollectionView.ContentOffset;
CollectionView.ContentInset = new UIEdgeInsets(0, headerWidth, 0, footerWidth);
var xOffset = currentOffset.X + (currentInset.Left - CollectionView.ContentInset.Left);
if (CollectionView.ContentSize.Width + headerWidth <= CollectionView.Bounds.Width)
xOffset = -headerWidth;
// if the header grows it will scroll off the screen because if you change the content inset iOS adjusts the content offset so the list doesn't move
// this changes the offset of the list by however much the header size has changed
CollectionView.ContentOffset = new CoreGraphics.CGPoint(xOffset, CollectionView.ContentOffset.Y);
}
}
else
{
var currentInset = CollectionView.ContentInset;
nfloat headerHeight = _headerUIView?.Frame.Height ?? 0f;
nfloat footerHeight = _footerUIView?.Frame.Height ?? 0f;
nfloat emptyHeight = emptyView?.Frame.Height ?? 0f;
if (CollectionView.ContentInset.Top != headerHeight || CollectionView.ContentInset.Bottom != footerHeight)
{
var currentOffset = CollectionView.ContentOffset;
CollectionView.ContentInset = new UIEdgeInsets(headerHeight, 0, footerHeight, 0);
// if the header grows it will scroll off the screen because if you change the content inset iOS adjusts the content offset so the list doesn't move
// this changes the offset of the list by however much the header size has changed
var yOffset = currentOffset.Y + (currentInset.Top - CollectionView.ContentInset.Top);
if (CollectionView.ContentSize.Height + headerHeight <= CollectionView.Bounds.Height)
yOffset = -headerHeight;
CollectionView.ContentOffset = new CoreGraphics.CGPoint(CollectionView.ContentOffset.X, yOffset);
}
if (_headerUIView != null && _headerUIView.Frame.Y != headerHeight)
{
_headerUIView.Frame = new CoreGraphics.CGRect(0, -headerHeight, CollectionView.Frame.Width, headerHeight);
}
nfloat height = 0;
if (IsViewLoaded && View.Window != null)
{
height = ItemsViewLayout.CollectionViewContentSize.Height;
}
if (_footerUIView != null && (_footerUIView.Frame.Y != height || emptyHeight > 0))
{
_footerUIView.Frame = new CoreGraphics.CGRect(0, height + emptyHeight, CollectionView.Frame.Width, footerHeight);
}
}
}
protected override void HandleFormsElementMeasureInvalidated(VisualElement formsElement)
{
base.HandleFormsElementMeasureInvalidated(formsElement);
UpdateHeaderFooterPosition();
}
internal void UpdateLayoutMeasurements()
{
if (_headerViewFormsElement != null)
HandleFormsElementMeasureInvalidated(_headerViewFormsElement);
if (_footerViewFormsElement != null)
HandleFormsElementMeasureInvalidated(_footerViewFormsElement);
}
}
}

Просмотреть файл

@ -0,0 +1,65 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Controls.Internals;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal static class TemplateHelpers
{
public static INativeViewHandler GetHandler(View view, IMauiContext context)
{
if (view == null)
{
throw new ArgumentNullException(nameof(view));
}
var handler = view.Handler;
if(handler == null)
handler = view.ToHandler(context);
(handler.NativeView as UIView).Frame = view.Bounds.ToCGRect();
return (INativeViewHandler)handler;
}
public static (UIView NativeView, VisualElement FormsElement) RealizeView(object view, DataTemplate viewTemplate, ItemsView itemsView)
{
if (viewTemplate != null)
{
// Run this through the extension method in case it's really a DataTemplateSelector
viewTemplate = viewTemplate.SelectDataTemplate(view, itemsView);
// We have a template; turn it into a Forms view
var templateElement = viewTemplate.CreateContent() as View;
// Make sure the Visual property is available when the renderer is created
PropertyPropagationExtensions.PropagatePropertyChanged(null, templateElement, itemsView);
var renderer = GetHandler(templateElement, itemsView.FindMauiContext());
var element = renderer.VirtualView as VisualElement;
// and set the view as its BindingContext
element.BindingContext = view;
return ((UIView)renderer.NativeView, element);
}
if (view is View formsView)
{
// Make sure the Visual property is available when the renderer is created
PropertyPropagationExtensions.PropagatePropertyChanged(null, formsView, itemsView);
// No template, and the EmptyView is a Forms view; use that
var renderer = GetHandler(formsView, itemsView.FindMauiContext());
var element = renderer.VirtualView as VisualElement;
return ((UIView)renderer.NativeView, element);
}
return (new UILabel { TextAlignment = UITextAlignment.Center, Text = $"{view}" }, null);
}
}
}

Просмотреть файл

@ -0,0 +1,303 @@
using System;
using CoreGraphics;
using Foundation;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Graphics;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
public abstract class TemplatedCell : ItemsViewCell
{
public event EventHandler<EventArgs> ContentSizeChanged;
public event EventHandler<LayoutAttributesChangedEventArgs> LayoutAttributesChanged;
protected CGSize ConstrainedSize;
protected nfloat ConstrainedDimension;
public DataTemplate CurrentTemplate { get; private set; }
// Keep track of the cell size so we can verify whether a measure invalidation
// actually changed the size of the cell
Size _size;
internal CGSize CurrentSize => _size.ToCGSize();
[Export("initWithFrame:")]
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
protected TemplatedCell(CGRect frame) : base(frame)
{
}
internal INativeViewHandler NativeHandler { get; private set; }
public override void ConstrainTo(CGSize constraint)
{
ClearConstraints();
ConstrainedSize = constraint;
}
public override void ConstrainTo(nfloat constant)
{
ClearConstraints();
ConstrainedDimension = constant;
}
protected void ClearConstraints()
{
ConstrainedSize = default;
ConstrainedDimension = default;
}
public override UICollectionViewLayoutAttributes PreferredLayoutAttributesFittingAttributes(
UICollectionViewLayoutAttributes layoutAttributes)
{
var preferredAttributes = base.PreferredLayoutAttributesFittingAttributes(layoutAttributes);
var preferredSize = preferredAttributes.Frame.Size;
if (SizesAreSame(preferredSize, _size)
&& AttributesConsistentWithConstrainedDimension(preferredAttributes))
{
return preferredAttributes;
}
var size = UpdateCellSize();
// Adjust the preferred attributes to include space for the Forms element
preferredAttributes.Frame = new CGRect(preferredAttributes.Frame.Location, size);
OnLayoutAttributesChanged(preferredAttributes);
//_isMeasured = true;
return preferredAttributes;
}
CGSize UpdateCellSize()
{
// Measure this cell (including the Forms element) if there is no constrained size
var size = ConstrainedSize == default ? Measure() : ConstrainedSize;
// Update the size of the root view to accommodate the Forms element
var nativeView = NativeHandler.NativeView;
nativeView.Frame = new CGRect(CGPoint.Empty, size);
// Layout the Maui element
var nativeBounds = nativeView.Frame.ToRectangle();
NativeHandler.VirtualView.Arrange(nativeBounds);
_size = nativeBounds.Size;
return size;
}
public void Bind(DataTemplate template, object bindingContext, ItemsView itemsView)
{
var oldElement = NativeHandler?.VirtualView as View;
// Run this through the extension method in case it's really a DataTemplateSelector
var itemTemplate = template.SelectDataTemplate(bindingContext, itemsView);
if (itemTemplate != CurrentTemplate)
{
// Remove the old view, if it exists
if (oldElement != null)
{
oldElement.MeasureInvalidated -= MeasureInvalidated;
oldElement.BindingContext = null;
itemsView.RemoveLogicalChild(oldElement);
ClearSubviews();
_size = Size.Zero;
}
// Create the content and renderer for the view
var view = itemTemplate.CreateContent() as View;
// Set the binding context _before_ we create the renderer; that way, it's available during OnElementChanged
view.BindingContext = bindingContext;
var renderer = TemplateHelpers.GetHandler(view, itemsView.FindMauiContext());
SetRenderer(renderer);
// And make the new Element a "child" of the ItemsView
// We deliberately do this _after_ setting the binding context for the new element;
// if we do it before, the element briefly inherits the ItemsView's bindingcontext and we
// emit a bunch of needless binding errors
itemsView.AddLogicalChild(view);
// Prevents the use of default color when there are VisualStateManager with Selected state setting the background color
// First we check whether the cell has the default selected background color; if it does, then we should check
// to see if the cell content is the VSM to set a selected color
if (SelectedBackgroundView.BackgroundColor == ColorExtensions.Gray && IsUsingVSMForSelectionColor(view))
{
SelectedBackgroundView = new UIView
{
BackgroundColor = UIColor.Clear
};
}
}
else
{
// Same template
if (oldElement != null)
{
if (oldElement.BindingContext == null || !(oldElement.BindingContext.Equals(bindingContext)))
{
// If the data is different, update it
// Unhook the MeasureInvalidated handler, otherwise it'll fire for every invalidation during the
// BindingContext change
oldElement.MeasureInvalidated -= MeasureInvalidated;
oldElement.BindingContext = bindingContext;
oldElement.MeasureInvalidated += MeasureInvalidated;
UpdateCellSize();
}
}
}
CurrentTemplate = itemTemplate;
}
void SetRenderer(INativeViewHandler renderer)
{
NativeHandler = renderer;
var nativeView = NativeHandler.NativeView;
// Clear out any old views if this cell is being reused
ClearSubviews();
InitializeContentConstraints(nativeView);
(renderer.VirtualView as View).MeasureInvalidated += MeasureInvalidated;
}
protected void Layout(CGSize constraints)
{
var nativeView = NativeHandler.NativeView;
var width = constraints.Width;
var height = constraints.Height;
NativeHandler.VirtualView.Measure(width, height);
nativeView.Frame = new CGRect(0, 0, width, height);
var rectangle = nativeView.Frame.ToRectangle();
NativeHandler.VirtualView.Arrange(rectangle);
_size = rectangle.Size;
}
void ClearSubviews()
{
for (int n = ContentView.Subviews.Length - 1; n >= 0; n--)
{
ContentView.Subviews[n].RemoveFromSuperview();
}
}
internal void UseContent(TemplatedCell measurementCell)
{
// Copy all the content and values from the measurement cell
ConstrainedDimension = measurementCell.ConstrainedDimension;
ConstrainedSize = measurementCell.ConstrainedSize;
CurrentTemplate = measurementCell.CurrentTemplate;
_size = measurementCell._size;
SetRenderer(measurementCell.NativeHandler);
}
bool IsUsingVSMForSelectionColor(View view)
{
var groups = VisualStateManager.GetVisualStateGroups(view);
for (var groupIndex = 0; groupIndex < groups.Count; groupIndex++)
{
var group = groups[groupIndex];
for (var stateIndex = 0; stateIndex < group.States.Count; stateIndex++)
{
var state = group.States[stateIndex];
if (state.Name != VisualStateManager.CommonStates.Selected)
{
continue;
}
for (var setterIndex = 0; setterIndex < state.Setters.Count; setterIndex++)
{
var setter = state.Setters[setterIndex];
if (setter.Property.PropertyName == VisualElement.BackgroundColorProperty.PropertyName)
{
return true;
}
}
}
}
return false;
}
public override bool Selected
{
get => base.Selected;
set
{
base.Selected = value;
var element = NativeHandler?.VirtualView as VisualElement;
if (element != null)
{
VisualStateManager.GoToState(element, value
? VisualStateManager.CommonStates.Selected
: VisualStateManager.CommonStates.Normal);
}
}
}
protected abstract (bool, Size) NeedsContentSizeUpdate(Size currentSize);
void MeasureInvalidated(object sender, EventArgs args)
{
var (needsUpdate, toSize) = NeedsContentSizeUpdate(_size);
if (!needsUpdate)
{
return;
}
// Cache the size for next time
_size = toSize;
// Let the controller know that things need to be laid out again
OnContentSizeChanged();
}
protected void OnContentSizeChanged()
{
ContentSizeChanged?.Invoke(this, EventArgs.Empty);
}
protected void OnLayoutAttributesChanged(UICollectionViewLayoutAttributes newAttributes)
{
LayoutAttributesChanged?.Invoke(this, new LayoutAttributesChangedEventArgs(newAttributes));
}
protected abstract bool AttributesConsistentWithConstrainedDimension(UICollectionViewLayoutAttributes attributes);
bool SizesAreSame(CGSize preferredSize, Size elementSize)
{
const double tolerance = 0.000001;
if (Math.Abs(preferredSize.Height - elementSize.Height) > tolerance)
{
return false;
}
if (Math.Abs(preferredSize.Width - elementSize.Width) > tolerance)
{
return false;
}
return true;
}
}
}

Просмотреть файл

@ -0,0 +1,29 @@
using CoreGraphics;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal sealed class VerticalCell : WidthConstrainedTemplatedCell
{
public static NSString ReuseId = new NSString("Microsoft.Maui.Controls.Compatibility.Platform.iOS.VerticalCell");
[Export("initWithFrame:")]
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
public VerticalCell(CGRect frame) : base(frame)
{
}
public override CGSize Measure()
{
var measure = NativeHandler.VirtualView.Measure(ConstrainedDimension, double.PositiveInfinity);
return new CGSize(ConstrainedDimension, measure.Height);
}
protected override bool AttributesConsistentWithConstrainedDimension(UICollectionViewLayoutAttributes attributes)
{
return attributes.Frame.Width == ConstrainedDimension;
}
}
}

Просмотреть файл

@ -0,0 +1,30 @@
using CoreGraphics;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal sealed class VerticalDefaultCell : DefaultCell
{
public static NSString ReuseId = new NSString("Microsoft.Maui.Controls.Compatibility.Platform.iOS.VerticalDefaultCell");
[Export("initWithFrame:")]
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
public VerticalDefaultCell(CGRect frame) : base(frame)
{
Constraint = Label.WidthAnchor.ConstraintEqualTo(Frame.Width);
Constraint.Priority = (float)UILayoutPriority.DefaultHigh;
Constraint.Active = true;
}
public override void ConstrainTo(CGSize constraint)
{
Constraint.Constant = constraint.Width;
}
public override CGSize Measure()
{
return new CGSize(Constraint.Constant, Label.IntrinsicContentSize.Height);
}
}
}

Просмотреть файл

@ -0,0 +1,32 @@
using CoreGraphics;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal sealed class VerticalDefaultSupplementalView : DefaultCell
{
public static NSString ReuseId = new NSString("Microsoft.Maui.Controls.Compatibility.Platform.iOS.VerticalDefaultSupplementalView");
[Export("initWithFrame:")]
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
public VerticalDefaultSupplementalView(CGRect frame) : base(frame)
{
Label.Font = UIFont.PreferredHeadline;
Constraint = Label.WidthAnchor.ConstraintEqualTo(Frame.Width);
Constraint.Priority = (float)UILayoutPriority.DefaultHigh;
Constraint.Active = true;
}
public override void ConstrainTo(CGSize constraint)
{
Constraint.Constant = constraint.Width;
}
public override CGSize Measure()
{
return new CGSize(Constraint.Constant, Label.IntrinsicContentSize.Height);
}
}
}

Просмотреть файл

@ -0,0 +1,37 @@
using CoreGraphics;
using Foundation;
using UIKit;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal sealed class VerticalSupplementaryView : WidthConstrainedTemplatedCell
{
public static NSString ReuseId = new NSString("Microsoft.Maui.Controls.Compatibility.Platform.iOS.VerticalSupplementaryView");
[Export("initWithFrame:")]
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
public VerticalSupplementaryView(CGRect frame) : base(frame)
{
}
public override CGSize Measure()
{
if (NativeHandler?.VirtualView == null)
{
return CGSize.Empty;
}
var measure = NativeHandler.VirtualView.Measure(ConstrainedDimension, double.PositiveInfinity);
var height = NativeHandler.VirtualView.Height > 0
? NativeHandler.VirtualView.Height : measure.Height;
return new CGSize(ConstrainedDimension, height);
}
protected override bool AttributesConsistentWithConstrainedDimension(UICollectionViewLayoutAttributes attributes)
{
return attributes.Frame.Width == ConstrainedDimension;
}
}
}

Просмотреть файл

@ -0,0 +1,48 @@
using CoreGraphics;
using Foundation;
using Microsoft.Maui.Graphics;
namespace Microsoft.Maui.Controls.Handlers.Items
{
internal abstract class WidthConstrainedTemplatedCell : TemplatedCell
{
[Export("initWithFrame:")]
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
public WidthConstrainedTemplatedCell(CGRect frame) : base(frame)
{
}
public override void ConstrainTo(CGSize constraint)
{
ClearConstraints();
ConstrainedDimension = constraint.Width;
}
protected override (bool, Size) NeedsContentSizeUpdate(Size currentSize)
{
var size = Size.Zero;
if (NativeHandler?.VirtualView == null)
{
return (false, size);
}
var bounds = NativeHandler.VirtualView.Frame;
if (bounds.Width <= 0 || bounds.Height <= 0)
{
return (false, size);
}
var desiredBounds = NativeHandler.VirtualView.Measure(bounds.Width, double.PositiveInfinity);
if (desiredBounds.Height == currentSize.Height)
{
// Nothing in the cell needs more room, so leave it as it is
return (false, size);
}
return (true, desiredBounds);
}
}
}

Просмотреть файл

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Microsoft.Maui.Controls.Handlers;
using Microsoft.Maui.Controls.Handlers.Items;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Hosting;
@ -10,6 +11,9 @@ namespace Microsoft.Maui.Controls.Hosting
{
static readonly Dictionary<Type, Type> DefaultMauiControlHandlers = new Dictionary<Type, Type>
{
#if __IOS__
{ typeof(CollectionView), typeof(CollectionViewHandler) },
#endif
#if WINDOWS || __ANDROID__
{ typeof(Shell), typeof(ShellHandler) },
#endif

Просмотреть файл

@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Graphics;
using UIKit;
namespace Microsoft.Maui.Controls
{
public partial class ColorExtensions
{
internal static readonly UIColor Black = UIColor.Black;
internal static readonly UIColor SeventyPercentGrey = new UIColor(0.7f, 0.7f, 0.7f, 1);
internal static UIColor LabelColor
{
get
{
if (NativeVersion.IsAtLeast(13))
return UIColor.LabelColor;
return UIColor.Black;
}
}
internal static UIColor PlaceholderColor
{
get
{
if (NativeVersion.IsAtLeast(13))
return UIColor.PlaceholderTextColor;
return SeventyPercentGrey;
}
}
internal static UIColor SecondaryLabelColor
{
get
{
if (NativeVersion.IsAtLeast(13))
return UIColor.SecondaryLabelColor;
return new Color(.32f, .4f, .57f).ToNative();
}
}
internal static UIColor BackgroundColor
{
get
{
if (NativeVersion.IsAtLeast(13))
return UIColor.SystemBackgroundColor;
return UIColor.White;
}
}
internal static UIColor SeparatorColor
{
get
{
if (NativeVersion.IsAtLeast(13))
return UIColor.SeparatorColor;
return UIColor.Gray;
}
}
internal static UIColor OpaqueSeparatorColor
{
get
{
if (NativeVersion.IsAtLeast(13))
return UIColor.OpaqueSeparatorColor;
return UIColor.Black;
}
}
internal static UIColor GroupedBackground
{
get
{
if (NativeVersion.IsAtLeast(13))
return UIColor.SystemGroupedBackgroundColor;
return new UIColor(247f / 255f, 247f / 255f, 247f / 255f, 1);
}
}
internal static UIColor AccentColor
{
get
{
if (NativeVersion.IsAtLeast(13))
return UIColor.SystemBlueColor;
return Color.FromRgba(50, 79, 133, 255).ToNative();
}
}
internal static UIColor Red
{
get
{
if (NativeVersion.IsAtLeast(13))
return UIColor.SystemRedColor;
return UIColor.FromRGBA(255, 0, 0, 255);
}
}
internal static UIColor Gray
{
get
{
if (NativeVersion.IsAtLeast(13))
return UIColor.SystemGrayColor;
return UIColor.Gray;
}
}
internal static UIColor LightGray
{
get
{
if (NativeVersion.IsAtLeast(13))
return UIColor.SystemGray2Color;
return UIColor.LightGray;
}
}
}
}

Просмотреть файл

@ -0,0 +1,17 @@
using UIKit;
namespace Microsoft.Maui
{
public static class CollectionViewExtensions
{
public static void UpdateVerticalScrollBarVisibility(this UICollectionView collectionView, ScrollBarVisibility scrollBarVisibility)
{
collectionView.ShowsVerticalScrollIndicator = scrollBarVisibility == ScrollBarVisibility.Always || scrollBarVisibility == ScrollBarVisibility.Default;
}
public static void UpdateHorizontalScrollBarVisibility(this UICollectionView collectionView, ScrollBarVisibility scrollBarVisibility)
{
collectionView.ShowsHorizontalScrollIndicator = scrollBarVisibility == ScrollBarVisibility.Always || scrollBarVisibility == ScrollBarVisibility.Default;
}
}
}

Просмотреть файл

@ -16,6 +16,18 @@ namespace Microsoft.Maui
}
public static UIView ToNative(this IElement view, IMauiContext context)
{
var handler = view.ToHandler(context);
if (handler.NativeView is not UIView result)
{
throw new InvalidOperationException($"Unable to convert {view} to {typeof(UIView)}");
}
return result;
}
public static INativeViewHandler ToHandler(this IElement view, IMauiContext context)
{
_ = view ?? throw new ArgumentNullException(nameof(view));
_ = context ?? throw new ArgumentNullException(nameof(context));
@ -43,7 +55,7 @@ namespace Microsoft.Maui
throw new InvalidOperationException($"Unable to convert {view} to {typeof(UIView)}");
}
return result;
return (INativeViewHandler)handler;
}
public static void SetApplicationHandler(this UIApplicationDelegate nativeApplication, IApplication application, IMauiContext context) =>