Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS/MacCatalyst] CollectionViewHandler2 with compositional layout API's #23928

Merged
merged 7 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/Controls/samples/Controls.Sample/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using Microsoft.Maui.Foldable;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.LifecycleEvents;

#if COMPATIBILITY_ENABLED
using Microsoft.Maui.Controls.Compatibility.Hosting;
#endif
Expand All @@ -39,6 +40,8 @@ public static class MauiProgram
{
static bool UseMauiGraphicsSkia = false;

static bool UseCollectionView2 = true;

enum PageType { Main, Blazor, Shell, Template, FlyoutPage, TabbedPage }
readonly static PageType _pageType = PageType.Main;

Expand All @@ -58,6 +61,18 @@ public static MauiApp CreateMauiApp()
appBuilder.UseMauiApp<XamlApp>();
var services = appBuilder.Services;

if (UseCollectionView2)
{
#if IOS || MACCATALYST

appBuilder.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler<Microsoft.Maui.Controls.CollectionView, Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2>();
handlers.AddHandler<Microsoft.Maui.Controls.CarouselView, Microsoft.Maui.Controls.Handlers.Items2.CarouselViewHandler2>();
});
#endif
}

if (UseMauiGraphicsSkia)
{
/*
Expand Down
169 changes: 169 additions & 0 deletions src/Controls/src/Core/Handlers/Items2/CarouselViewHandler2.iOS.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#nullable disable
using System;
using Foundation;
using Microsoft.Maui.Graphics;
using UIKit;

namespace Microsoft.Maui.Controls.Handlers.Items2
{
public partial class CarouselViewHandler2 : ItemsViewHandler2<CarouselView>
{
protected override CarouselViewController2 CreateController(CarouselView newElement, UICollectionViewLayout layout)
=> new(newElement, layout);

protected override UICollectionViewLayout SelectLayout()
{
bool IsHorizontal = VirtualView.ItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal;
UICollectionViewScrollDirection scrollDirection = IsHorizontal ? UICollectionViewScrollDirection.Horizontal : UICollectionViewScrollDirection.Vertical;

NSCollectionLayoutDimension itemWidth = NSCollectionLayoutDimension.CreateFractionalWidth(1);
NSCollectionLayoutDimension itemHeight = NSCollectionLayoutDimension.CreateFractionalHeight(1);
NSCollectionLayoutDimension groupWidth = NSCollectionLayoutDimension.CreateFractionalWidth(1);
NSCollectionLayoutDimension groupHeight = NSCollectionLayoutDimension.CreateFractionalHeight(1);
nfloat itemSpacing = 0;

var layout = new UICollectionViewCompositionalLayout((sectionIndex, environment) =>
{
double sectionMargin = 0.0;
if (!IsHorizontal)
{
sectionMargin = VirtualView.PeekAreaInsets.VerticalThickness / 2;
var newGroupHeight = environment.Container.ContentSize.Height - VirtualView.PeekAreaInsets.VerticalThickness;
groupHeight = NSCollectionLayoutDimension.CreateAbsolute((nfloat)newGroupHeight);
groupWidth = NSCollectionLayoutDimension.CreateFractionalWidth(1);
}
else
{
sectionMargin = VirtualView.PeekAreaInsets.HorizontalThickness / 2;
var newGroupWidth = environment.Container.ContentSize.Width - VirtualView.PeekAreaInsets.HorizontalThickness;
groupWidth = NSCollectionLayoutDimension.CreateAbsolute((nfloat)newGroupWidth);
groupHeight = NSCollectionLayoutDimension.CreateFractionalHeight(1);
}

// Each item has a size
var itemSize = NSCollectionLayoutSize.Create(itemWidth, itemHeight);
// Create the item itself from the size
var item = NSCollectionLayoutItem.Create(layoutSize: itemSize);

//item.ContentInsets = new NSDirectionalEdgeInsets(0, itemInset, 0, 0);

var groupSize = NSCollectionLayoutSize.Create(groupWidth, groupHeight);

var group = IsHorizontal ? NSCollectionLayoutGroup.GetHorizontalGroup(groupSize, item, 1) :
NSCollectionLayoutGroup.GetVerticalGroup(groupSize, item, 1);

int currentPosition = 0;

// Create our section layout
var section = NSCollectionLayoutSection.Create(group: group);
section.InterGroupSpacing = itemSpacing;
section.OrthogonalScrollingBehavior = UICollectionLayoutSectionOrthogonalScrollingBehavior.GroupPagingCentered;
section.VisibleItemsInvalidationHandler = (items, offset, env) =>
{
var page = (offset.X + sectionMargin) / env.Container.ContentSize.Width;

// Check if we are at the beginning or end of the page
if (Math.Abs(page % 1) <= (double.Epsilon * 100) && Controller.ItemsSource.ItemCount > 0)
{
var pageIndex = (int)page;

if (ItemsView.Loop)
{
var maxIndex = (Controller.ItemsSource as Items.ILoopItemsViewSource).LoopCount - 1;

//To mimic looping, we needed to modify the ItemSource and inserted a new item at the beginning and at the end
if (pageIndex == maxIndex)
{
//When at last item, we need to change to 2nd item, so we can scroll right or left
pageIndex = 1;
}
else if (pageIndex == 0)
{
//When at first item, need to change to one before last, so we can scroll right or left
pageIndex = maxIndex - 1;
}

//since we added one item at the beginning, we need to subtract one
var realPage = pageIndex - 1;

if (currentPosition != realPage)
{
currentPosition = realPage;
var pageNumberIndex = NSIndexPath.FromRowSection(pageIndex, 0);
Controller.CollectionView.ScrollToItem(pageNumberIndex, UICollectionViewScrollPosition.Left, false);

//Update the CarouselView position
(Controller as CarouselViewController2)?.SetPosition(realPage);
}
}
else
{
(Controller as CarouselViewController2)?.SetPosition((int)page);
}
}
};

return section;
});

return layout;
}

protected override void ScrollToRequested(object sender, ScrollToRequestEventArgs args)
{
// if (VirtualView?.Loop == true)
// {
// var goToIndexPath = (Controller as CarouselViewController2).GetScrollToIndexPath(args.Index);

// if (!IsIndexPathValid(goToIndexPath))
// {
// return;
// }

// Controller.CollectionView.ScrollToItem(goToIndexPath,
// args.ScrollToPosition.ToCollectionViewScrollPosition( UICollectionViewScrollDirection.Vertical), // TODO: Fix _layout.ScrollDirection),
// args.IsAnimated);
// }
// else
// {
base.ScrollToRequested(sender, args);
// }
}

public static void MapIsSwipeEnabled(CarouselViewHandler2 handler, CarouselView carouselView)
{
handler.Controller.CollectionView.ScrollEnabled = carouselView.IsSwipeEnabled;
}

public static void MapIsBounceEnabled(CarouselViewHandler2 handler, CarouselView carouselView)
{
handler.Controller.CollectionView.Bounces = carouselView.IsBounceEnabled;
}

public static void MapPeekAreaInsets(CarouselViewHandler2 handler, CarouselView carouselView)
{
handler.UpdateLayout();
}

public static void MapCurrentItem(CarouselViewHandler2 handler, CarouselView carouselView)
{
(handler.Controller as CarouselViewController2)?.UpdateFromCurrentItem();
}

public static void MapPosition(CarouselViewHandler2 handler, CarouselView carouselView)
{
// If the initial position hasn't been set, we have a UpdateInitialPosition call on CarouselViewController2
// that will handle this so we want to skip this mapper call. We need to wait for the CollectionView to be ready
if (handler.Controller is CarouselViewController2 CarouselViewController2 && CarouselViewController2.InitialPositionSet)
{
CarouselViewController2.UpdateFromPosition();
}
}

public static void MapLoop(CarouselViewHandler2 handler, CarouselView carouselView)
{
(handler.Controller as CarouselViewController2)?.UpdateLoop();
}

}
}
154 changes: 154 additions & 0 deletions src/Controls/src/Core/Handlers/Items2/CollectionViewHandler2.iOS.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using System.Text;
using Foundation;
using Microsoft.Maui.Handlers;
using ObjCRuntime;
using UIKit;

namespace Microsoft.Maui.Controls.Handlers.Items2
{
internal class LayoutGroupingInfo
{
public bool IsGrouped { get; set; }
public bool HasHeader { get; set; }
public bool HasFooter { get; set; }
}

internal class LayoutSnapInfo
{
public SnapPointsAlignment SnapAligment { get; set; }
public SnapPointsType SnapType { get; set; }
}

public partial class CollectionViewHandler2 : ItemsViewHandler2<ReorderableItemsView>
{
// Reorderable
protected override ItemsViewController2<ReorderableItemsView> CreateController(ReorderableItemsView itemsView, UICollectionViewLayout layout)
=> new ReorderableItemsViewController2<ReorderableItemsView>(itemsView, layout);

public static void MapCanReorderItems(CollectionViewHandler2 handler, ReorderableItemsView itemsView)
{
(handler.Controller as ReorderableItemsViewController2<ReorderableItemsView>)?.UpdateCanReorderItems();
}


// Groupable
protected override void ScrollToRequested(object sender, ScrollToRequestEventArgs args)
{
if (WillNeedScrollAdjustment(args))
{
if (args.IsAnimated)
{
(Controller as GroupableItemsViewController2<ReorderableItemsView>)?.SetScrollAnimationEndedCallback(() => base.ScrollToRequested(sender, args));
}
else
{
base.ScrollToRequested(sender, args);
}
}

base.ScrollToRequested(sender, args);
}

public static void MapIsGrouped(CollectionViewHandler2 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);
}


// Selectable
public static void MapItemsSource(CollectionViewHandler2 handler, SelectableItemsView itemsView)
{
ItemsViewHandler2<ReorderableItemsView>.MapItemsSource(handler, itemsView);
MapSelectedItem(handler, itemsView);
}

public static void MapSelectedItem(CollectionViewHandler2 handler, SelectableItemsView itemsView)
{
(handler.Controller as SelectableItemsViewController2<ReorderableItemsView>)?.UpdatePlatformSelection();
}

public static void MapSelectedItems(CollectionViewHandler2 handler, SelectableItemsView itemsView)
{
(handler.Controller as SelectableItemsViewController2<ReorderableItemsView>)?.UpdatePlatformSelection();
}

public static void MapSelectionMode(CollectionViewHandler2 handler, SelectableItemsView itemsView)
{
(handler.Controller as SelectableItemsViewController2<ReorderableItemsView>)?.UpdateSelectionMode();
}


// Structured
protected override UICollectionViewLayout SelectLayout()
{
var groupInfo = new LayoutGroupingInfo();

if (ItemsView is GroupableItemsView groupableItemsView && groupableItemsView.IsGrouped)
{
groupInfo.IsGrouped = groupableItemsView.IsGrouped;
groupInfo.HasHeader = groupableItemsView.GroupHeaderTemplate is not null;
groupInfo.HasFooter = groupableItemsView.GroupFooterTemplate is not null;
}

var itemSizingStrategy = ItemsView.ItemSizingStrategy;
var itemsLayout = ItemsView.ItemsLayout;

//TODO: Find a better way to do this
itemsLayout.PropertyChanged += (sender, args) =>
{
if (args.PropertyName == nameof(ItemsLayout.SnapPointsAlignment) ||
args.PropertyName == nameof(ItemsLayout.SnapPointsType) ||
args.PropertyName == nameof(GridItemsLayout.VerticalItemSpacing) ||
args.PropertyName == nameof(GridItemsLayout.HorizontalItemSpacing) ||
args.PropertyName == nameof(GridItemsLayout.Span) ||
args.PropertyName == nameof(LinearItemsLayout.ItemSpacing))

{
UpdateLayout();
}
};

if (itemsLayout is GridItemsLayout gridItemsLayout)
{
return LayoutFactory2.CreateGrid(gridItemsLayout, groupInfo);
}

if (itemsLayout is LinearItemsLayout listItemsLayout)
{
return LayoutFactory2.CreateList(listItemsLayout, groupInfo);
}

// Fall back to vertical list
return LayoutFactory2.CreateList(new LinearItemsLayout(ItemsLayoutOrientation.Vertical), groupInfo);
}

public static void MapHeaderTemplate(CollectionViewHandler2 handler, StructuredItemsView itemsView)
{
(handler.Controller as StructuredItemsViewController2<ReorderableItemsView>)?.UpdateHeaderView();
}

public static void MapFooterTemplate(CollectionViewHandler2 handler, StructuredItemsView itemsView)
{
(handler.Controller as StructuredItemsViewController2<ReorderableItemsView>)?.UpdateFooterView();
}

public static void MapItemsLayout(CollectionViewHandler2 handler, StructuredItemsView itemsView)
{
handler.UpdateLayout();
}

public static void MapItemSizingStrategy(CollectionViewHandler2 handler, StructuredItemsView itemsView)
{
handler.UpdateLayout();
}
}
}
Loading
Loading