Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make the duration of an animation dynamic?

Let's imagine we have an animation that changes a Rectangle's width from 50 to 150 when user moves the cursor over it and back from 150 to 50 when user moves the cursor away. Let the duration of each animation be 1 second.

Everything is OK if user moves the cursor over the rectangle, then waits for 1 second for animation to finish, and then moves the cursor away from the rectangle. Two animations take 2 seconds totally and their speed is exactly what we are expecting to be.

But if user moves the cursor away before the first animation (50 -> 150) is finished, the second animation's duration will be 1 second but the speed of it will be very slow. I mean, the second animation will animate the rectangle's width not from 150 to 50 but from let's say 120 to 50 or from 70 to 50 if you take cursor away very fast. But for the same 1 second!

So what I want to understand is how to make the duration of the "backwards" animation to be dynamic. Dynamic based on at what point the first animation was stopped. Or, if I specify From and To values to 150 and 50, Duration to 1 seconds and rectangle width will be 100 – WPF would calculate by itself that animation is 50% done.

I was testing different ways to use animation like Triggers, EventTrigger in style, and VisualStateGroups but no success.

Here is a XAML sample that shows my problem. If you want to see by yourself what I am talking about, move the cursor over the rectangle, wait for 1-2 seconds (first animation is finished) then move the cursor away. After that, move the cursor over the rectangle for like 0.25 seconds and move it away. You will see that the rectangle's width changes very slowly.

<Rectangle Width="50"
           Height="100"
           HorizontalAlignment="Left"
           Fill="Black">
<Rectangle.Triggers>
    <EventTrigger RoutedEvent="MouseEnter">
        <BeginStoryboard>
            <Storyboard>
                <DoubleAnimation Storyboard.TargetProperty="Width"
                                 To="150"
                                 Duration="0:0:1" />
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
    <EventTrigger RoutedEvent="MouseLeave">
        <BeginStoryboard>
            <Storyboard>
                <DoubleAnimation Storyboard.TargetProperty="Width" />
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</Rectangle.Triggers>

Also, I would like to explain what calculation I am expecting to see. So let's imagine another animation that goes from 100 to 1100 and has a duration of 2 seconds. If we stop it at the point of 300, it must start to go back to 100 but not for 2 seconds but for:

((300 - 100) / (1100 - 100)) * 2s = 0.4s

If we stop at 900, duration will be:

((900 - 100) / (1100 - 100)) * 2s = 1.6s

And if we let animation finish normally it will be:

((1100 - 100) / (1100 - 100)) * 2s = 2s
like image 516
Yurii Avatar asked Nov 08 '22 01:11

Yurii


1 Answers

WPF provides the possibility to write custom animations.

You may create one with a MinSpeed property:

public class MinSpeedDoubleAnimation : DoubleAnimation
{
    public static readonly DependencyProperty MinSpeedProperty =
        DependencyProperty.Register(
            nameof(MinSpeed), typeof(double?), typeof(MinSpeedDoubleAnimation));

    public double? MinSpeed
    {
        get { return (double?)GetValue(MinSpeedProperty); }
        set { SetValue(MinSpeedProperty, value); }
    }

    protected override Freezable CreateInstanceCore()
    {
        return new MinSpeedDoubleAnimation();
    }

    protected override double GetCurrentValueCore(
        double defaultOriginValue, double defaultDestinationValue,
        AnimationClock animationClock)
    {
        var destinationValue = To ?? defaultDestinationValue;
        var originValue = From ?? defaultOriginValue;
        var duration = Duration != Duration.Automatic ? Duration :
            animationClock.NaturalDuration;
        var speed = (destinationValue - originValue) / duration.TimeSpan.TotalSeconds;

        if (MinSpeed.HasValue && Math.Abs(speed) < MinSpeed)
        {
            speed = Math.Sign(speed) * MinSpeed.Value;
        }

        var value = originValue + speed * animationClock.CurrentTime.Value.TotalSeconds;

        return speed > 0 ?
            Math.Min(destinationValue, value) :
            Math.Max(destinationValue, value);
    }
}

and use it like this:

<EventTrigger RoutedEvent="MouseEnter">
    <BeginStoryboard>
        <Storyboard>
            <DoubleAnimation
                Storyboard.TargetProperty="Width"
                To="150" Duration="0:0:1" />
        </Storyboard>
    </BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
    <BeginStoryboard>
        <Storyboard>
            <local:MinSpeedDoubleAnimation
                Storyboard.TargetProperty="Width"
                To="50" MinSpeed="100"/>
        </Storyboard>
    </BeginStoryboard>
</EventTrigger>

For completeness here is also the most simple code-behind solution without any Triggers and Storyboards:

<Rectangle Width="50" Height="100" HorizontalAlignment="Left" Fill="Black" 
           MouseEnter="RectangleMouseEnter" MouseLeave="RectangleMouseLeave"/>

with these event handlers:

private void RectangleMouseEnter(object sender, MouseEventArgs e)
{
    var element = (FrameworkElement)sender;
    var animation = new DoubleAnimation(150, TimeSpan.FromSeconds(1));
    element.BeginAnimation(FrameworkElement.WidthProperty, animation);
}

private void RectangleMouseLeave(object sender, MouseEventArgs e)
{
    var element = (FrameworkElement)sender;
    var duration = (element.ActualWidth - 50) / 100;
    var animation = new DoubleAnimation(50, TimeSpan.FromSeconds(duration));
    element.BeginAnimation(FrameworkElement.WidthProperty, animation);
}
like image 199
Clemens Avatar answered Nov 14 '22 22:11

Clemens