From 7e3113d8a2c3936901560079f98015d2890ba274 Mon Sep 17 00:00:00 2001 From: Poker Date: Sat, 27 May 2023 00:07:16 +0800 Subject: [PATCH] WrapLayout Fix --- .../Primitives/src/WrapLayout/UvBounds.cs | 5 +- .../Primitives/src/WrapLayout/UvMeasure.cs | 48 +++++-- .../Primitives/src/WrapLayout/WrapItem.cs | 4 +- .../Primitives/src/WrapLayout/WrapLayout.cs | 126 +++++++----------- .../src/WrapLayout/WrapLayoutState.cs | 56 +++----- 5 files changed, 107 insertions(+), 132 deletions(-) diff --git a/components/Primitives/src/WrapLayout/UvBounds.cs b/components/Primitives/src/WrapLayout/UvBounds.cs index 782f605c..fd7aea7d 100644 --- a/components/Primitives/src/WrapLayout/UvBounds.cs +++ b/components/Primitives/src/WrapLayout/UvBounds.cs @@ -2,13 +2,16 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Windows.Foundation; +using Microsoft.UI.Xaml.Controls; + namespace CommunityToolkit.WinUI.Controls; internal struct UvBounds { public UvBounds(Orientation orientation, Rect rect) { - if (orientation == Orientation.Horizontal) + if (orientation is Orientation.Horizontal) { UMin = rect.Left; UMax = rect.Right; diff --git a/components/Primitives/src/WrapLayout/UvMeasure.cs b/components/Primitives/src/WrapLayout/UvMeasure.cs index 549e1113..20e68434 100644 --- a/components/Primitives/src/WrapLayout/UvMeasure.cs +++ b/components/Primitives/src/WrapLayout/UvMeasure.cs @@ -2,39 +2,61 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics; +using Windows.Foundation; +using Microsoft.UI.Xaml.Controls; + namespace CommunityToolkit.WinUI.Controls; [DebuggerDisplay("U = {U} V = {V}")] internal struct UvMeasure { - internal static readonly UvMeasure Zero = default(UvMeasure); - internal double U { get; set; } internal double V { get; set; } - public UvMeasure(Orientation orientation, double width, double height) + public UvMeasure(Orientation orientation, Size size) { - if (orientation == Orientation.Horizontal) + if (orientation is Orientation.Horizontal) { - U = width; - V = height; + U = size.Width; + V = size.Height; } else { - U = height; - V = width; + U = size.Height; + V = size.Width; } } + public Point GetPoint(Orientation orientation) + { + return orientation is Orientation.Horizontal ? new Point(U, V) : new Point(V, U); + } + + public Size GetSize(Orientation orientation) + { + return orientation is Orientation.Horizontal ? new Size(U, V) : new Size(V, U); + } + + public static bool operator ==(UvMeasure measure1, UvMeasure measure2) + { + return measure1.U == measure2.U && measure1.V == measure2.V; + } + + public static bool operator !=(UvMeasure measure1, UvMeasure measure2) + { + return !(measure1 == measure2); + } + public override bool Equals(object? obj) { - if (obj is UvMeasure measure) - { - return (measure.U == U) && (measure.V == V); - } + return obj is UvMeasure measure && this == measure; + } - return false; + public bool Equals(UvMeasure value) + { + return this == value; } public override int GetHashCode() diff --git a/components/Primitives/src/WrapLayout/WrapItem.cs b/components/Primitives/src/WrapLayout/WrapItem.cs index af311002..96bebc7e 100644 --- a/components/Primitives/src/WrapLayout/WrapItem.cs +++ b/components/Primitives/src/WrapLayout/WrapItem.cs @@ -2,13 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.UI.Xaml; + namespace CommunityToolkit.WinUI.Controls; internal class WrapItem { public WrapItem(int index) { - this.Index = index; + Index = index; } public int Index { get; } diff --git a/components/Primitives/src/WrapLayout/WrapLayout.cs b/components/Primitives/src/WrapLayout/WrapLayout.cs index a1108517..e232966a 100644 --- a/components/Primitives/src/WrapLayout/WrapLayout.cs +++ b/components/Primitives/src/WrapLayout/WrapLayout.cs @@ -2,8 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using Microsoft.UI.Xaml.Controls; using System.Collections.Specialized; +using Windows.Foundation; +using Microsoft.UI.Xaml; namespace CommunityToolkit.WinUI.Controls; @@ -20,8 +23,8 @@ public class WrapLayout : VirtualizingLayout /// public double HorizontalSpacing { - get { return (double)GetValue(HorizontalSpacingProperty); } - set { SetValue(HorizontalSpacingProperty, value); } + get => (double)GetValue(HorizontalSpacingProperty); + set => SetValue(HorizontalSpacingProperty, value); } /// @@ -40,8 +43,8 @@ public double HorizontalSpacing /// public double VerticalSpacing { - get { return (double)GetValue(VerticalSpacingProperty); } - set { SetValue(VerticalSpacingProperty, value); } + get => (double)GetValue(VerticalSpacingProperty); + set => SetValue(VerticalSpacingProperty, value); } /// @@ -61,8 +64,8 @@ public double VerticalSpacing /// public Orientation Orientation { - get { return (Orientation)GetValue(OrientationProperty); } - set { SetValue(OrientationProperty, value); } + get => (Orientation)GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); } /// @@ -87,8 +90,7 @@ private static void LayoutPropertyChanged(DependencyObject d, DependencyProperty /// protected override void InitializeForContextCore(VirtualizingLayoutContext context) { - var state = new WrapLayoutState(context); - context.LayoutState = state; + context.LayoutState = new WrapLayoutState(context); base.InitializeForContextCore(context); } @@ -110,7 +112,7 @@ protected override void OnItemsChangedCore(VirtualizingLayoutContext context, ob state.RemoveFromIndex(args.NewStartingIndex); break; case NotifyCollectionChangedAction.Move: - int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex); + var minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex); state.RemoveFromIndex(minIndex); state.RecycleElementAt(args.OldStartingIndex); @@ -134,11 +136,8 @@ protected override void OnItemsChangedCore(VirtualizingLayoutContext context, ob /// protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) { - var totalMeasure = UvMeasure.Zero; - var parentMeasure = new UvMeasure(Orientation, availableSize.Width, availableSize.Height); - var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing); - var realizationBounds = new UvBounds(Orientation, context.RealizationRect); - var position = UvMeasure.Zero; + var parentMeasure = new UvMeasure(Orientation, availableSize); + var spacingMeasure = new UvMeasure(Orientation, new Size(HorizontalSpacing, VerticalSpacing)); var state = (WrapLayoutState)context.LayoutState; if (state.Orientation != Orientation) @@ -146,38 +145,31 @@ protected override Size MeasureOverride(VirtualizingLayoutContext context, Size state.SetOrientation(Orientation); } - if (spacingMeasure.Equals(state.Spacing) == false) + if (spacingMeasure != state.Spacing || state.AvailableU != parentMeasure.U) { state.ClearPositions(); state.Spacing = spacingMeasure; - } - - if (state.AvailableU != parentMeasure.U) - { - state.ClearPositions(); state.AvailableU = parentMeasure.U; } double currentV = 0; - for (int i = 0; i < context.ItemCount; i++) + var realizationBounds = new UvBounds(Orientation, context.RealizationRect); + var position = new UvMeasure(); + for (var i = 0; i < context.ItemCount; ++i) { - bool measured = false; - WrapItem item = state.GetItemAt(i); - if (item.Measure == null) + var measured = false; + var item = state.GetItemAt(i); + if (item.Measure is null) { item.Element = context.GetOrCreateElementAt(i); item.Element.Measure(availableSize); - item.Measure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height); + item.Measure = new UvMeasure(Orientation, item.Element.DesiredSize); measured = true; } - UvMeasure currentMeasure = item.Measure.Value; - if (currentMeasure.U == 0) - { - continue; // ignore collapsed items - } + var currentMeasure = item.Measure.Value; - if (item.Position == null) + if (item.Position is null) { if (parentMeasure.U < position.U + currentMeasure.U) { @@ -192,20 +184,22 @@ protected override Size MeasureOverride(VirtualizingLayoutContext context, Size position = item.Position.Value; - double vEnd = position.V + currentMeasure.V; + var vEnd = position.V + currentMeasure.V; if (vEnd < realizationBounds.VMin) { // Item is "above" the bounds - if (item.Element != null) + if (item.Element is not null) { context.RecycleElement(item.Element); item.Element = null; } + + continue; } else if (position.V > realizationBounds.VMax) { // Item is "below" the bounds. - if (item.Element != null) + if (item.Element is not null) { context.RecycleElement(item.Element); item.Element = null; @@ -214,14 +208,14 @@ protected override Size MeasureOverride(VirtualizingLayoutContext context, Size // We don't need to measure anything below the bounds break; } - else if (measured == false) + else if (!measured) { // Always measure elements that are within the bounds item.Element = context.GetOrCreateElementAt(i); item.Element.Measure(availableSize); - currentMeasure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height); - if (currentMeasure.Equals(item.Measure) == false) + currentMeasure = new UvMeasure(Orientation, item.Element.DesiredSize); + if (currentMeasure != item.Measure) { // this item changed size; we need to recalculate layout for everything after this state.RemoveFromIndex(i + 1); @@ -249,20 +243,17 @@ protected override Size MeasureOverride(VirtualizingLayoutContext context, Size // if the last loop is (parentMeasure.U > currentMeasure.U) the currentMeasure isn't added to the total so add it here // for the last condition it is zeros so adding it will make no difference // this way is faster than an if condition in every loop for checking the last item - totalMeasure.U = parentMeasure.U; - // Propagating an infinite size causes a crash. This can happen if the parent is scrollable and infinite in the opposite // axis to the panel. Clearing to zero prevents the crash. - // This is likely an incorrect use of the control by the developer, however we need stability here so setting a default that wont crash. - if (double.IsInfinity(totalMeasure.U)) - { - totalMeasure.U = 0.0; - } + // This is likely an incorrect use of the control by the developer, however we need stability here so setting a default that won't crash. - totalMeasure.V = state.GetHeight(); + var totalMeasure = new UvMeasure + { + U = double.IsInfinity(parentMeasure.U) ? 0 : Math.Ceiling(parentMeasure.U), + V = state.GetHeight() + }; - totalMeasure.U = Math.Ceiling(totalMeasure.U); - return Orientation == Orientation.Horizontal ? new Size((float)totalMeasure.U, (float)totalMeasure.V) : new Size((float)totalMeasure.V, (float)totalMeasure.U); + return totalMeasure.GetSize(Orientation); } /// @@ -270,49 +261,25 @@ protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size { if (context.ItemCount > 0) { - var parentMeasure = new UvMeasure(Orientation, finalSize.Width, finalSize.Height); - var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing); var realizationBounds = new UvBounds(Orientation, context.RealizationRect); var state = (WrapLayoutState)context.LayoutState; - bool Arrange(WrapItem item, bool isLast = false) + bool ArrangeItem(WrapItem item) { - if (item.Measure.HasValue == false) - { - return false; - } - - if (item.Position == null) + if (item is { Measure: null } or { Position: null }) { return false; } var desiredMeasure = item.Measure.Value; - if (desiredMeasure.U == 0) - { - return true; // if an item is collapsed, avoid adding the spacing - } - UvMeasure position = item.Position.Value; + var position = item.Position.Value; - // Stretch the last item to fill the available space - if (isLast) - { - desiredMeasure.U = parentMeasure.U - position.U; - } - - if (((position.V + desiredMeasure.V) >= realizationBounds.VMin) && (position.V <= realizationBounds.VMax)) + if (realizationBounds.VMin <= position.V + desiredMeasure.V && position.V <= realizationBounds.VMax) { // place the item - UIElement child = context.GetOrCreateElementAt(item.Index); - if (Orientation == Orientation.Horizontal) - { - child.Arrange(new Rect((float)position.U, (float)position.V, (float)desiredMeasure.U, (float)desiredMeasure.V)); - } - else - { - child.Arrange(new Rect((float)position.V, (float)position.U, (float)desiredMeasure.V, (float)desiredMeasure.U)); - } + var child = context.GetOrCreateElementAt(item.Index); + child.Arrange(new Rect(position.GetPoint(Orientation), desiredMeasure.GetSize(Orientation))); } else if (position.V > realizationBounds.VMax) { @@ -322,10 +289,9 @@ bool Arrange(WrapItem item, bool isLast = false) return true; } - for (var i = 0; i < context.ItemCount; i++) + for (var i = 0; i < context.ItemCount; ++i) { - bool continueArranging = Arrange(state.GetItemAt(i)); - if (continueArranging == false) + if (!ArrangeItem(state.GetItemAt(i))) { break; } diff --git a/components/Primitives/src/WrapLayout/WrapLayoutState.cs b/components/Primitives/src/WrapLayout/WrapLayoutState.cs index 6c7d35b5..b50ff40c 100644 --- a/components/Primitives/src/WrapLayout/WrapLayoutState.cs +++ b/components/Primitives/src/WrapLayout/WrapLayoutState.cs @@ -2,18 +2,21 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.UI.Xaml.Controls; namespace CommunityToolkit.WinUI.Controls; internal class WrapLayoutState { - private List _items = new List(); - private VirtualizingLayoutContext _context; + private readonly List _items = new(); + private readonly VirtualizingLayoutContext _context; public WrapLayoutState(VirtualizingLayoutContext context) { - this._context = context; + _context = context; } public Orientation Orientation { get; private set; } @@ -35,7 +38,7 @@ internal WrapItem GetItemAt(int index) } else { - WrapItem item = new WrapItem(index); + var item = new WrapItem(index); _items.Add(item); return item; } @@ -54,7 +57,7 @@ internal void RemoveFromIndex(int index) return; } - int numToRemove = _items.Count - index; + var numToRemove = _items.Count - index; _items.RemoveRange(index, numToRemove); } @@ -62,10 +65,8 @@ internal void SetOrientation(Orientation orientation) { foreach (var item in _items.Where(i => i.Measure.HasValue)) { - UvMeasure measure = item.Measure!.Value; - double v = measure.V; - measure.V = measure.U; - measure.U = v; + var measure = item.Measure!.Value; + (measure.V, measure.U) = (measure.U, measure.V); item.Measure = measure; item.Position = null; } @@ -84,58 +85,39 @@ internal void ClearPositions() internal double GetHeight() { - if (_items.Count == 0) + if (_items.Count is 0) { return 0; } - bool calculateAverage = true; - if ((_items.Count == _context.ItemCount) && _items[_items.Count - 1].Position.HasValue) - { - calculateAverage = false; - } - UvMeasure? lastPosition = null; double maxV = 0; - int itemCount = _items.Count; - for (int i = _items.Count - 1; i >= 0; i--) + for (var i = _items.Count - 1; i >= 0; --i) { var item = _items[i]; - if (item.Position == null) + if (item.Position is null || item.Measure is null) { - itemCount--; continue; } - if (lastPosition != null) + if (lastPosition is not null && lastPosition.Value.V > item.Position.Value.V) { - if (lastPosition.Value.V > item.Position.Value.V) - { - // This is a row above the last item. Exit and calculate the average - break; - } + // This is a row above the last item. + break; } lastPosition = item.Position; - maxV = Math.Max(maxV, item.Measure!.Value.V); + maxV = Math.Max(maxV, item.Measure.Value.V); } - double totalHeight = lastPosition!.Value.V + maxV; - if (calculateAverage) - { - return (totalHeight / itemCount) * _context.ItemCount; - } - else - { - return totalHeight; - } + return lastPosition?.V + maxV ?? 0; } internal void RecycleElementAt(int index) { - UIElement element = _context.GetOrCreateElementAt(index); + var element = _context.GetOrCreateElementAt(index); _context.RecycleElement(element); } }