如何使动画的持续时间动态化?

Yur*_*rii 5 .net c# wpf animation

假设我们有一个动画,当用户将光标移到矩形上方时,该动画将矩形的宽度从50更改为 150,并在用户将光标移开时将其宽度从150更改为 50。让每个动画的持续时间为1 秒

如果用户将光标移动到矩形上,然后等待 1 秒动画完成,然后将光标移离矩形,则一切正常。两个动画总共需要 2 秒,它们的速度正是我们所期望的。

但是如果用户在第一个动画 (50 -> 150) 完成之前将光标移开,则第二个动画的持续时间将为 1 秒,但速度会非常慢。我的意思是,第二个动画将动画矩形的宽度不是从150 到 50,而是从120 到 50或从70 到 50,如果您非常快速地将光标移开。但是同样的1秒

所以我想了解的是如何使“向后”动画的持续时间成为动态的。基于第一个动画停止的点的动态。或者,如果我指定FromTo值150和50,Duration为1秒,矩形宽度为100 - WPF将由自身计算出动画50%完成。

我正在测试使用动画的不同方法,如触发器、EventTrigger in style 和 VisualStateGroups,但没有成功。

这是一个显示我的问题的 XAML 示例。如果您想自己看看我在说什么,请将光标移到矩形上方,等待 1-2 秒(第一个动画完成)然后将光标移开。之后,将光标移到矩形上方 0.25 秒,然后将其移开。您将看到矩形的宽度变化非常缓慢。

<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>
Run Code Online (Sandbox Code Playgroud)

另外,我想解释一下我期望看到的计算。所以让我们想象另一个动画,从100 到 1100,持续时间为2 秒。如果我们在300点停止它,它必须开始回到100但不是 2 秒,而是:

((300 - 100) / (1100 - 100)) * 2s = 0.4s
Run Code Online (Sandbox Code Playgroud)

如果我们停在 900,持续时间将是:

((900 - 100) / (1100 - 100)) * 2s = 1.6s
Run Code Online (Sandbox Code Playgroud)

如果我们让动画正常完成,它将是:

((1100 - 100) / (1100 - 100)) * 2s = 2s
Run Code Online (Sandbox Code Playgroud)

Cle*_*ens 1

WPF 提供了编写自定义动画的可能性。

您可以创建一个具有MinSpeed以下属性的属性:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

并像这样使用它:

<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>
Run Code Online (Sandbox Code Playgroud)

为了完整起见,这里也是最简单的代码隐藏解决方案,没有任何触发器和故事板:

<Rectangle Width="50" Height="100" HorizontalAlignment="Left" Fill="Black" 
           MouseEnter="RectangleMouseEnter" MouseLeave="RectangleMouseLeave"/>
Run Code Online (Sandbox Code Playgroud)

使用这些事件处理程序:

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);
}
Run Code Online (Sandbox Code Playgroud)