Pixiview/Gallery/UI/FlowLayout.cs
2021-08-04 10:27:41 +08:00

270 lines
9.2 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Xamarin.Forms;
namespace Gallery.UI
{
public class FlowLayout : Layout<View>
{
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 event EventHandler<HeightEventArgs> MaxHeightChanged;
public double ColumnWidth { get; private set; }
private bool freezed;
private double maximumHeight;
private readonly Dictionary<View, Rectangle> cachedLayout = new Dictionary<View, Rectangle>();
protected override void LayoutChildren(double x, double y, double width, double height)
{
if (freezed)
{
return;
}
var column = Column;
if (column <= 0)
{
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, 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 v))
{
if (v != rect)
{
cachedLayout[item] = rect;
item.Layout(rect);
}
}
else
{
cachedLayout.Add(item, rect);
item.Layout(rect);
}
columnHeights[col] += measured.Request.Height + rowSpacing;
}
}
private double lastWidth = -1;
private SizeRequest lastSizeRequest;
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
var column = Column;
if (column <= 0)
{
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, 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;
}
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 IIllustCollectionChanged oldNotify)
{
oldNotify.CollectionChanged -= flowLayout.OnCollectionChanged;
}
flowLayout.lastWidth = -1;
if (newValue == null)
{
flowLayout.cachedLayout.Clear();
flowLayout.Children.Clear();
flowLayout.InvalidateLayout();
}
else if (newValue is IList newList)
{
flowLayout.freezed = true;
flowLayout.cachedLayout.Clear();
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 IIllustCollectionChanged newNotify)
{
newNotify.CollectionChanged += flowLayout.OnCollectionChanged;
}
flowLayout.freezed = false;
flowLayout.UpdateChildrenLayout();
flowLayout.InvalidateLayout();
}
}
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;
UpdateChildrenLayout();
InvalidateLayout();
}
if (e.NewItems == null)
{
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 IIllustCollectionChanged
{
event EventHandler<CollectionChangedEventArgs> 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; }
}
}