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 e2851b261b0f..fc315a610290 100644
--- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs
+++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs
@@ -724,6 +724,8 @@ private int ComputeScrollRange(RecyclerView.State state, Orientation orientation
remainingGroupExtent = remainingGroups * lastGroup.HeaderExtent;
}
+ CorrectForEstimationErrors();
+
var range = ContentOffset + remainingItemExtent + remainingGroupExtent + headerExtent + footerExtent +
//TODO: An inline group header might actually be the view at the bottom of the viewport, we should take this into account
GetChildEndWithMargin(base.GetChildAt(FirstItemView + ItemViewCount - 1));
@@ -732,6 +734,26 @@ private int ComputeScrollRange(RecyclerView.State state, Orientation orientation
return range;
}
+ ///
+ /// Correct the scroll offset, eg if items were added/removed or had their databound heights changed while they were scrolled out
+ /// of view.
+ ///
+ private void CorrectForEstimationErrors()
+ {
+ if (ContentOffset < 0)
+ {
+ // Scroll offset should always be non-negative
+ ContentOffset = 0;
+ }
+
+ var firstVisible = GetFirstVisibleIndexPath();
+ if (firstVisible.Row == 0 && firstVisible.Section == 0)
+ {
+ // If first item is in view, we can set ContentOffset exactly
+ ContentOffset = -GetContentStart();
+ }
+ }
+
///
/// Update the internal state of the layout, as well as 'floating' views like group headers, when the scrolled offset changes.
///
@@ -742,7 +764,9 @@ private int ComputeScrollRange(RecyclerView.State state, Orientation orientation
private void ApplyOffset(int delta)
{
ContentOffset -= delta;
- Debug.Assert(ContentOffset >= 0, "ContentOffset must be non-negative.");
+
+ CorrectForEstimationErrors();
+
foreach (var group in _groups)
{
group.Start += delta;