diff --git a/Pixiview/UI/FlowLayout.cs b/Pixiview/UI/FlowLayout.cs new file mode 100644 index 0000000..39a48a1 --- /dev/null +++ b/Pixiview/UI/FlowLayout.cs @@ -0,0 +1,194 @@ +using System.Collections; +using System.Collections.Specialized; +using System.Linq; +using Xamarin.Forms; + +namespace Pixiview.UI +{ + public class FlowLayout : Layout + { + public static readonly BindableProperty ColumnProperty = BindableProperty.Create( + nameof(Column), typeof(int), typeof(FlowLayout), 2, propertyChanged: OnColumnPropertyChanged); + public static readonly BindableProperty RowSpacingProperty = BindableProperty.Create( + nameof(RowSpacing), typeof(double), typeof(FlowLayout), 10.0); + public static readonly BindableProperty ColumnSpacingProperty = BindableProperty.Create( + nameof(ColumnSpacing), typeof(double), typeof(FlowLayout), 10.0); + + private static void OnColumnPropertyChanged(BindableObject obj, object oldValue, object newValue) + { + var flowLayout = (FlowLayout)obj; + if (oldValue is int column && column != flowLayout.Column) + { + //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 double ColumnWidth { get; private set; } + + private double maximumHeight; + + protected override void LayoutChildren(double x, double y, double width, double height) + { + var column = Column; + if (column <= 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, MeasureFlags.IncludeMargins); + var col = 0; + for (var i = 1; i < column; i++) + { + if (columnHeights[i] < columnHeights[col]) + { + col = i; + } + } + item.Layout(new Rectangle( + col * (columnWidth + columnSpacing), + columnHeights[col], + columnWidth, + measured.Request.Height)); + columnHeights[col] += measured.Request.Height + rowSpacing; + } + } + + protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint) + { + var column = Column; + if (column <= 0) + { + return base.OnMeasure(widthConstraint, heightConstraint); + } + 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, 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(); + + return new SizeRequest(new Size(widthConstraint, maximumHeight)); + } + + 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); + + public DataTemplate ItemTemplate + { + get => (DataTemplate)GetValue(ItemTemplateProperty); + set => SetValue(ItemTemplateProperty, value); + } + public IList ItemsSource + { + get => (IList)GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + + private static void OnItemsSourcePropertyChanged(BindableObject obj, object oldValue, object newValue) + { + var flowLayout = (FlowLayout)obj; + if (oldValue is INotifyCollectionChanged oldNotify) + { + oldNotify.CollectionChanged -= flowLayout.OnCollectionChanged; + } + if (newValue is IList newList) + { + flowLayout.Children.Clear(); + for (var i = 0; i < newList.Count; i++) + { + var child = flowLayout.ItemTemplate.CreateContent(); + if (child is View view) + { + view.BindingContext = newList[i]; + flowLayout.Children.Add(view); + } + } + + if (newValue is INotifyCollectionChanged newNotify) + { + newNotify.CollectionChanged += flowLayout.OnCollectionChanged; + } + + flowLayout.UpdateChildrenLayout(); + flowLayout.InvalidateLayout(); + } + } + + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.OldItems != null) + { + var index = e.OldStartingIndex; + for (var i = index + e.OldItems.Count - 1; i >= index; i--) + { + Children.RemoveAt(i); + } + UpdateChildrenLayout(); + InvalidateLayout(); + } + + if (e.NewItems == null) + { + return; + } + + 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); + } + } + + UpdateChildrenLayout(); + InvalidateLayout(); + } + } +}