Skip to content

Commit

Permalink
feat(listview): [iOS] [Android] Support ItemsPresenter.MinHeight
Browse files Browse the repository at this point in the history
Match Windows' behavior when ItemsPresenter.MinHeight (or MinWidth for horizontal-scrolling list) is set inside of a ListView or GridView. Since the ItemsPresenter is actually outside the scrollable container (NativeListViewBase), this is implemented by mapping the MinHeight value to the native layout calculated by VirtualizingPanelLayout.

This supports scenarios where a minimal scrollable extent, regardless of number of items, is desired.

(cherry picked from commit 6dfc251)

# Conflicts:
#	src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.Measure.cs
  • Loading branch information
davidjohnoliver authored and mergify-bot committed Nov 2, 2021
1 parent 162dee5 commit 486e8eb
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,119 @@ public async Task When_Available_Breadth_Changes()
await WindowHelper.WaitForEqual(244, () => border.ActualWidth);
}

<<<<<<< HEAD
=======
[TestMethod]
public async Task When_Item_Margins_And_Scrolled()
{
var SUT = new ListView
{
HorizontalAlignment = HorizontalAlignment.Left,
Width = 250,
Height = 200,
ItemsSource = Enumerable.Range(0, 11).ToArray(),
ItemContainerStyle = ContainerMarginStyle,
ItemTemplate = FixedSizeItemTemplate,
ItemsPanel = NoCacheItemsStackPanel
};

WindowHelper.WindowContent = SUT;

await WindowHelper.WaitForLoaded(SUT);


await WindowHelper.WaitForNonNull(() => SUT.ContainerFromIndex(0));
VerifyItemHeight(0);

await ScrollDownAndBackChecked();
await ScrollDownAndBackChecked();
await ScrollDownAndBackChecked();
await ScrollDownAndBackChecked();

async Task ScrollDownAndBackChecked()
{
await ScrollByInIncrements(SUT, 1000);
await WindowHelper.WaitForNonNull(() => SUT.ContainerFromIndex(10));
VerifyItemHeight(9);
VerifyItemHeight(8);

await ScrollByInIncrements(SUT, -100);
await WindowHelper.WaitForNonNull(() => SUT.ContainerFromIndex(1));
await WindowHelper.WaitForNonNull(() => SUT.ContainerFromIndex(0));
VerifyItemHeight(0);
VerifyItemHeight(1);
VerifyItemHeight(2);
}

void VerifyItemHeight(int position)
{
var container = SUT.ContainerFromIndex(position) as ListViewItem;
Assert.IsNotNull(container);
const double ExpectedContainerHeight = 29;
const double TopAndBottomMargin = 15;
Assert.AreEqual(ExpectedContainerHeight, container.ActualHeight, 1);

var containerNext = SUT.ContainerFromIndex(position + 1) as ListViewItem;
Assert.IsNotNull(containerNext);

var containerRect = container.GetOnScreenBounds();
var containerNextRect = containerNext.GetOnScreenBounds();
Assert.AreEqual(ExpectedContainerHeight + TopAndBottomMargin, containerNextRect.Y - containerRect.Y);
}
}

[TestMethod]
public async Task When_ItemsPresenter_MinHeight()
{
var SUT = new ListView
{
ItemContainerStyle = NoSpaceContainerStyle,
ItemTemplate = FixedSizeItemTemplate,
ItemsSource = Enumerable.Range(0, 3).ToArray(),
Height = 250
};

WindowHelper.WindowContent = SUT;
var itemsPresenter = await WindowHelper.WaitForNonNull(() => SUT.FindFirstChild<ItemsPresenter>());
itemsPresenter.MinHeight = 310;

await WindowHelper.WaitForIdle();
await WindowHelper.WaitForLoaded(SUT);

Assert.AreEqual(250, SUT.ActualHeight, 1);

var container = SUT.ContainerFromIndex(2) as ListViewItem;
Assert.IsNotNull(container);
var initialRect = container.GetRelativeBounds(SUT);
const double HeightOfTwoItems = 29 * 2;
Assert.AreEqual(HeightOfTwoItems, initialRect.Y, 1);

await Task.Delay(2000);
var sv = SUT.FindFirstChild<ScrollViewer>();
ScrollBy(SUT, 40);
double InitialScroll()
{
#if NETFX_CORE
// For some reason on UWP the initial ChangeView may not work
ScrollBy(SUT, 40);
#endif
return sv.VerticalOffset;
}

await WindowHelper.WaitForEqual(40, InitialScroll);

var rectScrolledPartial = container.GetRelativeBounds(SUT);
Assert.AreEqual(HeightOfTwoItems - 40, rectScrolledPartial.Y, 1);

await ScrollByInIncrements(SUT, 200);
const double MaxPossibleScroll = 310 - 250;
await WindowHelper.WaitForEqual(MaxPossibleScroll, () => sv.VerticalOffset);

var rectScrollFinal = container.GetRelativeBounds(SUT);
Assert.AreEqual(HeightOfTwoItems - MaxPossibleScroll, rectScrollFinal.Y, 1);
}

>>>>>>> 6dfc25156 (feat(listview): [iOS] [Android] Support ItemsPresenter.MinHeight)

// Works around ScrollIntoView() not implemented for all platforms
private static void ScrollBy(ListViewBase listViewBase, double scrollBy)
Expand Down
18 changes: 15 additions & 3 deletions src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsPresenter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,21 @@ public Thickness Padding
private void OnPaddingChanged(Thickness oldValue, Thickness newValue)
{
this.InvalidateMeasure();
PropagatePadding();
PropagateLayoutValues();
}

#endregion

internal override void OnPropertyChanged2(DependencyPropertyChangedEventArgs args)
{
base.OnPropertyChanged2(args);

if (args.Property == FrameworkElement.MinHeightProperty || args.Property == FrameworkElement.MinWidthProperty)
{
PropagateLayoutValues();
}
}

/// <summary>
/// Indicates whether the ItemsPresenter is actually enclosed in the scrollable area of the
/// ItemsControl (or derived type). This is always true on Windows, but on Uno it's not the case
Expand Down Expand Up @@ -132,7 +142,7 @@ internal void SetItemsPanel(View panel)
AddChild(_itemsPanel);
#endif

PropagatePadding();
PropagateLayoutValues();
}

this.InvalidateMeasure();
Expand All @@ -152,13 +162,15 @@ private void RemoveChildViews()
#endif
}

private void PropagatePadding()
private void PropagateLayoutValues()
{
#if XAMARIN && !__MACOS__
var asListViewBase = _itemsPanel as NativeListViewBase;
if (asListViewBase != null)
{
asListViewBase.Padding = Padding;
asListViewBase.ItemsPresenterMinWidth = MinWidth;
asListViewBase.ItemsPresenterMinHeight = MinHeight;
}
#endif
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#if __IOS__ || __ANDROID__
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;

namespace Windows.UI.Xaml.Controls
{
partial class ItemsPresenter : ILayoutOptOut
{
public bool ShouldUseMinSize => !(_itemsPanel is NativeListViewBase);
}
}
#endif
9 changes: 7 additions & 2 deletions src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -530,15 +530,20 @@ protected Thickness MarginChild(View view)
var childMaxWidth = frameworkElement.MaxWidth;
var childMinHeight = frameworkElement.MinHeight;
var childMinWidth = frameworkElement.MinWidth;
if (frameworkElement is ILayoutOptOut optOutElement && !optOutElement.ShouldUseMinSize)
{
childMinHeight = 0;
childMinWidth = 0;
}
var childWidth = frameworkElement.Width;
var childHeight = frameworkElement.Height;
var childMargin = frameworkElement.Margin;
var hasChildHeight = !IsNaN(childHeight);
var hasChildWidth = !IsNaN(childWidth);
var hasChildMaxWidth = !IsInfinity(childMaxWidth) && !IsNaN(childMaxWidth);
var hasChildMaxHeight = !IsInfinity(childMaxHeight) && !IsNaN(childMaxHeight);
var hasChildMinWidth = childMinWidth > 0.0f;
var hasChildMinHeight = childMinHeight > 0.0f;
var hasChildMinWidth = childMinWidth > 0.0;
var hasChildMinHeight = childMinHeight > 0.0;

if (
childVerticalAlignment != VerticalAlignment.Stretch
Expand Down
24 changes: 24 additions & 0 deletions src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,30 @@ public float GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment a
{
return ((IScrollSnapPointsInfo)NativeLayout).GetRegularSnapPoints(orientation, alignment, out offset);
}

internal double ItemsPresenterMinWidth
{
get => NativeLayout?.ItemsPresenterMinWidth ?? double.NaN;
set
{
if (NativeLayout != null)
{
NativeLayout.ItemsPresenterMinWidth = value;
}
}
}

internal double ItemsPresenterMinHeight
{
get => NativeLayout?.ItemsPresenterMinHeight ?? double.NaN;
set
{
if (NativeLayout != null)
{
NativeLayout.ItemsPresenterMinHeight = value;
}
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,30 @@ public Thickness Padding
}
}

private double _itemsPresenterMinWidth;
internal double ItemsPresenterMinWidth
{
get => _itemsPresenterMinWidth;
set
{
_itemsPresenterMinWidth = value;
RequestLayout();
}
}

private double itemsPresenterMinHeight;
internal double ItemsPresenterMinHeight
{
get => itemsPresenterMinHeight;
set
{
itemsPresenterMinHeight = value;
RequestLayout();
}
}

private int ItemsPresenterMinExtent => (int)ViewHelper.LogicalToPhysicalPixels(ScrollOrientation == Orientation.Vertical ? ItemsPresenterMinHeight : ItemsPresenterMinWidth);

private int InitialExtentPadding => (int)ViewHelper.LogicalToPhysicalPixels(ScrollOrientation == Orientation.Vertical ? Padding.Top : Padding.Left);
private int FinalExtentPadding => (int)ViewHelper.LogicalToPhysicalPixels(ScrollOrientation == Orientation.Vertical ? Padding.Bottom : Padding.Right);
private int InitialBreadthPadding => (int)ViewHelper.LogicalToPhysicalPixels(ScrollOrientation == Orientation.Vertical ? Padding.Left : Padding.Top);
Expand Down Expand Up @@ -731,7 +755,7 @@ private int ComputeScrollRange(RecyclerView.State state, Orientation orientation
GetChildEndWithMargin(base.GetChildAt(FirstItemView + ItemViewCount - 1));
Debug.Assert(range > 0, "Must report a non-negative scroll range.");
Debug.Assert(remainingItems == 0 || range > Extent, "If any items are non-visible, the content range must be greater than the viewport extent.");
return range;
return Math.Max(range, ItemsPresenterMinExtent);
}

/// <summary>
Expand Down Expand Up @@ -1161,8 +1185,9 @@ private int ScrollByInner(int offset, RecyclerView.Recycler recycler, RecyclerVi
int maxPossibleDelta;
if (fillDirection == GeneratorDirection.Forward)
{
var contentEnd = Math.Max(GetContentEnd(), ItemsPresenterMinExtent - ContentOffset);
// If this value is negative, collection dimensions are larger than all children and we should not scroll
maxPossibleDelta = Math.Max(0, GetContentEnd() - Extent);
maxPossibleDelta = Math.Max(0, contentEnd - Extent);
// In the rare case that GetContentStart() is positive (see below), permit a positive value.
maxPossibleDelta = Math.Max(GetContentStart(), maxPossibleDelta);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,30 @@ public Thickness Padding
}
}

private double _itemsPresenterMinWidth;
internal double ItemsPresenterMinWidth
{
get => _itemsPresenterMinWidth;
set
{
_itemsPresenterMinWidth = value;
InvalidateLayout();
}
}

private double itemsPresenterMinHeight;
internal double ItemsPresenterMinHeight
{
get => itemsPresenterMinHeight;
set
{
itemsPresenterMinHeight = value;
InvalidateLayout();
}
}

private Size ItemsPresenterMinSize => new Size(ItemsPresenterMinWidth, ItemsPresenterMinHeight);

private double InitialExtentPadding => ScrollOrientation == Orientation.Vertical ? Padding.Top : Padding.Left;
private double FinalExtentPadding => ScrollOrientation == Orientation.Vertical ? Padding.Bottom : Padding.Right;
private double InitialBreadthPadding => ScrollOrientation == Orientation.Vertical ? Padding.Left : Padding.Top;
Expand Down Expand Up @@ -271,13 +295,15 @@ public override CGSize CollectionViewContentSize
{
if (ScrollOrientation == Orientation.Vertical)
{
return new CGSize(measured.Width, DynamicContentExtent);
measured = new CGSize(measured.Width, DynamicContentExtent);
}
else
{
return new CGSize(DynamicContentExtent, measured.Height);
measured = new CGSize(DynamicContentExtent, measured.Height);
}
}

measured = LayoutHelper.Max(measured, ItemsPresenterMinSize);
return measured;
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/Uno.UI/UI/Xaml/ILayoutOptOut.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace Windows.UI.Xaml
{
/// <summary>
/// Uno-specific interface that allows controls to specify that particular properties should be ignored by the shared layouting, eg for
/// compatibility when a native template is used.
/// </summary>
internal interface ILayoutOptOut
{
bool ShouldUseMinSize { get; }
}
}

0 comments on commit 486e8eb

Please sign in to comment.