Skip to content

Commit 3587774

Browse files
fix(listview): [WASM [Skia] Trim excess scroll when item at end of list is removed
1 parent 5184fb5 commit 3587774

File tree

5 files changed

+145
-9
lines changed

5 files changed

+145
-9
lines changed

src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.cs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using Windows.UI.Xaml.Controls;
2121
using Windows.UI.Xaml.Controls.Primitives;
2222
using static Private.Infrastructure.TestServices;
23+
using Windows.Foundation;
2324

2425
namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls
2526
{
@@ -33,6 +34,8 @@ public partial class Given_ListViewBase
3334

3435
private Style ContainerMarginStyle => _testsResources["ListViewContainerMarginStyle"] as Style;
3536

37+
private Style NoSpaceContainerStyle => _testsResources["NoExtraSpaceListViewContainerStyle"] as Style;
38+
3639
private DataTemplate TextBlockItemTemplate => _testsResources["TextBlockItemTemplate"] as DataTemplate;
3740

3841
private DataTemplate SelfHostingItemTemplate => _testsResources["SelfHostingItemTemplate"] as DataTemplate;
@@ -538,7 +541,8 @@ public async Task When_CollectionViewSource_In_Xaml()
538541
await WindowHelper.WaitFor(() => (lvi = page.SubjectListView.ContainerFromItem("One") as ListViewItem) != null);
539542
}
540543

541-
private static ContentControl[] GetPanelChildren(ListViewBase list) {
544+
private static ContentControl[] GetPanelChildren(ListViewBase list)
545+
{
542546
#if __ANDROID__ || __IOS__
543547
return list.GetItemsPanelChildren().OfType<ContentControl>().ToArray();
544548
#else
@@ -610,5 +614,47 @@ public void When_Selection_SelectedValue_Path_Not_Set()
610614
Assert.AreEqual(null, SUT.SelectedItem);
611615
Assert.AreEqual(-1, SUT.SelectedIndex);
612616
}
617+
618+
[TestMethod]
619+
public async Task When_Scrolled_To_End_And_Last_Item_Removed()
620+
{
621+
var container = new Grid { Height = 210 };
622+
623+
var list = new ListView
624+
{
625+
ItemContainerStyle = NoSpaceContainerStyle,
626+
ItemTemplate = FixedSizeItemTemplate
627+
};
628+
container.Children.Add(list);
629+
630+
var source = new ObservableCollection<int>(Enumerable.Range(0, 20));
631+
list.ItemsSource = source;
632+
633+
WindowHelper.WindowContent = container;
634+
await WindowHelper.WaitForLoaded(list);
635+
636+
ScrollBy(list, 10000); // Scroll to end
637+
638+
ListViewItem lastItem = null;
639+
await WindowHelper.WaitFor(() => (lastItem = list.ContainerFromItem(19) as ListViewItem) != null);
640+
var secondLastItem = list.ContainerFromItem(18) as ListViewItem;
641+
642+
Assert.AreEqual(181, GetTop(lastItem), delta: 1);
643+
Assert.AreEqual(152, GetTop(secondLastItem), delta: 1);
644+
645+
source.Remove(19);
646+
647+
await WindowHelper.WaitFor(() => list.Items.Count == 19);
648+
649+
await WindowHelper.WaitFor(() => ApproxEquals(181, GetTop(secondLastItem)), message: $"Expected 181 but got {GetTop(secondLastItem)}");
650+
651+
double GetTop(FrameworkElement element)
652+
{
653+
var transform = element.TransformToVisual(container);
654+
return transform.TransformPoint(new Point()).Y;
655+
}
656+
}
657+
658+
private bool ApproxEquals(double value1, double value2) => Math.Abs(value1 - value2) <= 1;
613659
}
614660
}

src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/TestsResources.xaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@
1616
</Setter.Value>
1717
</Setter>
1818
</Style>
19+
<Style x:Key="NoExtraSpaceListViewContainerStyle"
20+
TargetType="ListViewItem">
21+
<Setter Property="Padding"
22+
Value="0" />
23+
<Setter Property="MinWidth"
24+
Value="0" />
25+
<Setter Property="MinHeight"
26+
Value="1" />
27+
<Setter Property="Template">
28+
<Setter.Value>
29+
<ControlTemplate TargetType="ListViewItem">
30+
<ContentPresenter Content="{TemplateBinding Content}"
31+
ContentTemplate="{TemplateBinding ContentTemplate}" />
32+
</ControlTemplate>
33+
</Setter.Value>
34+
</Setter>
35+
</Style>
1936
<Style x:Key="CounterItemsControlContainerStyle"
2037
TargetType="ContentControl">
2138
<Setter Property="Template">

src/Uno.UI/UI/Xaml/Controls/ScrollViewer/ScrollViewer.cs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -636,9 +636,14 @@ protected override Size ArrangeOverride(Size finalSize)
636636
{
637637
ViewportArrangeSize = finalSize;
638638

639-
return base.ArrangeOverride(finalSize);
639+
var arrangeSize = base.ArrangeOverride(finalSize);
640+
TrimOverscroll(Orientation.Horizontal);
641+
TrimOverscroll(Orientation.Vertical);
642+
return arrangeSize;
640643
}
641644

645+
partial void TrimOverscroll(Orientation orientation);
646+
642647
internal override void OnLayoutUpdated()
643648
{
644649
base.OnLayoutUpdated();
@@ -795,7 +800,6 @@ protected override void OnApplyTemplate()
795800
{
796801
// Cleanup previous template
797802
DetachScrollBars();
798-
799803

800804
base.OnApplyTemplate();
801805

@@ -886,7 +890,7 @@ private void ApplyScrollContentPresenterContent()
886890
provider.Store.SetValue(provider.Store.TemplatedParentProperty, null, DependencyPropertyValuePrecedences.Local);
887891
}
888892
}
889-
893+
890894
// Then explicitly propagate the Content to the _presenter
891895
if (_presenter != null)
892896
{
@@ -1040,7 +1044,7 @@ private void AttachScrollBars()
10401044
}
10411045

10421046
bool hasManagedHorizontalScrollBar;
1043-
if (_horizontalScrollbar is {} horizontal)
1047+
if (_horizontalScrollbar is { } horizontal)
10441048
{
10451049
horizontal.Scroll += OnHorizontalScrollBarScrolled;
10461050
hasManagedHorizontalScrollBar = true;
@@ -1094,7 +1098,7 @@ private void OnHorizontalScrollBarScrolled(object sender, ScrollEventArgs e)
10941098
};
10951099

10961100
ChangeViewScroll(e.NewValue, null, disableAnimation: immediate);
1097-
}
1101+
}
10981102
#endregion
10991103

11001104
// Presenter to Control, i.e. OnPresenterScrolled
@@ -1152,6 +1156,8 @@ private void Update(bool isIntermediate)
11521156
HorizontalOffset = _pendingHorizontalOffset;
11531157
VerticalOffset = _pendingVerticalOffset;
11541158

1159+
UpdatePartial(isIntermediate);
1160+
11551161
#if !__SKIA__
11561162
// Effective viewport support
11571163
ScrollOffsets = new Point(_pendingHorizontalOffset, _pendingVerticalOffset);
@@ -1161,6 +1167,8 @@ private void Update(bool isIntermediate)
11611167
ViewChanged?.Invoke(this, new ScrollViewerViewChangedEventArgs { IsIntermediate = isIntermediate });
11621168
}
11631169

1170+
partial void UpdatePartial(bool isIntermediate);
1171+
11641172
/// <summary>
11651173
/// Causes the ScrollViewer to load a new view into the viewport using the specified offsets and zoom factor, and optionally disables scrolling animation.
11661174
/// </summary>
@@ -1205,7 +1213,7 @@ public bool ChangeView(double? horizontalOffset, double? verticalOffset, float?
12051213
partial void ChangeViewScroll(double? horizontalOffset, double? verticalOffset, bool disableAnimation);
12061214
partial void ChangeViewZoom(float zoomFactor, bool disableAnimation);
12071215

1208-
#region Scroll indicators visual states (Managed scroll bars only)
1216+
#region Scroll indicators visual states (Managed scroll bars only)
12091217
private DispatcherQueueTimer? _indicatorResetTimer;
12101218
private string? _indicatorState;
12111219

@@ -1255,7 +1263,7 @@ private void ResetScrollIndicator(bool forced = false)
12551263
{
12561264
// We don't auto hide the indicators if the pointer is over it!
12571265
// Note: the pointer has to move over this ScrollViewer to exit the ScrollBar, so we will restart the reset timer!
1258-
return;
1266+
return;
12591267
}
12601268

12611269
VisualStateManager.GoToState(this, VisualStates.ScrollingIndicator.None, true);
@@ -1284,6 +1292,6 @@ private void HideScrollBarSeparator(object sender, PointerRoutedEventArgs e) //
12841292
VisualStateManager.GoToState(this, VisualStates.ScrollBarsSeparator.Collapsed, true);
12851293
}
12861294
}
1287-
#endregion
1295+
#endregion
12881296
}
12891297
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#nullable enable
2+
using Windows.UI.Xaml.Controls;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
using Windows.UI.Xaml.Data;
9+
using Windows.UI.Xaml.Media;
10+
using Windows.UI.Xaml.Media.Animation;
11+
using Windows.Foundation;
12+
using Windows.UI;
13+
14+
namespace Windows.UI.Xaml.Controls
15+
{
16+
public partial class ScrollViewer
17+
{
18+
/// <summary>
19+
/// Trim excess scroll, which can be present if the content size is reduced.
20+
/// </summary>
21+
partial void TrimOverscroll(Orientation orientation)
22+
{
23+
if (_presenter is ContentPresenter presenter && presenter.Content is FrameworkElement presenterContent)
24+
{
25+
var presenterViewportSize = GetActualExtent(presenter, orientation);
26+
var contentExtent = GetActualExtent(presenterContent, orientation);
27+
var offset = GetOffsetForOrientation(orientation);
28+
var viewportEnd = offset + presenterViewportSize;
29+
var overscroll = contentExtent - viewportEnd;
30+
if (offset > 0 && overscroll < -0.5)
31+
{
32+
ChangeViewForOrientation(orientation, overscroll);
33+
}
34+
}
35+
}
36+
37+
private double GetOffsetForOrientation(Orientation orientation)
38+
=> orientation == Orientation.Horizontal ? HorizontalOffset : VerticalOffset;
39+
40+
private void ChangeViewForOrientation(Orientation orientation, double scrollAdjustment)
41+
{
42+
if (orientation == Orientation.Vertical)
43+
{
44+
ChangeView(null, VerticalOffset + scrollAdjustment, null, disableAnimation: true);
45+
}
46+
else
47+
{
48+
ChangeView(HorizontalOffset + scrollAdjustment, null, null, disableAnimation: true);
49+
}
50+
}
51+
52+
private static double GetActualExtent(FrameworkElement element, Orientation orientation)
53+
=> orientation == Orientation.Horizontal ? element.ActualWidth : element.ActualHeight;
54+
}
55+
}

src/Uno.UI/UI/Xaml/Controls/ScrollViewer/ScrollViewer.wasm.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.Extensions.Logging;
55
using Uno.Logging;
66
using Uno.UI.Xaml;
7+
using Uno.UI;
78

89
namespace Windows.UI.Xaml.Controls
910
{
@@ -31,5 +32,14 @@ partial void ChangeViewScroll(double? horizontalOffset, double? verticalOffset,
3132
_log.Warn("Cannot ChangeView as ScrollContentPresenter is not ready yet.");
3233
}
3334
}
35+
36+
partial void UpdatePartial(bool isIntermediate)
37+
{
38+
if (FeatureConfiguration.UIElement.AssignDOMXamlProperties)
39+
{
40+
UpdateDOMXamlProperty(nameof(HorizontalOffset), HorizontalOffset);
41+
UpdateDOMXamlProperty(nameof(VerticalOffset), VerticalOffset);
42+
}
43+
}
3444
}
3545
}

0 commit comments

Comments
 (0)