using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Xamarin.Forms; namespace Gallery.Resources.UI { public class FlowLayout : AbsoluteLayout { public static readonly BindableProperty ColumnProperty = BindableProperty.Create(nameof(Column), typeof(int), typeof(FlowLayout), defaultValue: 2, propertyChanged: OnColumnPropertyChanged); public static readonly BindableProperty RowSpacingProperty = BindableProperty.Create(nameof(RowSpacing), typeof(double), typeof(FlowLayout), defaultValue: 10.0); public static readonly BindableProperty ColumnSpacingProperty = BindableProperty.Create(nameof(ColumnSpacing), typeof(double), typeof(FlowLayout), defaultValue: 10.0); private static void OnColumnPropertyChanged(BindableObject obj, object old, object @new) { var flowLayout = (FlowLayout)obj; if (old is int column && column != flowLayout.Column) { flowLayout.UpdateChildrenLayout(); //flowLayout.InvalidateLayout(); } } public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(FlowLayout)); public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IList), typeof(FlowLayout), propertyChanged: OnItemsSourcePropertyChanged); private static void OnItemsSourcePropertyChanged(BindableObject obj, object old, object @new) { var flowLayout = (FlowLayout)obj; if (old is ICollectionChanged oldNotify) { oldNotify.CollectionChanged -= flowLayout.OnCollectionChanged; } flowLayout.lastWidth = -1; if (@new == null) { flowLayout.freezed = true; flowLayout.cachedLayout.Clear(); flowLayout.Children.Clear(); flowLayout.freezed = false; flowLayout.InvalidateLayout(); } else if (@new is IList list) { flowLayout.freezed = true; flowLayout.cachedLayout.Clear(); flowLayout.Children.Clear(); for (var i = 0; i < list.Count; i++) { var child = flowLayout.ItemTemplate.CreateContent(); if (child is View view) { view.BindingContext = list[i]; flowLayout.Children.Add(view); } } if (@new is ICollectionChanged newNotify) { newNotify.CollectionChanged += flowLayout.OnCollectionChanged; } flowLayout.freezed = false; flowLayout.UpdateChildrenLayout(); //flowLayout.InvalidateLayout(); } } public int Column { get => (int)GetValue(ColumnProperty); set => SetValue(ColumnProperty, value); } public double RowSpacing { get => (double)GetValue(RowSpacingProperty); set => SetValue(RowSpacingProperty, value); } public double ColumnSpacing { get => (double)GetValue(ColumnSpacingProperty); set => SetValue(ColumnSpacingProperty, value); } public DataTemplate ItemTemplate { get => (DataTemplate)GetValue(ItemTemplateProperty); set => SetValue(ItemTemplateProperty, value); } public IList ItemsSource { get => (IList)GetValue(ItemsSourceProperty); set => SetValue(ItemsSourceProperty, value); } public event EventHandler MaxHeightChanged; public double ColumnWidth { get; private set; } private bool freezed; private double maximumHeight; private readonly Dictionary cachedLayout = new(); private double lastWidth = -1; private SizeRequest lastSizeRequest; protected override void LayoutChildren(double x, double y, double width, double height) { if (freezed) { return; } var column = Column; if (column < 1) { return; } var source = ItemsSource; if (source == null || source.Count <= 0) { return; } var columnSpacing = ColumnSpacing; var rowSpacing = RowSpacing; var columnHeights = new double[column]; var columnSpacingTotal = columnSpacing * (column - 1); var columnWidth = (width - columnSpacingTotal) / column; ColumnWidth = columnWidth; foreach (var item in Children) { var measured = item.Measure(columnWidth, height, flags: MeasureFlags.IncludeMargins); var col = 0; for (var i = 1; i < column; i++) { if (columnHeights[i] < columnHeights[col]) { col = i; } } var rect = new Rectangle( col * (columnWidth + columnSpacing), columnHeights[col], columnWidth, measured.Request.Height); if (cachedLayout.TryGetValue(item, out var r)) { if (r != rect) { cachedLayout[item] = rect; item.Layout(rect); //SetLayoutBounds(item, rect); } } else { cachedLayout.Add(item, rect); item.Layout(rect); //SetLayoutBounds(item, rect); } columnHeights[col] += measured.Request.Height + rowSpacing; } } protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint) { var column = Column; if (column < 1) { return base.OnMeasure(widthConstraint, heightConstraint); } if (lastWidth == widthConstraint) { return lastSizeRequest; } lastWidth = widthConstraint; var columnSpacing = ColumnSpacing; var rowSpacing = RowSpacing; var columnHeights = new double[column]; var columnSpacingTotal = columnSpacing * (column - 1); var columnWidth = (widthConstraint - columnSpacingTotal) / column; ColumnWidth = columnWidth; foreach (var item in Children) { var measured = item.Measure(columnWidth, heightConstraint, flags: MeasureFlags.IncludeMargins); var col = 0; for (var i = 1; i < column; i++) { if (columnHeights[i] < columnHeights[col]) { col = i; } } columnHeights[col] += measured.Request.Height + rowSpacing; } maximumHeight = columnHeights.Max(); if (maximumHeight > 0) { MaxHeightChanged?.Invoke(this, new HeightEventArgs { ContentHeight = maximumHeight }); } lastSizeRequest = new SizeRequest(new Size(widthConstraint, maximumHeight)); return lastSizeRequest; } private void OnCollectionChanged(object sender, CollectionChangedEventArgs e) { lastWidth = -1; if (e.OldItems != null) { freezed = true; cachedLayout.Clear(); var index = e.OldStartingIndex; for (var i = index + e.OldItems.Count - 1; i >= index; i--) { Children.RemoveAt(i); } freezed = false; } if (e.NewItems == null) { UpdateChildrenLayout(); //InvalidateLayout(); return; } freezed = true; var start = e.NewStartingIndex; for (var i = 0; i < e.NewItems.Count; i++) { var child = ItemTemplate.CreateContent(); if (child is View view) { view.BindingContext = e.NewItems[i]; Children.Insert(start + i, view); } } freezed = false; UpdateChildrenLayout(); //InvalidateLayout(); } } public interface ICollectionChanged { event EventHandler CollectionChanged; } public class CollectionChangedEventArgs : EventArgs { public int OldStartingIndex { get; set; } public IList OldItems { get; set; } public int NewStartingIndex { get; set; } public IList NewItems { get; set; } } public class HeightEventArgs : EventArgs { public double ContentHeight { get; set; } } }