Подтвердить что ты не робот

Анимированная (плавная) прокрутка на ScrollViewer

У меня есть ScrollViewer в моем приложении WPF, и я хочу, чтобы он имел эффект плавной/анимированной прокрутки, как в Firefox (если вы знаете, о чем я говорю).

Я пытался искать в Интернете, и единственное, что я нашел, это:

Как создать анимированный ScrollViewer (или ListBox) в WPF

Он работает довольно хорошо, но у меня есть одна проблема - он анимирует эффект прокрутки, но ScrollViewer Thumb идет прямо к нажатой точке - я хочу, чтобы он тоже был анимирован

Как я могу заставить анимировать ScrollViewer Thumb, или же есть рабочий элемент управления с теми же свойствами/функциями, которые я хочу?

4b9b3361

Ответ 1

В вашем примере есть два элемента управления, унаследованных от ScrollViewer и ListBox, анимация реализована с помощью SplineDoubleKeyFrame [MSDN]. В свое время я реализовал прокрутку анимации с помощью присоединенного свойства зависимостей VerticalOffsetProperty, которое позволяет вам напрямую переводить полосу прокрутки смещения в двойную анимацию, например:

DoubleAnimation verticalAnimation = new DoubleAnimation();

verticalAnimation.From = scrollViewer.VerticalOffset;
verticalAnimation.To = some value;
verticalAnimation.Duration = new Duration( some duration );

Storyboard storyboard = new Storyboard();

storyboard.Children.Add(verticalAnimation);
Storyboard.SetTarget(verticalAnimation, scrollViewer);
Storyboard.SetTargetProperty(verticalAnimation, new PropertyPath(ScrollAnimationBehavior.VerticalOffsetProperty)); // Attached dependency property
storyboard.Begin();

Примеры можно найти здесь:

Как: анимировать свойства Horizontal/VerticalOffset ScrollViewer

WPF - Animate ListBox.ScrollViewer.HorizontOffset?

В этом случае хорошо работает плавная прокрутка контента и Thumb. Основываясь на этом подходе и используя ваш пример [Как создать анимированный ScrollViewer (или ListBox) в WPF], я создал прикрепленное поведение ScrollAnimationBehavior, которое можно применять к ScrollViewer и ListBox.

Пример использования:

XAML

<Window x:Class="ScrollAnimateBehavior.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:AttachedBehavior="clr-namespace:ScrollAnimateBehavior.AttachedBehaviors"
        Title="MainWindow" 
        WindowStartupLocation="CenterScreen"
        Height="350"
        Width="525">

    <Window.Resources>
        <x:Array x:Key="TestArray" Type="{x:Type sys:String}">
            <sys:String>TEST 1</sys:String>
            <sys:String>TEST 2</sys:String>
            <sys:String>TEST 3</sys:String>
            <sys:String>TEST 4</sys:String>
            <sys:String>TEST 5</sys:String>
            <sys:String>TEST 6</sys:String>
            <sys:String>TEST 7</sys:String>
            <sys:String>TEST 8</sys:String>
            <sys:String>TEST 9</sys:String>
            <sys:String>TEST 10</sys:String>
        </x:Array>
    </Window.Resources>

    <Grid>
        <TextBlock Text="ScrollViewer"
                   FontFamily="Verdana"
                   FontSize="14"
                   VerticalAlignment="Top"
                   HorizontalAlignment="Left"
                   Margin="80,80,0,0" />

        <ScrollViewer AttachedBehavior:ScrollAnimationBehavior.IsEnabled="True"                         
                      AttachedBehavior:ScrollAnimationBehavior.TimeDuration="00:00:00.20"
                      AttachedBehavior:ScrollAnimationBehavior.PointsToScroll="16"
                      HorizontalAlignment="Left"
                      Width="250"
                      Height="100">

            <StackPanel>
                <ItemsControl ItemsSource="{StaticResource TestArray}"
                              FontSize="16" />
            </StackPanel>
        </ScrollViewer>

        <TextBlock Text="ListBox"
                   FontFamily="Verdana"
                   FontSize="14"
                   VerticalAlignment="Top"
                   HorizontalAlignment="Right"
                   Margin="0,80,100,0" />

        <ListBox AttachedBehavior:ScrollAnimationBehavior.IsEnabled="True"
                 ItemsSource="{StaticResource TestArray}"
                 ScrollViewer.CanContentScroll="False"
                 HorizontalAlignment="Right"
                 FontSize="16"
                 Width="250"
                 Height="100" />        
    </Grid>
</Window>

Output

enter image description here

Свойство IsEnabled отвечает за анимацию прокрутки для ScrollViewer и для ListBox. Ниже его реализация:

public static DependencyProperty IsEnabledProperty =
                                 DependencyProperty.RegisterAttached("IsEnabled",
                                 typeof(bool),
                                 typeof(ScrollAnimationBehavior),
                                 new UIPropertyMetadata(false, OnIsEnabledChanged));

public static void SetIsEnabled(FrameworkElement target, bool value)
{
    target.SetValue(IsEnabledProperty, value);
}

public static bool GetIsEnabled(FrameworkElement target)
{
    return (bool)target.GetValue(IsEnabledProperty);
}

private static void OnIsEnabledChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    var target = sender;

    if (target != null && target is ScrollViewer)
    {
        ScrollViewer scroller = target as ScrollViewer;
        scroller.Loaded += new RoutedEventHandler(scrollerLoaded);
    }

    if (target != null && target is ListBox) 
    {
        ListBox listbox = target as ListBox;
        listbox.Loaded += new RoutedEventHandler(listboxLoaded);
    }
}

В этих обработчиках Loaded установлены обработчики событий для PreviewMouseWheel и PreviewKeyDown.

Помощники (вспомогательные процедуры) взяты из примера и предоставляют значение типа double, которое передается в процедуру AnimateScroll(). Вот и есть волшебный ключ анимации:

private static void AnimateScroll(ScrollViewer scrollViewer, double ToValue)
{
    DoubleAnimation verticalAnimation = new DoubleAnimation();

    verticalAnimation.From = scrollViewer.VerticalOffset;
    verticalAnimation.To = ToValue;
    verticalAnimation.Duration = new Duration(GetTimeDuration(scrollViewer));

    Storyboard storyboard = new Storyboard();

    storyboard.Children.Add(verticalAnimation);
    Storyboard.SetTarget(verticalAnimation, scrollViewer);
    Storyboard.SetTargetProperty(verticalAnimation, new PropertyPath(ScrollAnimationBehavior.VerticalOffsetProperty));
    storyboard.Begin();
}

Some notes

  • В примере реализована только вертикальная анимация, если вы примете этот проект, вы без проблем реализуете горизонтальную анимацию.

  • Выбор текущего элемента в ListBox, не переданного следующему элементу этого, происходит из-за перехвата событий PreviewKeyDown, поэтому вы должны подумать об этом моменте.

  • Эта реализация полностью подходит для шаблона MVVM. Чтобы использовать это поведение в Blend, вам нужно наследовать интерфейс Behavior. Пример можно найти здесь и здесь.

Tested on Windows XP, Windows Seven, .NET 4.0.


Пример проекта доступен по этой ссылке.


Ниже приведен полный код этой реализации:

public static class ScrollAnimationBehavior
{
    #region Private ScrollViewer for ListBox

    private static ScrollViewer _listBoxScroller = new ScrollViewer();

    #endregion

    #region VerticalOffset Property

    public static DependencyProperty VerticalOffsetProperty =
        DependencyProperty.RegisterAttached("VerticalOffset",
                                            typeof(double),
                                            typeof(ScrollAnimationBehavior),
                                            new UIPropertyMetadata(0.0, OnVerticalOffsetChanged));

    public static void SetVerticalOffset(FrameworkElement target, double value)
    {
        target.SetValue(VerticalOffsetProperty, value);
    }

    public static double GetVerticalOffset(FrameworkElement target)
    {
        return (double)target.GetValue(VerticalOffsetProperty);
    }

    #endregion

    #region TimeDuration Property

    public static DependencyProperty TimeDurationProperty =
        DependencyProperty.RegisterAttached("TimeDuration",
                                            typeof(TimeSpan),
                                            typeof(ScrollAnimationBehavior),
                                            new PropertyMetadata(new TimeSpan(0, 0, 0, 0, 0)));

    public static void SetTimeDuration(FrameworkElement target, TimeSpan value)
    {
        target.SetValue(TimeDurationProperty, value);
    }

    public static TimeSpan GetTimeDuration(FrameworkElement target)
    {
        return (TimeSpan)target.GetValue(TimeDurationProperty);
    }

    #endregion

    #region PointsToScroll Property

    public static DependencyProperty PointsToScrollProperty =
        DependencyProperty.RegisterAttached("PointsToScroll",
                                            typeof(double),
                                            typeof(ScrollAnimationBehavior),
                                            new PropertyMetadata(0.0));

    public static void SetPointsToScroll(FrameworkElement target, double value)
    {
        target.SetValue(PointsToScrollProperty, value);
    }

    public static double GetPointsToScroll(FrameworkElement target)
    {
        return (double)target.GetValue(PointsToScrollProperty);
    }

    #endregion

    #region OnVerticalOffset Changed

    private static void OnVerticalOffsetChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        ScrollViewer scrollViewer = target as ScrollViewer;

        if (scrollViewer != null)
        {
            scrollViewer.ScrollToVerticalOffset((double)e.NewValue);
        }
    }

    #endregion

    #region IsEnabled Property

    public static DependencyProperty IsEnabledProperty =
                                            DependencyProperty.RegisterAttached("IsEnabled",
                                            typeof(bool),
                                            typeof(ScrollAnimationBehavior),
                                            new UIPropertyMetadata(false, OnIsEnabledChanged));

    public static void SetIsEnabled(FrameworkElement target, bool value)
    {
        target.SetValue(IsEnabledProperty, value);
    }

    public static bool GetIsEnabled(FrameworkElement target)
    {
        return (bool)target.GetValue(IsEnabledProperty);
    }

    #endregion

    #region OnIsEnabledChanged Changed

    private static void OnIsEnabledChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var target = sender;

        if (target != null && target is ScrollViewer)
        {
            ScrollViewer scroller = target as ScrollViewer;
            scroller.Loaded += new RoutedEventHandler(scrollerLoaded);
        }

        if (target != null && target is ListBox) 
        {
            ListBox listbox = target as ListBox;
            listbox.Loaded += new RoutedEventHandler(listboxLoaded);
        }
    }

    #endregion

    #region AnimateScroll Helper

    private static void AnimateScroll(ScrollViewer scrollViewer, double ToValue)
    {
        DoubleAnimation verticalAnimation = new DoubleAnimation();

        verticalAnimation.From = scrollViewer.VerticalOffset;
        verticalAnimation.To = ToValue;
        verticalAnimation.Duration = new Duration(GetTimeDuration(scrollViewer));

        Storyboard storyboard = new Storyboard();

        storyboard.Children.Add(verticalAnimation);
        Storyboard.SetTarget(verticalAnimation, scrollViewer);
        Storyboard.SetTargetProperty(verticalAnimation, new PropertyPath(ScrollAnimationBehavior.VerticalOffsetProperty));
        storyboard.Begin();
    }

    #endregion

    #region NormalizeScrollPos Helper

    private static double NormalizeScrollPos(ScrollViewer scroll, double scrollChange, Orientation o)
    {
        double returnValue = scrollChange;

        if (scrollChange < 0)
        {
            returnValue = 0;
        }

        if (o == Orientation.Vertical && scrollChange > scroll.ScrollableHeight)
        {
            returnValue = scroll.ScrollableHeight;
        }
        else if (o == Orientation.Horizontal && scrollChange > scroll.ScrollableWidth)
        {
            returnValue = scroll.ScrollableWidth;
        }

        return returnValue;
    }

    #endregion

    #region UpdateScrollPosition Helper

    private static void UpdateScrollPosition(object sender)
    {
        ListBox listbox = sender as ListBox;

        if (listbox != null)
        {
            double scrollTo = 0;

            for (int i = 0; i < (listbox.SelectedIndex); i++)
            {
                ListBoxItem tempItem = listbox.ItemContainerGenerator.ContainerFromItem(listbox.Items[i]) as ListBoxItem;

                if (tempItem != null)
                {
                    scrollTo += tempItem.ActualHeight;
                }
            }

            AnimateScroll(_listBoxScroller, scrollTo);
        }
    }

    #endregion

    #region SetEventHandlersForScrollViewer Helper

    private static void SetEventHandlersForScrollViewer(ScrollViewer scroller) 
    {
        scroller.PreviewMouseWheel += new MouseWheelEventHandler(ScrollViewerPreviewMouseWheel);
        scroller.PreviewKeyDown += new KeyEventHandler(ScrollViewerPreviewKeyDown);
    }

    #endregion

    #region scrollerLoaded Event Handler

    private static void scrollerLoaded(object sender, RoutedEventArgs e)
    {
        ScrollViewer scroller = sender as ScrollViewer;

        SetEventHandlersForScrollViewer(scroller);
    }

    #endregion

    #region listboxLoaded Event Handler

    private static void listboxLoaded(object sender, RoutedEventArgs e)
    {
        ListBox listbox = sender as ListBox;

        _listBoxScroller = FindVisualChildHelper.GetFirstChildOfType<ScrollViewer>(listbox);
        SetEventHandlersForScrollViewer(_listBoxScroller);

        SetTimeDuration(_listBoxScroller, new TimeSpan(0, 0, 0, 0, 200));
        SetPointsToScroll(_listBoxScroller, 16.0);

        listbox.SelectionChanged += new SelectionChangedEventHandler(ListBoxSelectionChanged);
        listbox.Loaded += new RoutedEventHandler(ListBoxLoaded);
        listbox.LayoutUpdated += new EventHandler(ListBoxLayoutUpdated);
    }

    #endregion

    #region ScrollViewerPreviewMouseWheel Event Handler

    private static void ScrollViewerPreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
        double mouseWheelChange = (double)e.Delta;
        ScrollViewer scroller = (ScrollViewer)sender;
        double newVOffset = GetVerticalOffset(scroller) - (mouseWheelChange / 3);

        if (newVOffset < 0)
        {
            AnimateScroll(scroller, 0);
        }
        else if (newVOffset > scroller.ScrollableHeight)
        {
            AnimateScroll(scroller, scroller.ScrollableHeight);
        }
        else
        {
            AnimateScroll(scroller, newVOffset);
        }

        e.Handled = true;
    }

    #endregion

    #region ScrollViewerPreviewKeyDown Handler

    private static void ScrollViewerPreviewKeyDown(object sender, KeyEventArgs e)
    {
        ScrollViewer scroller = (ScrollViewer)sender;

        Key keyPressed = e.Key;
        double newVerticalPos = GetVerticalOffset(scroller);
        bool isKeyHandled = false;

        if (keyPressed == Key.Down)
        {
            newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + GetPointsToScroll(scroller)), Orientation.Vertical);
            isKeyHandled = true;
        }
        else if (keyPressed == Key.PageDown)
        {
            newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + scroller.ViewportHeight), Orientation.Vertical);
            isKeyHandled = true;
        }
        else if (keyPressed == Key.Up)
        {
            newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - GetPointsToScroll(scroller)), Orientation.Vertical);
            isKeyHandled = true;
        }
        else if (keyPressed == Key.PageUp)
        {
            newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - scroller.ViewportHeight), Orientation.Vertical);
            isKeyHandled = true;
        }

        if (newVerticalPos != GetVerticalOffset(scroller))
        {
            AnimateScroll(scroller, newVerticalPos);
        }

        e.Handled = isKeyHandled;
    }

    #endregion

    #region ListBox Event Handlers

    private static void ListBoxLayoutUpdated(object sender, EventArgs e)
    {
        UpdateScrollPosition(sender);
    }

    private static void ListBoxLoaded(object sender, RoutedEventArgs e)
    {
        UpdateScrollPosition(sender);
    }

    private static void ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        UpdateScrollPosition(sender);
    }

    #endregion
}

Ответ 2

Лучший пример настройки прокрутки можно найти в статье Sacha Barber в проекте Code. См. Эту статью статью о проекте проекта прокрутки трения на эту тему.

Ряд кода WPA Sacha Barbers был интегрирован в проект Github для WPF. См. MahaApps Metro для некоторых очень полезных реализаций WPF с открытым исходным кодом.

Ответ 3

Для тех, кто попадает сюда из Google, Анатолий работает, но у него есть некоторые проблемы с прокруткой колесика мыши.

Прокрутка без исправления (имейте в виду, что я пытаюсь быстро прокрутить до конца)

Прокрутка с исправлением

(самостоятельно подключите, вы можете узнать, чем закончилось это приложение здесь)

Давайте посмотрим, почему.

#region ScrollViewerPreviewMouseWheel Event Handler

private static void ScrollViewerPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
    double mouseWheelChange = (double)e.Delta;
    ScrollViewer scroller = (ScrollViewer)sender;
    double newVOffset = GetVerticalOffset(scroller) - (mouseWheelChange / 3);

    if (newVOffset < 0)
    {
        AnimateScroll(scroller, 0);
    }
    else if (newVOffset > scroller.ScrollableHeight)
    {
        AnimateScroll(scroller, scroller.ScrollableHeight);
    }
    else
    {
        AnimateScroll(scroller, newVOffset);
    }

    e.Handled = true;
}

В этом коде обработчика вы заметите, что он вызывается каждый раз, когда вы прокручиваете колесо мыши. Поэтому, когда вы нажимаете на нее с быстрой прокруткой, анимация не успевает завершиться, и вы застреваете, пытаясь прокрутить от того места, где вы находитесь в середине анимации. Это вызывает медленную прокрутку при попытке прокрутить быстрее.

Дополнительно их код здесь:

  private static void AnimateScroll(ScrollViewer scrollViewer, double ToValue)
{
    DoubleAnimation verticalAnimation = new DoubleAnimation();

    verticalAnimation.From = scrollViewer.VerticalOffset;
    verticalAnimation.To = ToValue;
    verticalAnimation.Duration = new Duration(GetTimeDuration(scrollViewer));

    Storyboard storyboard = new Storyboard();

    storyboard.Children.Add(verticalAnimation);
    Storyboard.SetTarget(verticalAnimation, scrollViewer);
    Storyboard.SetTargetProperty(verticalAnimation, new PropertyPath(ScrollAnimationBehavior.VerticalOffsetProperty));
    storyboard.Begin();
}

Имеет ненужную реализацию раскадровки, которую можно удалить, чтобы сделать анимацию прокрутки прерываемой, что нам потребуется для сглаживания быстрой прокрутки.

В верхней части их кода мы собираемся добавить новую переменную.

public static class ScrollAnimationBehavior
{
    public static double intendedLocation = 0;
...

Нам нужно сохранить предполагаемое местоположение анимации, чтобы при повторном вызове события скроллера мы могли сразу перейти к этому месту перед началом следующего вызова анимации.

Теперь есть что-то еще, что мы должны изменить. Намеренное расположение требуется обновлять, когда пользователь использует одно из событий нажатия клавиши (Page up или page down) или если он вручную перемещает полосу прокрутки с помощью мыши.

Поэтому нам нужно добавить еще одно событие для обработки левой кнопки мыши на средстве просмотра прокрутки, а когда мышь поднята (помещена в заданное место), мы можем изменить предполагаемое положение, чтобы колесо прокрутки получило обновленное положение.

private static void SetEventHandlersForScrollViewer(ScrollViewer scroller) 
    {
        scroller.PreviewMouseWheel += new MouseWheelEventHandler(ScrollViewerPreviewMouseWheel);
        scroller.PreviewKeyDown += new KeyEventHandler(ScrollViewerPreviewKeyDown);
        scroller.PreviewMouseLeftButtonUp += Scroller_PreviewMouseLeftButtonUp;

    }

private static void Scroller_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        intendedLocation = ((ScrollViewer)sender).VerticalOffset;
    }

Мы все еще должны обновить страницу вверх и страницу вниз, хотя.

        private static void ScrollViewerPreviewKeyDown(object sender, KeyEventArgs e)
    {
        ScrollViewer scroller = (ScrollViewer)sender;

        Key keyPressed = e.Key;
        double newVerticalPos = GetVerticalOffset(scroller);
        bool isKeyHandled = false;

        if (keyPressed == Key.Down)
        {
            newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + GetPointsToScroll(scroller)), Orientation.Vertical);
            intendedLocation = newVerticalPos;
            isKeyHandled = true;
        }
        else if (keyPressed == Key.PageDown)
        {
            newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + scroller.ViewportHeight), Orientation.Vertical);
            intendedLocation = newVerticalPos;
            isKeyHandled = true;
        }
        else if (keyPressed == Key.Up)
        {
            newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - GetPointsToScroll(scroller)), Orientation.Vertical);
            intendedLocation = newVerticalPos;
            isKeyHandled = true;
        }
        else if (keyPressed == Key.PageUp)
        {
            newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - scroller.ViewportHeight), Orientation.Vertical);
            intendedLocation = newVerticalPos;
            isKeyHandled = true;
        }

        if (newVerticalPos != GetVerticalOffset(scroller))
        {
            intendedLocation = newVerticalPos;
            AnimateScroll(scroller, newVerticalPos);
        }

        e.Handled = isKeyHandled;
    }

Теперь, когда мы обработали события, не относящиеся к колесу мыши, для обновления предполагаемого местоположения, давайте исправим событие колеса мыши.

private static void ScrollViewerPreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
        double mouseWheelChange = (double)e.Delta;
        ScrollViewer scroller = (ScrollViewer)sender;
        double newVOffset = intendedLocation - (mouseWheelChange * 2);
        //Incase we got hit by the mouse again. jump to the offset.
        scroller.ScrollToVerticalOffset(intendedLocation);
        if (newVOffset < 0)
        {
            newVOffset = 0;
        }
        if (newVOffset > scroller.ScrollableHeight)
        {
            newVOffset = scroller.ScrollableHeight;
        }

        AnimateScroll(scroller, newVOffset);
        intendedLocation = newVOffset;
        e.Handled = true;
}

Итак, изменения следующие

  1. Изменен newVOffset, чтобы он функционировал из назначенного местоположения и mouseWheelChange.

  2. Очищено, когда newVOffset превышает или находится в допустимых пределах.

  3. Мы перепрыгнули на предназначенный объект, который был создан последним событием прокрутки, с которым он хотел идти.

Если вы хотите изменить "скорость" прокрутки, просто измените

double newVOffset = intendedLocation - (mouseWheelChange * 2);

Вы можете изменить модификатор со времен 2 до 5 для более быстрого или 1 для более медленного.

Теперь, когда все события обработаны, давайте отменим анимацию, чтобы все это имело смысл.

private static void AnimateScroll(ScrollViewer scrollViewer, double ToValue)
{
        scrollViewer.BeginAnimation(VerticalOffsetProperty, null);
        DoubleAnimation verticalAnimation = new DoubleAnimation();
        verticalAnimation.From = scrollViewer.VerticalOffset;
        verticalAnimation.To = ToValue;
        verticalAnimation.Duration = new Duration(GetTimeDuration(scrollViewer));
        scrollViewer.BeginAnimation(VerticalOffsetProperty, verticalAnimation);
}

Поэтому мы удалили раскадровку и аннулировали любую существующую анимацию, чтобы мы могли начать с новой новой.

Ниже приведен полный код (при условии, что он есть), если вам лень его менять, как будто я его просто копировал.

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
using System.Windows.Input;

using ScrollAnimateBehavior.Helpers;

namespace ScrollAnimateBehavior.AttachedBehaviors
{
    public static class ScrollAnimationBehavior
    {
        public static double intendedLocation = 0;

        #region Private ScrollViewer for ListBox

        private static ScrollViewer _listBoxScroller = new ScrollViewer();

        #endregion

        #region VerticalOffset Property

        public static DependencyProperty VerticalOffsetProperty =
            DependencyProperty.RegisterAttached("VerticalOffset",
                                                typeof(double),
                                                typeof(ScrollAnimationBehavior),
                                                new UIPropertyMetadata(0.0, OnVerticalOffsetChanged));

        public static void SetVerticalOffset(FrameworkElement target, double value)
        {
            target.SetValue(VerticalOffsetProperty, value);
        }

        public static double GetVerticalOffset(FrameworkElement target)
        {
            return (double)target.GetValue(VerticalOffsetProperty);
        }

        #endregion

        #region TimeDuration Property

        public static DependencyProperty TimeDurationProperty =
            DependencyProperty.RegisterAttached("TimeDuration",
                                                typeof(TimeSpan),
                                                typeof(ScrollAnimationBehavior),
                                                new PropertyMetadata(new TimeSpan(0, 0, 0, 0, 0)));

        public static void SetTimeDuration(FrameworkElement target, TimeSpan value)
        {
            target.SetValue(TimeDurationProperty, value);
        }

        public static TimeSpan GetTimeDuration(FrameworkElement target)
        {
            return (TimeSpan)target.GetValue(TimeDurationProperty);
        }

        #endregion

        #region PointsToScroll Property

        public static DependencyProperty PointsToScrollProperty =
            DependencyProperty.RegisterAttached("PointsToScroll",
                                                typeof(double),
                                                typeof(ScrollAnimationBehavior),
                                                new PropertyMetadata(0.0));

        public static void SetPointsToScroll(FrameworkElement target, double value)
        {
            target.SetValue(PointsToScrollProperty, value);
        }

        public static double GetPointsToScroll(FrameworkElement target)
        {
            return (double)target.GetValue(PointsToScrollProperty);
        }

        #endregion

        #region OnVerticalOffset Changed

        private static void OnVerticalOffsetChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
        {
            ScrollViewer scrollViewer = target as ScrollViewer;
            if (scrollViewer != null)
            {
                scrollViewer.ScrollToVerticalOffset((double)e.NewValue);
            }
        }

        #endregion

        #region IsEnabled Property

        public static DependencyProperty IsEnabledProperty =
                                                DependencyProperty.RegisterAttached("IsEnabled",
                                                typeof(bool),
                                                typeof(ScrollAnimationBehavior),
                                                new UIPropertyMetadata(false, OnIsEnabledChanged));

        public static void SetIsEnabled(FrameworkElement target, bool value)
        {
            target.SetValue(IsEnabledProperty, value);
        }

        public static bool GetIsEnabled(FrameworkElement target)
        {
            return (bool)target.GetValue(IsEnabledProperty);
        }

        #endregion

        #region OnIsEnabledChanged Changed

        private static void OnIsEnabledChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var target = sender;

            if (target != null && target is ScrollViewer)
            {
                ScrollViewer scroller = target as ScrollViewer;
                scroller.Loaded += new RoutedEventHandler(scrollerLoaded);
            }

            if (target != null && target is ListBox) 
            {
                ListBox listbox = target as ListBox;
                listbox.Loaded += new RoutedEventHandler(listboxLoaded);
            }
        }

        #endregion

        #region AnimateScroll Helper

        private static void AnimateScroll(ScrollViewer scrollViewer, double ToValue)
        {
            scrollViewer.BeginAnimation(VerticalOffsetProperty, null);
            DoubleAnimation verticalAnimation = new DoubleAnimation();
            verticalAnimation.From = scrollViewer.VerticalOffset;
            verticalAnimation.To = ToValue;
            verticalAnimation.Duration = new Duration(GetTimeDuration(scrollViewer));
            scrollViewer.BeginAnimation(VerticalOffsetProperty, verticalAnimation);
        }

        #endregion

        #region NormalizeScrollPos Helper

        private static double NormalizeScrollPos(ScrollViewer scroll, double scrollChange, Orientation o)
        {
            double returnValue = scrollChange;

            if (scrollChange < 0)
            {
                returnValue = 0;
            }

            if (o == Orientation.Vertical && scrollChange > scroll.ScrollableHeight)
            {
                returnValue = scroll.ScrollableHeight;
            }
            else if (o == Orientation.Horizontal && scrollChange > scroll.ScrollableWidth)
            {
                returnValue = scroll.ScrollableWidth;
            }

            return returnValue;
        }

        #endregion

        #region UpdateScrollPosition Helper

        private static void UpdateScrollPosition(object sender)
        {
            ListBox listbox = sender as ListBox;

            if (listbox != null)
            {
                double scrollTo = 0;

                for (int i = 0; i < (listbox.SelectedIndex); i++)
                {
                    ListBoxItem tempItem = listbox.ItemContainerGenerator.ContainerFromItem(listbox.Items[i]) as ListBoxItem;

                    if (tempItem != null)
                    {
                        scrollTo += tempItem.ActualHeight;
                    }
                }

                AnimateScroll(_listBoxScroller, scrollTo);
            }
        }

        #endregion

        #region SetEventHandlersForScrollViewer Helper

        private static void SetEventHandlersForScrollViewer(ScrollViewer scroller) 
        {
            scroller.PreviewMouseWheel += new MouseWheelEventHandler(ScrollViewerPreviewMouseWheel);
            scroller.PreviewKeyDown += new KeyEventHandler(ScrollViewerPreviewKeyDown);
            scroller.PreviewMouseLeftButtonUp += Scroller_PreviewMouseLeftButtonUp;

        }

        private static void Scroller_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            intendedLocation = ((ScrollViewer)sender).VerticalOffset;
        }

        #endregion

        #region scrollerLoaded Event Handler

        private static void scrollerLoaded(object sender, RoutedEventArgs e)
        {
            ScrollViewer scroller = sender as ScrollViewer;

            SetEventHandlersForScrollViewer(scroller);
        }

        #endregion

        #region listboxLoaded Event Handler

        private static void listboxLoaded(object sender, RoutedEventArgs e)
        {
            ListBox listbox = sender as ListBox;

            _listBoxScroller = FindVisualChildHelper.GetFirstChildOfType<ScrollViewer>(listbox);
            SetEventHandlersForScrollViewer(_listBoxScroller);

            SetTimeDuration(_listBoxScroller, new TimeSpan(0, 0, 0, 0, 200));
            SetPointsToScroll(_listBoxScroller, 16.0);

            listbox.SelectionChanged += new SelectionChangedEventHandler(ListBoxSelectionChanged);
            listbox.Loaded += new RoutedEventHandler(ListBoxLoaded);
            listbox.LayoutUpdated += new EventHandler(ListBoxLayoutUpdated);
        }

        #endregion

        #region ScrollViewerPreviewMouseWheel Event Handler

        private static void ScrollViewerPreviewMouseWheel(object sender, MouseWheelEventArgs e)
        {
            double mouseWheelChange = (double)e.Delta;
            ScrollViewer scroller = (ScrollViewer)sender;
            double newVOffset = intendedLocation - (mouseWheelChange * 2);
            //We got hit by the mouse again. jump to the offset.
            scroller.ScrollToVerticalOffset(intendedLocation);
            if (newVOffset < 0)
            {
                newVOffset = 0;
            }
            if (newVOffset > scroller.ScrollableHeight)
            {
                newVOffset = scroller.ScrollableHeight;
            }

            AnimateScroll(scroller, newVOffset);
            intendedLocation = newVOffset;
            e.Handled = true;
        }

        #endregion

        #region ScrollViewerPreviewKeyDown Handler

        private static void ScrollViewerPreviewKeyDown(object sender, KeyEventArgs e)
        {
            ScrollViewer scroller = (ScrollViewer)sender;

            Key keyPressed = e.Key;
            double newVerticalPos = GetVerticalOffset(scroller);
            bool isKeyHandled = false;

            if (keyPressed == Key.Down)
            {
                newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + GetPointsToScroll(scroller)), Orientation.Vertical);
                intendedLocation = newVerticalPos;
                isKeyHandled = true;
            }
            else if (keyPressed == Key.PageDown)
            {
                newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + scroller.ViewportHeight), Orientation.Vertical);
                intendedLocation = newVerticalPos;
                isKeyHandled = true;
            }
            else if (keyPressed == Key.Up)
            {
                newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - GetPointsToScroll(scroller)), Orientation.Vertical);
                intendedLocation = newVerticalPos;
                isKeyHandled = true;
            }
            else if (keyPressed == Key.PageUp)
            {
                newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - scroller.ViewportHeight), Orientation.Vertical);
                intendedLocation = newVerticalPos;
                isKeyHandled = true;
            }

            if (newVerticalPos != GetVerticalOffset(scroller))
            {
                intendedLocation = newVerticalPos;
                AnimateScroll(scroller, newVerticalPos);
            }

            e.Handled = isKeyHandled;
        }

        #endregion

        #region ListBox Event Handlers

        private static void ListBoxLayoutUpdated(object sender, EventArgs e)
        {
            UpdateScrollPosition(sender);
        }

        private static void ListBoxLoaded(object sender, RoutedEventArgs e)
        {
            UpdateScrollPosition(sender);
        }

        private static void ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            UpdateScrollPosition(sender);
        }

        #endregion
    }
}