diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.Measure.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.Measure.cs index 63e6d53a518f..4f5013047855 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.Measure.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.Measure.cs @@ -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.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(); + 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) diff --git a/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsPresenter.cs b/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsPresenter.cs index 4e5e54594286..9d5710180b7e 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsPresenter.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsPresenter.cs @@ -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(); + } + } + /// /// 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 @@ -132,7 +142,7 @@ internal void SetItemsPanel(View panel) AddChild(_itemsPanel); #endif - PropagatePadding(); + PropagateLayoutValues(); } this.InvalidateMeasure(); @@ -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 } diff --git a/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsPresenter.iOSAndroid.cs b/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsPresenter.iOSAndroid.cs new file mode 100644 index 000000000000..49e671b854fa --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsPresenter.iOSAndroid.cs @@ -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 diff --git a/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.cs b/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.cs index 401cf801d358..e8f6acd8d697 100644 --- a/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.cs +++ b/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.cs @@ -530,6 +530,11 @@ 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; @@ -537,8 +542,8 @@ protected Thickness MarginChild(View view) 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 diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.cs index 42dec03bd212..d00cae04e077 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.cs @@ -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; + } + } + } } } diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs index fc315a610290..6adf7754e67a 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs @@ -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); @@ -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); } /// @@ -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); } diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.iOS.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.iOS.cs index f73e66cad5a7..7237ac20772a 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.iOS.cs @@ -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; @@ -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; } } diff --git a/src/Uno.UI/UI/Xaml/ILayoutOptOut.cs b/src/Uno.UI/UI/Xaml/ILayoutOptOut.cs new file mode 100644 index 000000000000..bd372a0158ff --- /dev/null +++ b/src/Uno.UI/UI/Xaml/ILayoutOptOut.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Windows.UI.Xaml +{ + /// + /// 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. + /// + internal interface ILayoutOptOut + { + bool ShouldUseMinSize { get; } + } +}