Windows 10 ScrollIntoView() 没有滚动到列表视图中间的项目

Windows 10 ScrollIntoView() is not scrolling to the items in the middle of a listview

我有一个包含 20 个项目的列表视图。我想以编程方式滚动列表视图。

ListView?.ScrollIntoView(ListView.Items[0])

会将列表视图滚动到第一项。

ListView?.ScrollIntoView(ListView.Items.Count - 1)

会将列表视图滚动到页面底部。

但是,我无法使用相同的功能将列表视图滚动到中间的项目。

Eg: ListView?.ScrollIntoView(ListView.Items[5])

应该滚动并将我带到列表的第 5 项。但它把我带到了列表的第一项。

如果可以通过一些解决方法实现此行为,那会很棒吗?

ScrollIntoView 只是将项目带入视图,期间,它不会滚动到一行。

如果您在成员上调用它并且它位于可见列表底部的下方,它会向下滚动直到该项目是可见列表中的最后一个成员。

如果您在成员上调用它并且它位于列表顶部上方,它会向上滚动直到该项目是列表中的第一个成员。

如果您在成员上调用它并且它当前可见,则它根本不执行任何操作。

我认为您正在寻找的是一种方法,它可以实际 滚动 一个元素到 ListView 顶部。

中,我创建了一个扩展方法,可以滚动到 ScrollViewer.

中的特定元素

你的想法是一样的。

您需要先在 ListView 中找到 ScrollViewer 实例,然后找到要滚动到的实际项目,即 ListViewItem

这是获取 ScrollViewer.

的扩展方法
public static ScrollViewer GetScrollViewer(this DependencyObject element)
{
    if (element is ScrollViewer)
    {
        return (ScrollViewer)element;
    }

    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
    {
        var child = VisualTreeHelper.GetChild(element, i);

        var result = GetScrollViewer(child);
        if (result == null)
        {
            continue;
        }
        else
        {
            return result;
        }
    }

    return null;
}

获得 ScrollViewer 实例后,我创建了另外两个扩展方法来分别根据项目的索引或附加对象滚动到项目。由于 ListViewGridView 共享相同的碱基 class ListViewBase。这两种扩展方法也应该适用于 GridView.

更新

基本上,这些方法将首先找到项目,如果它已经呈现,然后立即滚动到它。如果item为null,表示虚拟化开启,item尚未实现。所以要先实现项目,调用ScrollIntoViewAsync(基于任务的方法来包装内置的ScrollIntoView,与ChangeViewAsync相同,它提供了更简洁的代码),计算位置并保存它。因为现在我知道要滚动到的位置,所以我需要先将项目一直滚动到它之前的位置立即(即没有动画),然后最后滚动到所需的位置有动画。

public async static Task ScrollToIndex(this ListViewBase listViewBase, int index)
{
    bool isVirtualizing = default(bool);
    double previousHorizontalOffset = default(double), previousVerticalOffset = default(double);

    // get the ScrollViewer withtin the ListView/GridView
    var scrollViewer = listViewBase.GetScrollViewer();
    // get the SelectorItem to scroll to
    var selectorItem = listViewBase.ContainerFromIndex(index) as SelectorItem;

    // when it's null, means virtualization is on and the item hasn't been realized yet
    if (selectorItem == null)
    {
        isVirtualizing = true;

        previousHorizontalOffset = scrollViewer.HorizontalOffset;
        previousVerticalOffset = scrollViewer.VerticalOffset;

        // call task-based ScrollIntoViewAsync to realize the item
        await listViewBase.ScrollIntoViewAsync(listViewBase.Items[index]);

        // this time the item shouldn't be null again
        selectorItem = (SelectorItem)listViewBase.ContainerFromIndex(index);
    }

    // calculate the position object in order to know how much to scroll to
    var transform = selectorItem.TransformToVisual((UIElement)scrollViewer.Content);
    var position = transform.TransformPoint(new Point(0, 0));

    // when virtualized, scroll back to previous position without animation
    if (isVirtualizing)
    {
        await scrollViewer.ChangeViewAsync(previousHorizontalOffset, previousVerticalOffset, true);
    }

    // scroll to desired position with animation!
    scrollViewer.ChangeView(position.X, position.Y, null);
}

public async static Task ScrollToItem(this ListViewBase listViewBase, object item)
{
    bool isVirtualizing = default(bool);
    double previousHorizontalOffset = default(double), previousVerticalOffset = default(double);

    // get the ScrollViewer withtin the ListView/GridView
    var scrollViewer = listViewBase.GetScrollViewer();
    // get the SelectorItem to scroll to
    var selectorItem = listViewBase.ContainerFromItem(item) as SelectorItem;

    // when it's null, means virtualization is on and the item hasn't been realized yet
    if (selectorItem == null)
    {
        isVirtualizing = true;

        previousHorizontalOffset = scrollViewer.HorizontalOffset;
        previousVerticalOffset = scrollViewer.VerticalOffset;

        // call task-based ScrollIntoViewAsync to realize the item
        await listViewBase.ScrollIntoViewAsync(item);

        // this time the item shouldn't be null again
        selectorItem = (SelectorItem)listViewBase.ContainerFromItem(item);
    }

    // calculate the position object in order to know how much to scroll to
    var transform = selectorItem.TransformToVisual((UIElement)scrollViewer.Content);
    var position = transform.TransformPoint(new Point(0, 0));

    // when virtualized, scroll back to previous position without animation
    if (isVirtualizing)
    {
        await scrollViewer.ChangeViewAsync(previousHorizontalOffset, previousVerticalOffset, true);
    }

    // scroll to desired position with animation!
    scrollViewer.ChangeView(position.X, position.Y, null);
}

public static async Task ScrollIntoViewAsync(this ListViewBase listViewBase, object item)
{
    var tcs = new TaskCompletionSource<object>();
    var scrollViewer = listViewBase.GetScrollViewer();

    EventHandler<ScrollViewerViewChangedEventArgs> viewChanged = (s, e) => tcs.TrySetResult(null);
    try
    {
        scrollViewer.ViewChanged += viewChanged;
        listViewBase.ScrollIntoView(item, ScrollIntoViewAlignment.Leading);
        await tcs.Task;
    }
    finally
    {
        scrollViewer.ViewChanged -= viewChanged;
    }
}

public static async Task ChangeViewAsync(this ScrollViewer scrollViewer, double? horizontalOffset, double? verticalOffset, bool disableAnimation)
{
    var tcs = new TaskCompletionSource<object>();

    EventHandler<ScrollViewerViewChangedEventArgs> viewChanged = (s, e) => tcs.TrySetResult(null);
    try
    {
        scrollViewer.ViewChanged += viewChanged;
        scrollViewer.ChangeView(horizontalOffset, verticalOffset, null, disableAnimation);
        await tcs.Task;
    }
    finally
    {
        scrollViewer.ViewChanged -= viewChanged;
    }
}


更简单的方法,但没有动画

您还可以通过指定第二个参数来使用 ScrollIntoView 的新重载,以确保项目在上边缘对齐;但是,这样做在我以前的扩展方法中没有平滑的滚动过渡。

MyListView?.ScrollIntoView(MyListView.Items[5], ScrollIntoViewAlignment.Leading);

我这样解决:

 var sv = new ScrollViewerHelper().GetScrollViewer(listView);
        sv.UpdateLayout();
        sv.ChangeView(0, sv.ExtentHeight, null);

以及 GetScrollViewer 方法:

public ScrollViewer GetScrollViewer(DependencyObject element)
    {
        if (element is ScrollViewer)
        {
            return (ScrollViewer)element;
        }

        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
        {
            var child = VisualTreeHelper.GetChild(element, i);

            var result = GetScrollViewer(child);
            if (result == null)
            {
                continue;
            }
            else
            {
                return result;
            }
        }

        return null;
    }

感谢代码所有者

试试这个:

listView.SelectedIndex = i;
SemanticZoomLocation location = new SemanticZoomLocation {
    Item = listView.SelectedItem
};
listView.MakeVisible(location);

或者滚动到中间,没有选择项:

SemanticZoomLocation location = new SemanticZoomLocation {
    Item = listView.Items[listView.Items.Count / 2]
};
listView.MakeVisible(location);