using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using Xamarin.Forms; namespace Billing.UI { public class GroupStackLayout : Layout { public static readonly BindableProperty GroupHeaderTemplateProperty = BindableProperty.Create(nameof(GroupHeaderTemplate), typeof(DataTemplate), typeof(GroupStackLayout)); public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(GroupStackLayout)); public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IList), typeof(GroupStackLayout), propertyChanged: OnItemsSourcePropertyChanged); public static readonly BindableProperty SpacingProperty = BindableProperty.Create(nameof(Spacing), typeof(double), typeof(GroupStackLayout), defaultValue: 4d); public DataTemplate GroupHeaderTemplate { get => (DataTemplate)GetValue(GroupHeaderTemplateProperty); set => SetValue(GroupHeaderTemplateProperty, value); } public DataTemplate ItemTemplate { get => (DataTemplate)GetValue(ItemTemplateProperty); set => SetValue(ItemTemplateProperty, value); } public IList ItemsSource { get => (IList)GetValue(ItemsSourceProperty); set => SetValue(ItemsSourceProperty, value); } public double Spacing { get => (double)GetValue(SpacingProperty); set => SetValue(SpacingProperty, value); } private static void OnItemsSourcePropertyChanged(BindableObject obj, object old, object @new) { var stack = (GroupStackLayout)obj; stack.lastWidth = -1; if (@new == null) { stack.cachedLayout.Clear(); stack.Children.Clear(); stack.InvalidateLayout(); } else if (@new is IList list) { stack.freezed = true; stack.cachedLayout.Clear(); stack.Children.Clear(); var groupTemplate = stack.GroupHeaderTemplate; var itemTemplate = stack.ItemTemplate; for (var i = 0; i < list.Count; i++) { var item = list[i]; if (item is IList sublist) { if (groupTemplate != null) { var child = groupTemplate.CreateContent(); if (child is View view) { view.BindingContext = item; stack.Children.Add(view); } } foreach (var it in sublist) { var child = itemTemplate.CreateContent(); if (child is View view) { view.BindingContext = it; stack.Children.Add(view); } } } else { var child = itemTemplate.CreateContent(); if (child is View view) { view.BindingContext = list[i]; stack.Children.Add(view); } } } stack.freezed = false; stack.UpdateChildrenLayout(); stack.InvalidateLayout(); } } private readonly Dictionary cachedLayout = new(); private bool freezed; private double lastWidth = -1; private SizeRequest lastSizeRequest; public void Refresh(IList list) { OnItemsSourcePropertyChanged(this, null, list); } protected override void LayoutChildren(double x, double y, double width, double height) { if (freezed) { return; } var source = ItemsSource; if (source == null || source.Count <= 0) { return; } var spacing = Spacing; var lastHeight = 0.0; foreach (var item in Children) { var measured = item.Measure(width, height, MeasureFlags.IncludeMargins); var rect = new Rectangle( 0, lastHeight, width, 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); } lastHeight += rect.Height + spacing; } } protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint) { if (lastWidth == widthConstraint) { return lastSizeRequest; } lastWidth = widthConstraint; var spacing = Spacing; var lastHeight = 0.0; foreach (var item in Children) { var measured = item.Measure(widthConstraint, heightConstraint, MeasureFlags.IncludeMargins); lastHeight += measured.Request.Height + spacing; } lastSizeRequest = new SizeRequest(new Size(widthConstraint, lastHeight)); return lastSizeRequest; } } }