Centering selected item in a scroll viewer

12,531

Solution 1

Try ListView.ScrollIntoView() or ListView.MakeVisible first to scroll the container of the item into view and work around it being possibly virtualized out of the UI. Then use ListView.ItemContainerGenerator.ContainerFromIndex() to get the container of the item and then the VisualTreeHelper to get its position relative to the ScrollViewer. Then scroll the scrollviewer by the calculated offset.

*EDIT - Example positioning logic:

Get the VisualTreeHelperExtensions from WinRT XAML Toolkit to get access to the ScrollViewer easily with GetFirstDescendantOfType() extension method that wraps some calls to the VisualTreeHelper.

XAML

<Page
    x:Class="ListViewItemCentering.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ListViewItemCentering"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid
        Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <ListView
            x:Name="listView">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Border
                        Width="400"
                        Height="100">
                        <ContentControl
                            Content="{Binding}"
                            FontSize="48"
                            Padding="20,10"/>
                    </Border>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

        <Button
            Content="Skip"
            Width="200"
            Height="100"
            HorizontalAlignment="Right"
            VerticalAlignment="Bottom"
            Click="ButtonBase_OnClick"/>
    </Grid>
</Page>

C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using WinRTXamlToolkit.Controls.Extensions;

namespace ListViewItemCentering
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        private Random random = new Random();
        public MainPage()
        {
            this.InitializeComponent();
            this.listView.ItemsSource = Enumerable.Range(1, 1000);
            this.listView.SelectionChanged += OnListViewSelectionChanged;
        }

        private async void OnListViewSelectionChanged(object sender, SelectionChangedEventArgs selectionChangedEventArgs)
        {
            if (listView.SelectedItem == null)
            {
                return;
            }

            var item = listView.SelectedItem;

            // Calculations relative to screen or ListView
            var listViewItem = (FrameworkElement)listView.ContainerFromItem(item);

            if (listViewItem == null)
            {
                listView.ScrollIntoView(item);
            }

            while (listViewItem == null)
            {
                await Task.Delay(1); // wait for scrolling to complete - it takes a moment
                listViewItem = (FrameworkElement)listView.ContainerFromItem(item);
            }

            var topLeft =
                listViewItem
                    .TransformToVisual(listView)
                    .TransformPoint(new Point()).Y;
            var lvih = listViewItem.ActualHeight;
            var lvh = listView.ActualHeight;
            var desiredTopLeft = (lvh - lvih) / 2.0;
            var desiredDelta = topLeft - desiredTopLeft;

            // Calculations relative to the ScrollViewer within the ListView
            var scrollViewer = listView.GetFirstDescendantOfType<ScrollViewer>();
            var currentOffset = scrollViewer.VerticalOffset;
            var desiredOffset = currentOffset + desiredDelta;
            scrollViewer.ScrollToVerticalOffset(desiredOffset);

            // better yet if building for Windows 8.1 to make the scrolling smoother use:
            // scrollViewer.ChangeView(null, desiredOffset, null);
        }

        private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
        {
            this.listView.SelectedIndex = random.Next(0, ((IEnumerable<int>)this.listView.ItemsSource).Count());
        }
    }
}

Solution 2

Yup, this worked for me out of the box with ScrollIntoView

    private void CountKeyBoard_NavigationEvent(object sender, Controls.UserControls.NavigationEventArgs e)
    {
        if (lvInvDetails.SelectedItem != null && lvInvDetails.SelectedIndex != -1)
        {
            switch (e.Direction)
            {
                case Controls.UserControls.NavigationEventArgs.DirectionEnum.NEXT:
                    if (lvInvDetails.SelectedIndex >= (lvInvDetails.Items.Count - 1))
                        lvInvDetails.SelectedIndex = 0;
                    else
                        lvInvDetails.SelectedIndex++;
                    break;

                case Controls.UserControls.NavigationEventArgs.DirectionEnum.PREVIOUS:
                    if (lvInvDetails.SelectedIndex > 0)
                        lvInvDetails.SelectedIndex--;
                    else
                        lvInvDetails.SelectedIndex = lvInvDetails.Items.Count - 1;
                    break;
            }
            lvInvDetails.ScrollIntoView(lvInvDetails.SelectedItem);
        }
    }
Share:
12,531
Hady
Author by

Hady

Updated on June 15, 2022

Comments

  • Hady
    Hady almost 2 years

    I am trying to center a selected item in a ListView inside a ScrollViewer and struggling to calculate the vertical offset that I should be setting the ScrollViewer relative to the ListView.

    The following links set me on the right track, but because of the limitation of the WinRT API, was not able to use them:

    The desired effect is as follows:

    centered selected item in scrollviewer

    This is a sample setup in my XAML:

    <ScrollViewer x:Name="MyScrollViewer">
        <ListView x:Name="MyView" VerticalAlignment="Center"
                          SelectionChanged="Selector_OnSelectionChanged">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Grid Width="80" Height="80" Margin="0">
                        <TextBlock Text="{Binding}" />
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
            <ListView.Items>
                <x:String>1</x:String>
                <x:String>2</x:String>
                <x:String>3</x:String>
                <x:String>4</x:String>
                <x:String>5</x:String>
                <x:String>6</x:String>
                <x:String>7</x:String>
                <x:String>8</x:String>
                <x:String>9</x:String>
            </ListView.Items>
        </ListView>
    </ScrollViewer>
    

    Knowing the index of the selected item, how do I calculate the vertical offset that I can use in my method:

    private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        double maxVerticalOffset = MyScrollViewer.ExtentHeight - MyScrollViewer.ViewportHeight;
    
        int selectedItemIndex = MyView.SelectedIndex;
    
        double verticalOffset = ...
    
        MyScrollViewer.ChangeView(null, verticalOffset, null);
    }
    
  • Hady
    Hady about 10 years
    Are you able to expand on the logic pertaining to using the VisualTreeHelper to position the selected listview item in the middle of the scroll viewer?
  • Hady
    Hady about 10 years
    No luck with the above logic. Given that its a windows store app, I was able to initially get the listViewItem as follows: ListViewItem listViewItem = (ListViewItem)listView.ContainerFromItem(listView.SelectedIt‌​em);. However, your calculation which I didn't quite understand its logic is very off from being centered in the middle of the ScrollViewer. Perhaps you can post a tested example?
  • Filip Skakun
    Filip Skakun about 10 years
    Right, my desiredDelta was the wrong way round. Let me update with a solution.
  • Hady
    Hady about 10 years
    Fantastic. Your sample worked. I think the key mistake that I was making is that I was wrapping my ListView with a ScrollViewer control. I didn't know that the ListView presenter itself contained a ScrollViewer built in that I should be using to map coordinates to and from.
  • ceth
    ceth over 9 years
    @FilipSkakun, I can not find GetFirstDescendantOfType() for ListView. Why ? Thanks for your help.
  • Filip Skakun
    Filip Skakun over 9 years
    Yes, this is an extension method in VisualTreeHelperExtensions. You can grab the file or just the method or get the entire toolkit from NuGet.
  • yalematta
    yalematta about 9 years
    If I want to do the same feature on a ListView who has an Horizontal Orientation, what should I change in your code? @FilipSkakun
  • Filip Skakun
    Filip Skakun about 9 years
    Probably Y to X and Height to Width