From f9f463d594d74273892c5f0c9b0b75995aaa1d8c Mon Sep 17 00:00:00 2001 From: David Date: Wed, 26 May 2021 14:58:35 -0400 Subject: [PATCH] fix(calendar): Fix scrolling issues on iOS (inc. allow ChangeView on SV when not yet arranged) --- ...CalendarPanel.ModernCollectionBasePanel.cs | 20 ++++++++++-- .../NativeScrollContentPresenter.iOS.cs | 13 +++++++- .../Controls/ScrollViewer/ScrollViewer.iOS.cs | 32 ++++++++++++++++--- 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel.ModernCollectionBasePanel.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel.ModernCollectionBasePanel.cs index a0fdb5aa2d2d..ec82802aca3a 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel.ModernCollectionBasePanel.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel.ModernCollectionBasePanel.cs @@ -480,6 +480,13 @@ private Size base_MeasureOverride(Size availableSize) } var viewport = GetLayoutViewport(availableSize); + if (Rows > 0 && Cols > 0) + { + // Uno: This SetViewportSize should be done in the CalendarPanel_Partial.ArrangeOverride of the Panel(not the 'base_'), + // but (due to invalid layouting event sequence in uno?) it would cause a second layout pass. + // Invoking it here makes sure that the ItemSize is valid for this measure pass. + _layoutStrategy.SetViewportSize(viewport.Size, out _); + } _layoutStrategy.BeginMeasure(); #if __ANDROID__ // TODO: IOS @@ -488,6 +495,7 @@ private Size base_MeasureOverride(Size availableSize) ShouldInterceptInvalidate = true; #endif var index = -1; + var bottom = 0.0; try { // Gets the index of the first element to render and the actual viewport to use @@ -530,7 +538,9 @@ private Size base_MeasureOverride(Size availableSize) var itemSize = _layoutStrategy.GetElementMeasureSize(ElementType.ItemContainer, index, renderWindow); // Note: It's actually the same for all items var itemBounds = _layoutStrategy.GetElementBounds(ElementType.ItemContainer, index + StartIndex, itemSize, layout, renderWindow); - if (itemSize.Width < _minCellSize.Width && itemSize.Height < _minCellSize.Height || Cols == 0 || Rows == 0) + bottom = itemBounds.Bottom; + + if ((itemSize.Width < _minCellSize.Width && itemSize.Height < _minCellSize.Height) || Cols == 0 || Rows == 0) { // We don't have any valid cell size yet (This measure pass has been caused by DetermineTheBiggestItemSize), // so we stop right after having inserted the first child in the Children collection. @@ -610,6 +620,12 @@ private Size base_MeasureOverride(Size availableSize) default /* not used by CalendarLayoutStrategyImpl */, out var desiredSize); + // When the StartIndex is greater than 0, the desiredSize might ignore the last line. + if (desiredSize.Height < bottom) + { + desiredSize.Height = bottom; + } + return desiredSize; } @@ -646,7 +662,7 @@ private static void OnEffectiveViewportChanged(FrameworkElement sender, Effectiv { that._effectiveViewport = args.EffectiveViewport; - if (that._host is null || that._layoutStrategy is null) + if (that._host is null || that._layoutStrategy is null || that.Cols == 0 || that.Rows == 0) { return; } diff --git a/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/NativeScrollContentPresenter.iOS.cs b/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/NativeScrollContentPresenter.iOS.cs index 650d12565a74..3dfa73ad31b7 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/NativeScrollContentPresenter.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/NativeScrollContentPresenter.iOS.cs @@ -27,8 +27,19 @@ partial class NativeScrollContentPresenter : UIScrollView, DependencyObject /// private bool _isInAnimatedScroll; - internal CGPoint UpperScrollLimit => (CGPoint)(ContentSize - Frame.Size); CGPoint IUIScrollView.UpperScrollLimit => UpperScrollLimit; + internal CGPoint UpperScrollLimit + { + get + { + var extent = ContentSize; + var viewport = Frame.Size; + + return new CGPoint( + Math.Max(0, extent.Width - viewport.Width), + Math.Max(0, extent.Height - viewport.Height)); + } + } internal NativeScrollContentPresenter(ScrollViewer scroller) : this() { diff --git a/src/Uno.UI/UI/Xaml/Controls/ScrollViewer/ScrollViewer.iOS.cs b/src/Uno.UI/UI/Xaml/Controls/ScrollViewer/ScrollViewer.iOS.cs index 8d3074d9c7d6..3f05d8e7db06 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ScrollViewer/ScrollViewer.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ScrollViewer/ScrollViewer.iOS.cs @@ -57,15 +57,38 @@ private void SetScrollableContainer() } } + private (double? horizontal, double? vertical, bool disableAnimation)? _pendingChangeView; + + protected override void OnAfterArrange() + { + base.OnAfterArrange(); + + if (_pendingChangeView is {} req) + { + var success = ChangeViewNative(req.horizontal, req.vertical, null, req.disableAnimation); + if (success || !IsArrangeDirty) + { + _pendingChangeView = default; + } + } + } + private bool ChangeViewNative(double? horizontalOffset, double? verticalOffset, float? zoomFactor, bool disableAnimation) { if (_scrollableContainer != null) { // iOS doesn't limit the offset to the scrollable bounds by itself - var newOffset = new CGPoint(horizontalOffset ?? HorizontalOffset, verticalOffset ?? VerticalOffset) - .Clamp(CGPoint.Empty, _scrollableContainer.UpperScrollLimit); + var limit = _scrollableContainer.UpperScrollLimit; + var desiredOffsets = new Foundation.Point(horizontalOffset ?? HorizontalOffset, verticalOffset ?? VerticalOffset); + var clampedOffsets = new Foundation.Point(MathEx.Clamp(desiredOffsets.X, 0, limit.X), MathEx.Clamp(desiredOffsets.Y, 0, limit.Y)); + + var success = desiredOffsets == clampedOffsets; + if (!success && IsArrangeDirty) + { + _pendingChangeView = (horizontalOffset, verticalOffset, disableAnimation); + } - _scrollableContainer.SetContentOffset(newOffset, !disableAnimation); + _scrollableContainer.SetContentOffset(desiredOffsets, !disableAnimation); if(zoomFactor is { } zoom) { @@ -73,8 +96,7 @@ private bool ChangeViewNative(double? horizontalOffset, double? verticalOffset, } // Return true if successfully scrolled to asked offsets - return (horizontalOffset == null || horizontalOffset == newOffset.X) && - (verticalOffset == null || verticalOffset == newOffset.Y); + return success; } return false;