diff --git a/src/Controls/src/Core/Handlers/Items2/CarouselViewHandler2.iOS.cs b/src/Controls/src/Core/Handlers/Items2/CarouselViewHandler2.iOS.cs index 5094df2e6ae6..71051a2bb6dd 100644 --- a/src/Controls/src/Core/Handlers/Items2/CarouselViewHandler2.iOS.cs +++ b/src/Controls/src/Core/Handlers/Items2/CarouselViewHandler2.iOS.cs @@ -40,16 +40,121 @@ protected override CarouselViewController2 CreateController(CarouselView newElem protected override UICollectionViewLayout SelectLayout() { bool isHorizontal = VirtualView.ItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal; - var peekInsets = VirtualView.PeekAreaInsets; - var weakItemsView = new WeakReference(ItemsView); - var weakController = new WeakReference((CarouselViewController2)Controller); + NSCollectionLayoutDimension itemWidth = NSCollectionLayoutDimension.CreateFractionalWidth(1); + NSCollectionLayoutDimension itemHeight = NSCollectionLayoutDimension.CreateFractionalHeight(1); + NSCollectionLayoutDimension groupWidth = NSCollectionLayoutDimension.CreateFractionalWidth(1); + NSCollectionLayoutDimension groupHeight = NSCollectionLayoutDimension.CreateFractionalHeight(1); + nfloat itemSpacing = 0; + NSCollectionLayoutGroup group = null; - return LayoutFactory2.CreateCarouselLayout( - isHorizontal, - peekInsets, - weakItemsView, - weakController); + var layout = new UICollectionViewCompositionalLayout((sectionIndex, environment) => + { + if (VirtualView is null) + { + return null; + } + 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); + + if (OperatingSystem.IsIOSVersionAtLeast(16)) + { + group = isHorizontal ? NSCollectionLayoutGroup.GetHorizontalGroup(groupSize, item, 1) : + NSCollectionLayoutGroup.GetVerticalGroup(groupSize, item, 1); + } + else + { + group = isHorizontal ? NSCollectionLayoutGroup.CreateHorizontal(groupSize, item, 1) : + NSCollectionLayoutGroup.CreateVertical(groupSize, item, 1); + } + + // Create our section layout + var section = NSCollectionLayoutSection.Create(group: group); + section.InterGroupSpacing = itemSpacing; + section.OrthogonalScrollingBehavior = isHorizontal ? UICollectionLayoutSectionOrthogonalScrollingBehavior.GroupPagingCentered : UICollectionLayoutSectionOrthogonalScrollingBehavior.None; + section.VisibleItemsInvalidationHandler = (items, offset, env) => + { + //This will allow us to SetPosition when we are scrolling the items + //based on the current page + var page = (offset.X + sectionMargin) / env.Container.ContentSize.Width; + + // Check if we not are at the beginning or end of the page and if we have items + if (Math.Abs(page % 1) > (double.Epsilon * 100) || Controller.ItemsSource.ItemCount <= 0) + { + return; + } + + var pageIndex = (int)page; + var carouselPosition = pageIndex; + + var cv2Controller = (CarouselViewController2)Controller; + + //If we are looping, we need to get the correct position + if (ItemsView.Loop) + { + var maxIndex = (Controller.ItemsSource as 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 of our ItemSource, we need to subtract one + carouselPosition = pageIndex - 1; + + if (ItemsView.Position != carouselPosition) + { + //If we are updating the ItemsSource, we don't want to scroll the CollectionView + if (cv2Controller.IsUpdating()) + { + return; + } + + var goToIndexPath = cv2Controller.GetScrollToIndexPath(carouselPosition); + + //This will move the carousel to fake the loop + Controller.CollectionView.ScrollToItem(NSIndexPath.FromItemSection(pageIndex, 0), UICollectionViewScrollPosition.Left, false); + + } + } + + //Update the CarouselView position + cv2Controller?.SetPosition(carouselPosition); + + }; + return section; + }); + + return layout; } protected override void ScrollToRequested(object sender, ScrollToRequestEventArgs args) diff --git a/src/Controls/src/Core/Handlers/Items2/iOS/LayoutFactory2.cs b/src/Controls/src/Core/Handlers/Items2/iOS/LayoutFactory2.cs index 66bc21786d5d..8dbc33e96e1c 100644 --- a/src/Controls/src/Core/Handlers/Items2/iOS/LayoutFactory2.cs +++ b/src/Controls/src/Core/Handlers/Items2/iOS/LayoutFactory2.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Runtime.InteropServices; using CoreGraphics; -using Foundation; -using Microsoft.Maui.Controls.Handlers.Items; using UIKit; namespace Microsoft.Maui.Controls.Handlers.Items2; @@ -263,136 +261,6 @@ public static UICollectionViewLayout CreateHorizontalGrid(GridItemsLayout gridIt gridItemsLayout.Span); -#nullable disable - public static UICollectionViewLayout CreateCarouselLayout( - bool isHorizontal, - Thickness peekAreaInsets, - WeakReference weakItemsView, - WeakReference weakController) - { - NSCollectionLayoutDimension itemWidth = NSCollectionLayoutDimension.CreateFractionalWidth(1); - NSCollectionLayoutDimension itemHeight = NSCollectionLayoutDimension.CreateFractionalHeight(1); - NSCollectionLayoutDimension groupWidth = NSCollectionLayoutDimension.CreateFractionalWidth(1); - NSCollectionLayoutDimension groupHeight = NSCollectionLayoutDimension.CreateFractionalHeight(1); - nfloat itemSpacing = 0; - NSCollectionLayoutGroup group = null; - - var layout = new UICollectionViewCompositionalLayout((sectionIndex, environment) => - { - if (!weakItemsView.TryGetTarget(out var itemsView) || !weakController.TryGetTarget(out var controller)) - { - return null; - } - - double sectionMargin = 0.0; - - if (!isHorizontal) - { - sectionMargin = peekAreaInsets.VerticalThickness / 2; - var newGroupHeight = environment.Container.ContentSize.Height - peekAreaInsets.VerticalThickness; - groupHeight = NSCollectionLayoutDimension.CreateAbsolute((nfloat)newGroupHeight); - groupWidth = NSCollectionLayoutDimension.CreateFractionalWidth(1); - } - else - { - sectionMargin = peekAreaInsets.HorizontalThickness / 2; - var newGroupWidth = environment.Container.ContentSize.Width - 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); - - if (OperatingSystem.IsIOSVersionAtLeast(16)) - { - group = isHorizontal - ? NSCollectionLayoutGroup.GetHorizontalGroup(groupSize, item, 1) - : NSCollectionLayoutGroup.GetVerticalGroup(groupSize, item, 1); - } - else - { - group = isHorizontal - ? NSCollectionLayoutGroup.CreateHorizontal(groupSize, item, 1) - : NSCollectionLayoutGroup.CreateVertical(groupSize, item, 1); - } - - var section = NSCollectionLayoutSection.Create(group: group); - section.InterGroupSpacing = itemSpacing; - section.OrthogonalScrollingBehavior = isHorizontal - ? UICollectionLayoutSectionOrthogonalScrollingBehavior.GroupPagingCentered - : UICollectionLayoutSectionOrthogonalScrollingBehavior.None; - - section.VisibleItemsInvalidationHandler = (items, offset, env) => - { - if (!weakItemsView.TryGetTarget(out var itemsView) || !weakController.TryGetTarget(out var cv2Controller)) - { - return; - } - - var page = (offset.X + sectionMargin) / env.Container.ContentSize.Width; - - if (Math.Abs(page % 1) > (double.Epsilon * 100) || cv2Controller.ItemsSource.ItemCount <= 0) - { - return; - } - - var pageIndex = (int)page; - var carouselPosition = pageIndex; - - if (itemsView.Loop && cv2Controller.ItemsSource is ILoopItemsViewSource loopSource) - { - var maxIndex = loopSource.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 of our ItemSource, we need to subtract one - carouselPosition = pageIndex - 1; - - if (itemsView.Position != carouselPosition) - { - //If we are updating the ItemsSource, we don't want to scroll the CollectionView - if (cv2Controller.IsUpdating()) - { - return; - } - - var goToIndexPath = cv2Controller.GetScrollToIndexPath(carouselPosition); - - //This will move the carousel to fake the loop - cv2Controller.CollectionView.ScrollToItem( - NSIndexPath.FromItemSection(pageIndex, 0), - UICollectionViewScrollPosition.Left, - false); - } - } - - //Update the CarouselView position - cv2Controller?.SetPosition(carouselPosition); - }; - - return section; - }); - - return layout; - } -#nullable enable class CustomUICollectionViewCompositionalLayout : UICollectionViewCompositionalLayout { LayoutSnapInfo _snapInfo; diff --git a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs index a013bc74d4ac..9c6a714aa6b7 100644 --- a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs +++ b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs @@ -11,9 +11,6 @@ using Microsoft.Maui.Controls.Platform; using Microsoft.Maui.Controls.Shapes; using Microsoft.Maui.DeviceTests.Stubs; -#if IOS || MACCATALYST -using Microsoft.Maui.Controls.Handlers.Items2; -#endif using Microsoft.Maui.Handlers; using Microsoft.Maui.Hosting; using Xunit; @@ -23,11 +20,6 @@ namespace Microsoft.Maui.DeviceTests.Memory; [Category(TestCategory.Memory)] public class MemoryTests : ControlsHandlerTestBase { - // Subclasses used to enable memory tests for CV2 handlers - public class CollectionView2 : CollectionView { } - public class CarouselView2 : CarouselView { } - - void SetupBuilder() { EnsureHandlerCreated(builder => @@ -39,10 +31,6 @@ void SetupBuilder() handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); -#if IOS || MACCATALYST - handlers.AddHandler(); - handlers.AddHandler(); -#endif handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); @@ -182,10 +170,6 @@ await CreateHandlerAndAddToWindow(new Window(navPage), async () => [InlineData(typeof(TableView))] //[InlineData(typeof(WebView))] - This test was moved to MemoryTests.cs inside Appium [InlineData(typeof(CollectionView))] -#if IOS || MACCATALYST - //[InlineData(typeof(CollectionView2))] - Fails, Check https://github.com/dotnet/maui/issues/29619 - [InlineData(typeof(CarouselView2))] -#endif public async Task HandlerDoesNotLeak(Type type) { SetupBuilder(); @@ -193,7 +177,7 @@ public async Task HandlerDoesNotLeak(Type type) #if ANDROID // NOTE: skip certain controls on older Android devices if ((type == typeof(DatePicker) || type == typeof(ListView)) && !OperatingSystem.IsAndroidVersionAtLeast(30)) - return; + return; if (type == typeof(HybridWebView) && !OperatingSystem.IsAndroidVersionAtLeast(24)) { @@ -275,12 +259,8 @@ await InvokeOnMainThreadAsync(async () => await AssertionExtensions.WaitForGC(viewReference, handlerReference, platformViewReference); } - [Theory("CollectionView Header/Footer Doesn't Leak")] - [InlineData(typeof(CollectionView))] -#if IOS || MACCATALYST - //[InlineData(typeof(CollectionView2))] Fails, Check https://github.com/dotnet/maui/issues/29619 -#endif - public async Task CollectionViewHeaderFooterDoesntLeak(Type type) + [Fact("CollectionView Header/Footer Doesn't Leak")] + public async Task CollectionViewHeaderFooterDoesntLeak() { SetupBuilder(); @@ -293,19 +273,20 @@ public async Task CollectionViewHeaderFooterDoesntLeak(Type type) await CreateHandlerAndAddToWindow(new Window(navPage), async () => { - var cv = (CollectionView)Activator.CreateInstance(type); - cv.Footer = new VerticalStackLayout(); - cv.Header = new VerticalStackLayout(); - cv.ItemTemplate = new DataTemplate(() => + var cv = new CollectionView { - var view = new Label + Footer = new VerticalStackLayout(), + Header = new VerticalStackLayout(), + ItemTemplate = new DataTemplate(() => { - }; - view.SetBinding(Label.TextProperty, "."); - return view; - }); - cv.ItemsSource = observable; - + var view = new Label + { + }; + view.SetBinding(Label.TextProperty, "."); + return view; + }), + ItemsSource = observable + }; viewReference = new WeakReference(cv); handlerReference = new WeakReference(cv.Handler); @@ -317,20 +298,10 @@ await navPage.Navigation.PushAsync(new ContentPage }); -#if IOS || MACCATALYST - var cv1handler = cv.Handler as CollectionViewHandler; - var cv2handler = cv.Handler as CollectionViewHandler2; - - if (cv1handler is not null) - { - controllerReference = new WeakReference(cv1handler.Controller); - } - else if (cv2handler is not null) - { - controllerReference = new WeakReference(cv2handler.Controller); - } - cv1handler = null; - cv2handler = null; +#if IOS + var controller = (cv.Handler as CollectionViewHandler).Controller; + controllerReference = new WeakReference(controller); + controller = null; #else controllerReference = new WeakReference(new object()); #endif