Skip to content

Commit

Permalink
fix(listview): [WASM] [Skia] [macOS] Improve ListView measurement
Browse files Browse the repository at this point in the history
Do item arrangement on the Arrange pass for the managed ListView implementation. This is important for consistency with WinUI in certain layout configurations.
  • Loading branch information
davidjohnoliver committed Nov 3, 2020
1 parent c100863 commit b0b2733
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 50 deletions.
15 changes: 13 additions & 2 deletions src/Uno.UI/UI/Xaml/Controls/ContentControl/ContentControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,27 @@ public partial class ContentControl : Control, IEnumerable

protected override bool CanCreateTemplateWithoutParent { get { return _canCreateTemplateWithoutParent; } }

private VirtualizationInformation? _virtualizationInformation;
internal VirtualizationInformation GetVirtualizationInformation() => _virtualizationInformation ??= new VirtualizationInformation();

/// <summary>
/// Marks this control as a container generated by, eg, a <see cref="Primitives.Selector"/>, rather than an element explicitly
/// defined in xaml.
/// </summary>
internal bool IsGeneratedContainer { get; set; }
internal bool IsGeneratedContainer
{
get => _virtualizationInformation?.IsGeneratedContainer ?? false;
set => GetVirtualizationInformation().IsGeneratedContainer = value;
}

/// <summary>
/// Marks this as a container defined in the root of an ItemTemplate, so that it can be handled appropriately when recycled.
/// </summary>
internal bool IsContainerFromItemTemplate { get; set; }
internal bool IsContainerFromTemplateRoot
{
get => _virtualizationInformation?.IsContainerFromTemplateRoot ?? false;
set => GetVirtualizationInformation().IsContainerFromTemplateRoot = value;
}

#nullable disable // Public members should stay nullable-oblivious for now to stay consistent with WinUI
public ContentControl()
Expand Down
4 changes: 2 additions & 2 deletions src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1063,7 +1063,7 @@ object GetContent()
containerAsContentControl.Style = styleFromItemsControl;
}

if (!containerAsContentControl.IsContainerFromItemTemplate)
if (!containerAsContentControl.IsContainerFromTemplateRoot)
{
containerAsContentControl.ContentTemplate = ItemTemplate;
containerAsContentControl.ContentTemplateSelector = ItemTemplateSelector;
Expand All @@ -1078,7 +1078,7 @@ object GetContent()
// the result of the fallback value of the binding set below.
containerAsContentControl.DataContext = GetContent();

if (!containerAsContentControl.IsContainerFromItemTemplate && containerAsContentControl.GetBindingExpression(ContentControl.ContentProperty) == null)
if (!containerAsContentControl.IsContainerFromTemplateRoot && containerAsContentControl.GetBindingExpression(ContentControl.ContentProperty) == null)
{
containerAsContentControl.SetBinding(ContentControl.ContentProperty, new Binding());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
using System.Collections.Generic;
using System.Text;
using Uno.UI;
using Windows.Foundation;
using Windows.UI.Xaml.Controls.Primitives;
using static System.Math;

namespace Windows.UI.Xaml.Controls
{
Expand All @@ -28,6 +30,25 @@ protected override Line CreateLine(GeneratorDirection fillDirection, double exte
}

protected override int GetItemsPerLine() => 1;

protected override Rect GetElementArrangeBounds(int elementIndex, Rect containerBounds, Size windowConstraint, Size finalSize)
{
// we will give the container what it requested if bigger than the constraint, and let the clipping occur by the scrollviewer
// we will give the container the constraint of the window (the viewport really) so that it can be laid out inside of the
// viewport. Basically this means that a bigger container does not influence the alignment of smaller elements.
// we do not use the finalsize, because that will represent the largest element in the viewport.
var breadth = Max(GetBreadth(containerBounds), GetBreadth(windowConstraint));

// unfortunate, but incorrectly configured panels (for instance, panel is set to orient horizontally, where scrollviewer is set to enable scrolling vertically
// will potentially have infinity here. Also, the listview itself might have been inside of scrollviewer that allowed infinite in this direction
breadth = Min(breadth, GetBreadth(finalSize));
SetBreadth(ref containerBounds, breadth);

// Uno TODO
// result.* SizeFromRectInNonVirtualizingDirection() -= GetGroupPaddingAtStart().* SizeInNonVirtualizingDirection() + GetGroupPaddingAtEnd().* SizeInNonVirtualizingDirection();

return containerBounds;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,23 +139,23 @@ public bool ShouldBreadthStretch
{
get
{
if (XamlParent == null)
var stretchOwner =
#if !__IOS__ && !__ANDROID__
IsInsidePopup ? (FrameworkElement)OwnerPanel :
#endif
XamlParent;
if (stretchOwner == null)
{
return true;
}

if (IsInsidePopup)
{
return false;
}

if (ScrollOrientation == Orientation.Vertical)
{
return XamlParent.HorizontalAlignment == HorizontalAlignment.Stretch;
return stretchOwner.HorizontalAlignment == HorizontalAlignment.Stretch;
}
else
{
return XamlParent.VerticalAlignment == VerticalAlignment.Stretch;
return stretchOwner.VerticalAlignment == VerticalAlignment.Stretch;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ private void OnScrollChanged(object sender, ScrollViewerViewChangedEventArgs e)
unappliedDelta = Max(0, unappliedDelta);
UpdateLayout(extentAdjustment: sign * -unappliedDelta);
}
ArrangeElements(_availableSize, ViewportSize);
UpdateCompleted();

_lastScrollOffset = ScrollOffset;
Expand All @@ -272,7 +273,7 @@ private double GetScrollConsumptionIncrement(GeneratorDirection fillDirection)
return _averageLineHeight;
}

return GetExtent(incrementView);
return GetActualExtent(incrementView);
}

internal Size MeasureOverride(Size availableSize)
Expand Down Expand Up @@ -308,7 +309,7 @@ internal Size ArrangeOverride(Size finalSize)
{
if (this.Log().IsEnabled(LogLevel.Debug))
{
this.Log().LogDebug("Measured without an ItemsControl: simply return size(0,0) for now...");
this.Log().LogDebug("Arranged without an ItemsControl: simply return size(0,0) for now...");
}

return new Size(0, 0);
Expand All @@ -321,11 +322,28 @@ internal Size ArrangeOverride(Size finalSize)
}

_availableSize = finalSize;
UpdateLayout(extentAdjustment: _scrollAdjustmentForCollectionChanges);
var adjustedVisibleWindow = ViewportSize;
ArrangeElements(finalSize, adjustedVisibleWindow);

return EstimatePanelSize(isMeasure: false);
}

private void ArrangeElements(Size finalSize, Size adjustedVisibleWindow)
{
foreach (var line in _materializedLines)
{
var indexAdjustment = -1;
foreach (var view in line.ContainerViews)
{
indexAdjustment++;

var bounds = GetBoundsForElement(view);
var arrangedBounds = GetElementArrangeBounds(line.FirstItemFlat + indexAdjustment, bounds, adjustedVisibleWindow, finalSize);
view.Arrange(arrangedBounds);
}
}
}

/// <summary>
/// Update the item container layout by removing no-longer-visible views and adding visible views.
/// </summary>
Expand Down Expand Up @@ -419,7 +437,7 @@ private void UnfillLayout(double extentAdjustment)
void UnfillBackward()
{
var firstMaterializedLine = GetFirstMaterializedLine();
while (firstMaterializedLine != null && GetEnd(firstMaterializedLine.FirstView) < ExtendedViewportStart + extentAdjustment)
while (firstMaterializedLine != null && GetMeasuredEnd(firstMaterializedLine.FirstView) < ExtendedViewportStart + extentAdjustment)
{
// Dematerialize lines that are entirely outside extended viewport
RecycleLine(firstMaterializedLine);
Expand All @@ -431,7 +449,7 @@ void UnfillBackward()
void UnfillForward()
{
var lastMaterializedLine = GetLastMaterializedLine();
while (lastMaterializedLine != null && GetStart(lastMaterializedLine.FirstView) > ExtendedViewportEnd + extentAdjustment)
while (lastMaterializedLine != null && GetMeasuredStart(lastMaterializedLine.FirstView) > ExtendedViewportEnd + extentAdjustment)
{
// Dematerialize lines that are entirely outside extended viewport
RecycleLine(lastMaterializedLine);
Expand Down Expand Up @@ -617,12 +635,11 @@ private double EstimatePanelExtent()

private void UpdateAverageLineHeight()
{
_averageLineHeight = _materializedLines.Count > 0 ? _materializedLines.Select(l => GetExtent(l.FirstView)).Average()
_averageLineHeight = _materializedLines.Count > 0 ? _materializedLines.Select(l => GetMeasuredExtent(l.FirstView)).Average()
: 0;
}

private double CalculatePanelMeasureBreadth() => ShouldMeasuredBreadthStretch ? AvailableBreadth :
_materializedLines.Select(l => GetDesiredBreadth(l.FirstView)).MaxOrDefault()
private double CalculatePanelMeasureBreadth() => _materializedLines.Select(l => GetDesiredBreadth(l.FirstView)).MaxOrDefault()
#if __WASM__
+ GetBreadth(XamlParent.ScrollViewer.ScrollBarSize)
#endif
Expand Down Expand Up @@ -796,26 +813,29 @@ protected void AddView(FrameworkElement view, GeneratorDirection fillDirection,
new Point(breadthOffset, extentOffset + extentOffsetAdjustment) :
new Point(extentOffset + extentOffsetAdjustment, breadthOffset);

var adjustedDesiredSize = ScrollOrientation == Orientation.Vertical
? new Size(
ShouldMeasuredBreadthStretch ? AvailableBreadth :
(_lastMeasuredSize != default ? GetBreadth(_lastMeasuredSize) : view.DesiredSize.Width),
view.DesiredSize.Height
)
: new Size(
view.DesiredSize.Width,
ShouldMeasuredBreadthStretch ? AvailableBreadth :
(_lastMeasuredSize != default ? GetBreadth(_lastMeasuredSize) : view.DesiredSize.Height)
);

var finalRect = new Rect(topLeft, adjustedDesiredSize);
// TODO: GetElementBounds()
var finalRect = new Rect(topLeft, view.DesiredSize);

if (this.Log().IsEnabled(LogLevel.Debug))
{
this.Log().LogDebug($"{GetMethodTag()} finalRect={finalRect} AvailableBreadth={AvailableBreadth} adjustedDesiredSize={adjustedDesiredSize} DC={view.DataContext}");
this.Log().LogDebug($"{GetMethodTag()} finalRect={finalRect} AvailableBreadth={AvailableBreadth} DesiredSize={view.DesiredSize} DC={view.DataContext}");
}

view.Arrange(finalRect);
SetBounds(view, finalRect);
}

protected abstract Rect GetElementArrangeBounds(/*TODO ElementType, */int elementIndex, Rect containerBounds, Size windowConstraint, Size finalSize);

private void SetBounds(FrameworkElement view, Rect bounds)
{
if (view is ContentControl container)
{
container.GetVirtualizationInformation().Bounds = bounds;
}
else if (this.Log().IsEnabled(LogLevel.Warning))
{
this.Log().LogWarning($"Non-ContentControl containers aren't supported for virtualizing panel types.");
}
}

private Line? GetFirstMaterializedLine() => _materializedLines.Count > 0 ? _materializedLines[0] : null;
Expand All @@ -831,7 +851,7 @@ protected void AddView(FrameworkElement view, GeneratorDirection fillDirection,
var firstView = GetFirstMaterializedLine()?.FirstView;
if (firstView != null)
{
return GetStart(firstView);
return GetMeasuredStart(firstView);
}

return null;
Expand All @@ -842,7 +862,7 @@ protected void AddView(FrameworkElement view, GeneratorDirection fillDirection,
var lastView = GetLastMaterializedLine()?.LastView;
if (lastView != null)
{
return GetEnd(lastView);
return GetMeasuredEnd(lastView);
}

// This will be null except immediately after ScrapLayout(), when it will be the previous start of materialized items
Expand All @@ -853,37 +873,82 @@ protected void AddView(FrameworkElement view, GeneratorDirection fillDirection,

private double GetContentEnd() => GetItemsEnd() ?? 0;

private double GetStart(FrameworkElement child)
private double GetMeasuredStart(FrameworkElement child)
{
var offset = GetRelativePosition(child);
var bounds = GetBoundsForElement(child);

return ScrollOrientation == Orientation.Vertical ?
offset.Y - child.Margin.Top :
offset.X - child.Margin.Left;
bounds.Top :
bounds.Left;
}

private double GetEnd(FrameworkElement child)
private double GetMeasuredEnd(FrameworkElement child)
{
var offset = GetRelativePosition(child);
var bounds = GetBoundsForElement(child);

return ScrollOrientation == Orientation.Vertical ?
offset.Y + child.ActualHeight + child.Margin.Bottom :
offset.X + child.ActualWidth + child.Margin.Right;
bounds.Bottom :
bounds.Right;
}

private double GetExtent(FrameworkElement child)
private double GetMeasuredExtent(FrameworkElement child)
{
var bounds = GetBoundsForElement(child);

return ScrollOrientation == Orientation.Vertical ?
child.ActualHeight + child.Margin.Top + child.Margin.Bottom :
child.ActualWidth + child.Margin.Left + child.Margin.Right;
bounds.Height :
bounds.Width;

}

private double GetActualExtent(FrameworkElement child)
{
return ScrollOrientation == Orientation.Vertical ?
child.ActualHeight :
child.ActualWidth;
}

private Rect GetBoundsForElement(FrameworkElement child)
{
if (!(child is ContentControl container))
{
if (this.Log().IsEnabled(LogLevel.Warning))
{
this.Log().LogWarning($"Non-ContentControl containers aren't supported for virtualizing panel types.");
}

return default;
}

return container.GetVirtualizationInformation().Bounds;

}

private double GetExtent(Size size) => ScrollOrientation == Orientation.Vertical ?
size.Height :
size.Width;

private double GetBreadth(Size size) => ScrollOrientation == Orientation.Vertical ?
protected double GetBreadth(Size size) => ScrollOrientation == Orientation.Vertical ?
size.Width :
size.Height;

protected double GetBreadth(Rect rect) => ScrollOrientation == Orientation.Vertical ?
rect.Width :
rect.Height;

protected void SetBreadth(ref Rect rect, double breadth)
{
if (ScrollOrientation == Orientation.Vertical)
{
rect.Width = breadth;
}
else
{
rect.Height = breadth;
}
}


private double GetActualBreadth(FrameworkElement view) => ScrollOrientation == Orientation.Vertical ?
view.ActualWidth :
view.ActualHeight;
Expand Down
2 changes: 1 addition & 1 deletion src/Uno.UI/UI/Xaml/Controls/Primitives/Selector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ private protected override DependencyObject GetRootOfItemTemplateAsContainer(Dat
// The container has been created from a template and can be recycled, so we mark it as generated
contentControl.IsGeneratedContainer = true;

contentControl.IsContainerFromItemTemplate = true;
contentControl.IsContainerFromTemplateRoot = true;
}

return templateRoot as DependencyObject;
Expand Down
27 changes: 27 additions & 0 deletions src/Uno.UI/UI/Xaml/VirtualizationInformation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Text;
using Windows.Foundation;

namespace Windows.UI.Xaml
{
/// <summary>
/// Class that holds virtualization information. We do not create this class indiscriminately, but only
// for containers that happen to be virtualizing.
/// </summary>
internal sealed class VirtualizationInformation
{
/// <summary>
/// Marks this as a container generated by, eg, a <see cref="Controls.Primitives.Selector"/>, rather than an element explicitly
/// defined in xaml.
/// </summary>
public bool IsGeneratedContainer { get; set; }

/// <summary>
/// Marks this as a container defined in the root of an ItemTemplate, so that it can be handled appropriately when recycled.
/// </summary>
public bool IsContainerFromTemplateRoot { get; set; }

public Rect Bounds { get; set; }
}
}

0 comments on commit b0b2733

Please sign in to comment.