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:
- Make ListView.ScrollIntoView Scroll the Item into the Center of the ListView (C#)
- http://blogs.msdn.com/b/delay/archive/2009/04/19/fewer-gotchas-to-getcha-enhancing-the-scrollintoviewcentered-method-for-wpf-s-listbox.aspx
The desired effect is as follows:
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);
}
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());
}
}
}
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);
}
}