WPF ListBox scroll to selected item smoothly via animation including oscillation and springness

//xaml
<Window x:Class="WpfApp194.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:behavior="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:local="clr-namespace:WpfApp194"
        WindowState="Maximized"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <Style x:Key="SmoothScrollViewer" TargetType="ScrollViewer">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ScrollViewer">
                        <Grid>
                            <ScrollContentPresenter/>
                            <ScrollBar x:Name="VerticalScrollBar"
                              Orientation="Vertical"
                              Value="{TemplateBinding VerticalOffset}"
                              Maximum="{TemplateBinding ScrollableHeight}"
                              ViewportSize="{TemplateBinding ViewportHeight}"/>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style TargetType="ListBox">
            <Setter Property="ScrollViewer.CanContentScroll" Value="True"/>
            <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ListBox">
                        <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                            <ScrollViewer Style="{StaticResource SmoothScrollViewer}"
                                  Focusable="False"
                                  Padding="{TemplateBinding Padding}">
                                <ItemsPresenter/>
                            </ScrollViewer>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid>
        <ListBox x:Name="lbx">
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="Height" Value="50"/>
                    <Setter Property="FontSize" Value="30"/>
                    <Style.Triggers>
                        <Trigger Property="IsSelected" Value="True">
                            <Setter Property="Background" Value="Red"/>
                            <Setter Property="Foreground" Value="Red"/>
                            <Setter Property="Height" Value="50"/>
                            <Setter Property="FontSize" Value="40"/>
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </ListBox.ItemContainerStyle>            
        </ListBox>
    </Grid>
</Window>



//xaml.cs
using Microsoft.Xaml.Behaviors;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp194
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            InitLbx();
        }

        private Random rnd;
        private int selectedIdx;

        private System.Timers.Timer tmr;
        private void InitLbx()
        {
            lbx.Items.Clear();
            for(int i=0;i<100;i++)
            {
                lbx.Items.Add($"Item_{i}");
            }

            if (tmr == null)
            {
                tmr = new System.Timers.Timer();
                tmr.Interval = 1000;
                tmr.Elapsed += Tmr_Elapsed;
                tmr.Start();
            }

            if(rnd==null)
            {
                rnd=new Random();
            }
        }

        int prevIdx = 0;
        private void Tmr_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {

            Application.Current?.Dispatcher.BeginInvoke(new Action(() =>
            {
                selectedIdx = rnd.Next(0, 100);
                this.Title = $"{selectedIdx}";
                lbx.SelectedItem=lbx.Items[selectedIdx];
                var scrollViewer = FindVisualChild<ScrollViewer>(lbx);
                if (scrollViewer != null)
                {
                    double targetOffset = (selectedIdx - prevIdx) * 50;
                    var animation = new DoubleAnimation
                    {
                        To = targetOffset,
                        Duration = TimeSpan.FromMilliseconds(500),
                        EasingFunction = new ElasticEase { Oscillations = 5, Springiness = 5 }
                    };
                    scrollViewer.BeginAnimation(ScrollViewerBehavior.VerticalOffsetProperty, animation);
                }
            }));           
        }

        private static T FindVisualChild<T>(DependencyObject obj) where T : DependencyObject
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
            {
                var child = VisualTreeHelper.GetChild(obj, i);
                if (child is T result)
                {
                    return result;
                }
                    
                var descendant = FindVisualChild<T>(child);
                if (descendant != null)
                {
                    return descendant;
                }                    
            }
            return null;
        }
    } 

    public static class SmoothScroller
    {
        public static void SmoothScrollToItem(this ListBox listBox, object item)
        {
            var scrollViewer = FindVisualChild<ScrollViewer>(listBox);
            if (scrollViewer == null) return;

            var container = listBox.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
            if (container == null) return;

            // Calculate target offset
            var transform = container.TransformToVisual(scrollViewer);
            var position = transform.Transform(new Point(0, 0));
            var targetOffset = scrollViewer.VerticalOffset + position.Y;

            // Animate the scrolling
            var animation = new DoubleAnimation
            {
                To = targetOffset,
                Duration = TimeSpan.FromMilliseconds(300),
                EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
            };

            scrollViewer.BeginAnimation(ScrollViewerBehavior.VerticalOffsetProperty, animation);
        }

        private static T FindVisualChild<T>(DependencyObject obj) where T : DependencyObject
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
            {
                var child = VisualTreeHelper.GetChild(obj, i);
                if (child is T result)
                    return result;
                var descendant = FindVisualChild<T>(child);
                if (descendant != null)
                    return descendant;
            }
            return null;
        }
    }

    public static class ScrollViewerBehavior
    {
        public static readonly DependencyProperty VerticalOffsetProperty =
            DependencyProperty.RegisterAttached("VerticalOffset",typeof(double),
                typeof(ScrollViewerBehavior),
                 new FrameworkPropertyMetadata(50.0d, 
                     FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnVerticalOffsetChanged));

        public static double GetVerticalOffset(DependencyObject dObj)
        {
            return (double)dObj.GetValue(VerticalOffsetProperty);
        }

        public static void SetVerticalOffset(DependencyObject dObj,double value)
        {
            dObj.SetValue(VerticalOffsetProperty, value);
        }

        private static void OnVerticalOffsetChanged(DependencyObject dObj, 
            DependencyPropertyChangedEventArgs e)
        {
            if (dObj is ScrollViewer scrollViewer && !double.IsNaN((double)e.NewValue))
            {
                scrollViewer.ScrollToVerticalOffset((double)e.NewValue);
            }
        }
    }
}

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2025-04-04 19:31  FredGrit  阅读(23)  评论(0)    收藏  举报