diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/UniformGridPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/UniformGridPage.xaml new file mode 100644 index 0000000000..ec58f4fb7a --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/UniformGridPage.xaml @@ -0,0 +1,40 @@ + + + + + + + + \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/UniformGridPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/UniformGridPage.xaml.cs new file mode 100644 index 0000000000..c3f1b5b18a --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/UniformGridPage.xaml.cs @@ -0,0 +1,9 @@ +namespace CommunityToolkit.Maui.Sample.Pages.Views; + +public partial class UniformGridPage : BasePage +{ + public UniformGridPage() + { + InitializeComponent(); + } +} diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/ViewsGalleryPage.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/ViewsGalleryPage.cs new file mode 100644 index 0000000000..5aac09bec0 --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/ViewsGalleryPage.cs @@ -0,0 +1,11 @@ +using System; +using CommunityToolkit.Maui.Sample.ViewModels.Views; + +namespace CommunityToolkit.Maui.Sample.Pages.Views; + +public class ViewsGalleryPage : BaseGalleryPage +{ + public ViewsGalleryPage() : base("Views") + { + } +} diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/MainViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/MainViewModel.cs index 971e1c2f32..ac484c70ea 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/MainViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/MainViewModel.cs @@ -3,6 +3,7 @@ using CommunityToolkit.Maui.Sample.Pages.Alerts; using CommunityToolkit.Maui.Sample.Pages.Behaviors; using CommunityToolkit.Maui.Sample.Pages.Converters; +using CommunityToolkit.Maui.Sample.Pages.Views; using CommunityToolkit.Maui.Sample.Pages.Extensions; using Microsoft.Maui.Graphics; @@ -15,6 +16,12 @@ protected override IEnumerable CreateItems() => new[] new SectionModel(typeof(BehaviorsGalleryPage), "Behaviors", Color.FromArgb("#8E8CD8"), "Behaviors lets you add functionality to user interface controls without having to subclass them. Behaviors are written in code and added to controls in XAML or code"), + new SectionModel(typeof(ConvertersGalleryPage), "Converters", Color.FromArgb("#EA005E"), + "Converters let you convert bindings of a certain type to a different value, based on custom logic"), + + new SectionModel(typeof(ViewsGalleryPage), "Views", Color.FromArgb("#EA005E"), + "Views"), + }; new SectionModel(typeof(ConvertersGalleryPage), "Converters", Color.FromArgb("#EA005E"), "Converters let you convert bindings of a certain type to a different value, based on custom logic"), diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs new file mode 100644 index 0000000000..38fd87e3d5 --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using CommunityToolkit.Maui.Sample.Models; +using CommunityToolkit.Maui.Sample.Pages.Views; +using CommunityToolkit.Maui.Views; + +namespace CommunityToolkit.Maui.Sample.ViewModels.Views; + +public class ViewsGalleryViewModel : BaseGalleryViewModel +{ + protected override IEnumerable CreateItems() => new[] + { + new SectionModel( + typeof(UniformGridPage), + nameof(UniformGrid), + "UniformGrid") + }; +} diff --git a/src/CommunityToolkit.Maui.UnitTests/CommunityToolkit.Maui.UnitTests.csproj b/src/CommunityToolkit.Maui.UnitTests/CommunityToolkit.Maui.UnitTests.csproj index 6a3179ad02..44f73fd6a2 100644 --- a/src/CommunityToolkit.Maui.UnitTests/CommunityToolkit.Maui.UnitTests.csproj +++ b/src/CommunityToolkit.Maui.UnitTests/CommunityToolkit.Maui.UnitTests.csproj @@ -10,6 +10,7 @@ + diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/UniformGrid_Tests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/UniformGrid_Tests.cs new file mode 100644 index 0000000000..073d1dac1a --- /dev/null +++ b/src/CommunityToolkit.Maui.UnitTests/Views/UniformGrid_Tests.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CommunityToolkit.Maui.UnitTests.Mocks; +using CommunityToolkit.Maui.Views; +using Microsoft.Maui; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; +using NSubstitute; +using Xunit; + +namespace CommunityToolkit.Maui.UnitTests.Views; + +public class UniformGridTests : BaseTest +{ + UniformGrid uniformGrid; + IView uniformChild; + const double childCount = 3; + const double childWidth = 20; + const double childHeight = 10; + public UniformGridTests() + { + uniformChild = Substitute.For(); + uniformGrid = new UniformGrid() + { + Children = + { + uniformChild, + uniformChild, + uniformChild + } + }; + } + + [Fact] + public void MeasureUniformGrid_NoWrap() + { + var expectedSize = new Size(childWidth * childCount, childHeight); + var childSize = new Size(childWidth, childHeight); + SetupChildrenSize(childSize); + var actualSize = uniformGrid.Measure(childWidth * childCount, childHeight * childCount); + Assert.Equal(expectedSize, actualSize); + } + + [Fact] + public void MeasureUniformGrid_WrapOnNextRow() + { + var expectedSize = new Size(childWidth, childHeight * childCount); + var childSize = new Size(childWidth, childHeight); + SetupChildrenSize(childSize); + var actualSize = uniformGrid.Measure(childWidth, childHeight * childCount); + Assert.Equal(expectedSize, actualSize); + } + + [Fact] + public void ArrangeChildrenUniformGrid() + { + var expectedSize = new Size(childWidth, childHeight); + SetupChildrenSize(expectedSize); + var actualSize = uniformGrid.ArrangeChildren(new Rectangle(0, 0, childWidth * childCount, childHeight * childCount)); + Assert.Equal(expectedSize, actualSize); + } + + [Fact] + public void MaxRowsArrangeChildrenUniformGrid() + { + var expectedSize = new Size(childWidth, childHeight); + SetupChildrenSize(expectedSize); + uniformGrid.MaxColumns = 1; + uniformGrid.MaxRows = 1; + var actualSize = uniformGrid.Measure(double.PositiveInfinity, double.PositiveInfinity); + Assert.Equal(expectedSize, actualSize); + } + + void SetupChildrenSize(Size size) + { + uniformChild.Measure(double.PositiveInfinity, double.PositiveInfinity).ReturnsForAnyArgs(size); + uniformGrid.Measure(childWidth * childCount, childHeight * childCount); + } +} diff --git a/src/CommunityToolkit.Maui/Views/UniformGrid.shared.cs b/src/CommunityToolkit.Maui/Views/UniformGrid.shared.cs new file mode 100644 index 0000000000..891935c9fe --- /dev/null +++ b/src/CommunityToolkit.Maui/Views/UniformGrid.shared.cs @@ -0,0 +1,117 @@ +using System; +using Microsoft.Maui; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Layouts; + +namespace CommunityToolkit.Maui.Views; + +/// +/// The UniformGrid is just like the Grid, with the possibility of multiple rows and columns, but with one important difference: +/// All rows and columns will have the same size. +/// Use this when you need the Grid behavior without the need to specify different sizes for the rows and columns. +/// +public class UniformGrid : Grid, ILayoutManager +{ + double childWidth; + + double childHeight; + + /// + /// Assign this as a LayoutManager + /// + /// + protected override ILayoutManager CreateLayoutManager() + { + return this; + } + + /// + /// Backing BindableProperty for the property. + /// + public static readonly BindableProperty MaxRowsProperty = BindableProperty.Create(nameof(MaxRows), typeof(int), typeof(UniformGrid), int.MaxValue); + + /// + /// Backing BindableProperty for the property. + /// + public static readonly BindableProperty MaxColumnsProperty = BindableProperty.Create(nameof(MaxColumns), typeof(int), typeof(UniformGrid), int.MaxValue); + + /// + /// Max rows + /// + public int MaxRows + { + get { return (int)GetValue(MaxRowsProperty); } + set { SetValue(MaxRowsProperty, value); } + } + + /// + /// Max columns + /// + public int MaxColumns + { + get { return (int)GetValue(MaxColumnsProperty); } + set { SetValue(MaxColumnsProperty, value); } + } + + /// + /// Arrange children + /// + /// Grid rectangle + /// Child size + public Size ArrangeChildren(Rectangle rectangle) + { + Measure(rectangle.Width, rectangle.Height, MeasureFlags.None); + var columns = GetColumnsCount(Children.Count, rectangle.Width, childWidth); + var rows = GetRowsCount(Children.Count, columns); + var boundsWidth = rectangle.Width / columns; + var boundsHeight = childHeight; + var bounds = new Rectangle(0, 0, boundsWidth, boundsHeight); + var count = 0; + + for (var i = 0; i < rows; i++) + { + for (var j = 0; j < columns && count < Children.Count; j++) + { + var item = Children[count]; + bounds.X = j * boundsWidth; + bounds.Y = i * boundsHeight; + item.Arrange(bounds); + count++; + } + } + + return new Size(boundsWidth, boundsHeight); + } + + /// + /// Measure grid size + /// + /// Width constraint + /// Height constraint + /// Grid size + public Size Measure(double widthConstraint, double heightConstraint) + { + foreach (var child in Children) + { + if (child.Visibility != Visibility.Visible) + continue; + + var sizeRequest = child.Measure(double.PositiveInfinity, double.PositiveInfinity); + childHeight = sizeRequest.Height; + childWidth = sizeRequest.Width; + } + + var columns = GetColumnsCount(Children.Count, widthConstraint, childWidth); + var rows = GetRowsCount(Children.Count, columns); + return new Size(columns * childWidth, rows * childHeight); + } + + int GetColumnsCount(int visibleChildrenCount, double widthConstraint, double maxChildWidth) + => Math.Min(double.IsPositiveInfinity(widthConstraint) + ? visibleChildrenCount + : Math.Min((int)(widthConstraint / maxChildWidth), visibleChildrenCount), MaxColumns); + + int GetRowsCount(int visibleChildrenCount, int columnsCount) + => Math.Min((int)Math.Ceiling((double)visibleChildrenCount / columnsCount), MaxRows); +}