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

Posted

技术标签:

【中文标题】Windows 10 ScrollIntoView() 没有滚动到列表视图中间的项目【英文标题】:Windows 10 ScrollIntoView() is not scrolling to the items in the middle of a listview 【发布时间】:2015-12-10 00:10:09 【问题描述】:

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

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

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

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

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

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

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

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

如果可以通过一些变通方法来实现此行为会很好吗?

【问题讨论】:

【参考方案1】:

我认为您正在寻找的是一种将元素实际滚动ListView顶部的方法。

在this post 中,我创建了一个扩展方法,可以滚动到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,则表示虚拟化已开启,项目尚未实现。所以要先实现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);

【讨论】:

工作就像一个魅力。谢谢贾斯汀 :) 嗨贾斯汀!我想知道如何在 MVVM 方法中附加相同的逻辑。就像在转换器中使用 changeview 逻辑并将其直接绑定到 ListView 滚动查看器 XAML。 考虑将逻辑包装在附加属性中。看看***.com/q/8370209/231837 你是个天才。我爱你! @JustinXL 非常感谢。在您的帮助下,我成功创建了SmoothScrollingHelper【参考方案2】:

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

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

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

如果你在一个成员上调用它并且它当前是可见的,它根本不做任何操作。

【讨论】:

即使我调用可见列表底部下方的成员,该功能也不起作用。假设,我在可见列表视图中有 3 个项目。 ListView.ScrollIntoView(abc.ItemsCollection[6].item) 理想情况下应该将第 6 项作为可见列表视图的最后一项。但就我而言,什么都没有发生。【参考方案3】:

我这样解决这个问题:

 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;
    

感谢代码所有者

【讨论】:

【参考方案4】:

试试这个:

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

【讨论】:

以上是关于Windows 10 ScrollIntoView() 没有滚动到列表视图中间的项目的主要内容,如果未能解决你的问题,请参考以下文章

在 scrollIntoView 的动态长度数组中使用 Refs

Vaadin 10 滚动查看

scrollIntoView()

scrollIntoView将指定元素定位到浏览器顶部,底部,中间

scrollIntoView+锚点两种方式实现页面的平滑滚动,长页面分页

前端好用API之scrollIntoView