Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Listview.ScrollIntoView for non-virtualizing panels #16245

Merged
merged 7 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
xmlns:local="using:SamplesApp.Windows_UI_Xaml_Controls.ListView"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
Expand All @@ -16,12 +17,18 @@
</StackPanel>
</DataTemplate>
</UserControl.Resources>

<Grid>
<ListView x:Name="myList"
ItemsSource="{Binding [SampleItemsMed]}"
ItemTemplate="{StaticResource ListItemTemplate}"/>
<Button Content="Bring into view"
Click="BringIntoView" />
</Grid>

<StackPanel>
<StackPanel Orientation="Horizontal">
<muxc:NumberBox x:Name="nb" Minimum="0" SmallChange="1" />
<StackPanel>
<TextBlock Text="Leading" />
<CheckBox x:Name="chkBox" />
</StackPanel>
<Button Content="Bring into view" Click="BringIntoView" />
</StackPanel>
<Grid Height="400">
<ListView x:Name="myList" ItemTemplate="{StaticResource ListItemTemplate}" />
</Grid>
</StackPanel>
</UserControl>
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Uno.UI.Samples.Controls;
using System;
using System.Linq;
using Uno.UI.Samples.Controls;
using Microsoft.UI.Xaml.Controls;
using SamplesApp.Windows_UI_Xaml_Controls.Models;
using Microsoft.UI.Xaml;
Expand All @@ -14,13 +16,18 @@ public sealed partial class ListView_BringIntoView : UserControl
public ListView_BringIntoView()
{
this.InitializeComponent();

var random = new Random(42);
// using newlines to vary the height of each item
myList.ItemsSource = Enumerable.Range(0, 100).Select(i => $"item {i}" + new string('\n', random.Next(0, 5))).ToArray();
}

public void BringIntoView(object sender, RoutedEventArgs e)
{
var list = (Microsoft.UI.Xaml.Controls.ListViewBase)this.myList;

list.ScrollIntoView(list.SelectedItem, ScrollIntoViewAlignment.Leading);
var alignment = (chkBox.IsChecked ?? false) ? ScrollIntoViewAlignment.Leading : ScrollIntoViewAlignment.Default;
list.ScrollIntoView(list.ItemFromIndex((int)nb.Value), alignment);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4483,6 +4483,82 @@ void SetSelectedItem(object item)
else { throw new NotImplementedException(); }
}
}

[TestMethod]
[RunsOnUIThread]
#if __MACOS__
[Ignore("NotImplemented ListViewBase.ScrollIntoView")]
#elif __ANDROID__ || __IOS__
[Ignore("This test is for managed ListViewBase.")]
#endif
public async Task When_ScrollIntoView_No_Virtualization()
{
var source = Enumerable.Range(0, 100).ToArray();
var lv = new ListView { Width = 400, Height = 200, ItemsSource = source };

await UITestHelper.Load(lv);

lv.ScrollIntoView(source[50]);
await WindowHelper.WaitForIdle();

var sv = lv.FindFirstDescendant<ScrollViewer>();
var container = (ContentControl)lv.ContainerFromIndex(50);

var offset = container.TransformToVisual(lv).TransformPoint(default);
var (offsetStart, vpExtent) = (offset.Y, sv.ViewportHeight);

Assert.IsTrue(0 <= offsetStart && offsetStart + container.ActualHeight <= vpExtent, $"Container#50 should be within viewport: 0 <= {offsetStart} <= {vpExtent}");
}

#if HAS_UNO
[TestMethod]
[RunsOnUIThread]
#if __MACOS__
[Ignore("NotImplemented ListViewBase.ScrollIntoView")]
#else
[Ignore("https://github.com/unoplatform/uno/issues/16246")]
#endif
public async Task When_ScrollIntoView_Containers_With_Varying_Heights()
{
var random = new Random(42);
var lv = new ListView
{
Height = 400,
ItemsSource = Enumerable.Range(0, 100).Select(i => $"item {i}" + new string('\n', random.Next(0, 5))).ToArray(),
ItemTemplate = new DataTemplate(() =>
{
var tb = new TextBlock();
tb.SetBinding(TextBlock.TextProperty, new Binding());

return new StackPanel
{
Padding = new Thickness(16),
Children =
{
tb
}
};
})
};

await UITestHelper.Load(lv);

var sv = lv.FindFirstDescendant<ScrollViewer>();
var i = 7049.5;
while (i > 0)
{
sv.ScrollToVerticalOffset(i);
await WindowHelper.WaitForIdle();
i -= 400;
// try i -= 800, it will also fail the test even though visually, item 0 is visible, but the list view is interally corrupted
}

sv.ScrollToVerticalOffset(i);
await WindowHelper.WaitForIdle();

Assert.IsNotNull(lv.ContainerFromItem(0));
}
#endif
}

public partial class Given_ListViewBase // data class, data-context, view-model, template-selector
Expand Down
47 changes: 44 additions & 3 deletions src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBase.managed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,54 @@ private void TryLoadMoreItems()

public void ScrollIntoView(object item, ScrollIntoViewAlignment alignment)
{
if (VirtualizingPanel?.GetLayouter() is { } layouter)
if (ContainerFromItem(item) is UIElement element)
{
// The container we want to jump to is already materialized, so just jump to it.
// This means we're in a non-virtualizing panel or in a virtualizing panel where the container we want is materialized for some reason (e.g. partially in view)
ScrollIntoViewFastPath(element, alignment);
}
else if (VirtualizingPanel?.GetLayouter() is { } layouter)
{
layouter.ScrollIntoView(item, alignment);
}
else if (this.Log().IsEnabled(LogLevel.Warning))
}

private void ScrollIntoViewFastPath(UIElement element, ScrollIntoViewAlignment alignment)
{
if (ScrollViewer is { } sv && sv.Presenter is { } presenter)
{
this.Log().LogWarning($"{nameof(ScrollIntoView)} not supported when using non-virtualizing panels.");
var offsetXY = element.TransformToVisual(presenter).TransformPoint(Point.Zero);

var (newOffset, elementLength, presenterOffset, presenterViewportLength) =
ItemsPanelRoot.PhysicalOrientation is Orientation.Vertical
? (offsetXY.Y, element.ActualSize.Y, presenter.VerticalOffset, presenter.ViewportHeight)
: (offsetXY.X, element.ActualSize.X, presenter.HorizontalOffset, presenter.ViewportWidth);

if (presenterOffset < newOffset && newOffset + elementLength < presenterOffset + presenterViewportLength)
{
// if the element is within the visible viewport, do nothing.
return;
}

// If we use the above offset directly, the item we want to jump to will be the start of the viewport, i.e. leading
if (alignment is ScrollIntoViewAlignment.Default)
{
if (presenterOffset < newOffset)
{
// scroll one "viewport page" less: this brings the element's start right after the viewport's length ends
// we then scroll again by elementLength so that the end of the element is the end of the viewport
newOffset += (-presenterViewportLength) + elementLength;
}
}

if (ItemsPanelRoot.PhysicalOrientation is Orientation.Vertical)
{
sv.ScrollToVerticalOffset(newOffset);
}
else
{
sv.ScrollToHorizontalOffset(newOffset);
}
}
}
#endif
Expand Down
Loading