Windows 10的ScrollIntoView()方法无法滚动到ListView中间的项目

19

我有一个包含20个项目的Listview,我想通过编程的方式滚动Listview。

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

将滚动ListView到第一项。

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

使用该函数可以将列表滚动到页面底部。

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

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

应该滚动并把我带到列表的第五项。但是实际上它把我带到了列表的第一项。

如果能通过某些解决方法实现这种行为那就太好了?

4个回答

29

我认为你正在寻找的是一种方法来实际滚动元素到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共享相同的基类ListViewBase,所以这两个扩展方法也适用于GridView

更新

基本上,这些方法将首先查找该项,如果已经呈现,则立即滚动到该项。如果该项为null,则表示启用了虚拟化,并且尚未实现该项。因此,要先实现该项,请调用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);

1
像魔法般运作。谢谢Justin :) - Amar Zeno
2
考虑将逻辑封装在附加属性内。请查看https://dev59.com/J2sy5IYBdhLWcg3wtwQd - Justin XL
1
你是一个天才。我爱你! - Cole Peterson
1
@JustinXL 非常感谢你的帮助。在你的帮助下,我成功地创建了SmoothScrollingHelper - Vijay Nirmal
ItemsStackPanel的大距离滚动不起作用。在这个“ScrollToItem”方法中,“selectedItem”为空。 - Suresh Balaraman
显示剩余13条评论

1

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

如果您在成员上调用它,并且它在可见列表的底部以下,则向下滚动,直到该项成为可见列表中的最后一个成员。

如果您在成员上调用它,并且它在列表的顶部以上,则向上滚动,直到该项成为列表中的第一个成员。

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


2
即使我调用一个在可见列表底部以下的成员,该函数也无法正常工作。假设我在可见列表中有3个项目。ListView.ScrollIntoView(abc.ItemsCollection[6].item) 应该将第6个项目作为可见列表视图的最后一个项目带入。但在我的情况下,什么都没发生。 - Amar Zeno

0
我是这样解决的:
 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;
    }

致代码所有者的荣誉


0

试试这个:

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);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接