简体   繁体   English

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

[英]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. 让我们假设我们有一个动画,当用户将光标移到它上面时,将矩形的宽度从50改为150,当用户将光标移开时,将宽度从150改为50 Let the duration of each animation be 1 second . 让每个动画的持续时间为1秒

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. 如果用户将光标移动到矩形上,则等待1秒钟以完成动画,然后将光标移离矩形。 Two animations take 2 seconds totally and their speed is exactly what we are expecting to be. 两部动画完全耗时2秒,速度正是我们所期望的。

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. 但是如果用户在第一个动画(50 - > 150)完成之前将光标移开,则第二个动画的持续时间将为1秒,但速度将非常慢。 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. 我的意思是,第二个动画将动画矩形的宽度不是从150到50,但是从120到5070到50,如果你把光标拉得很快。 But for the same 1 second ! 但是同样的1秒

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. 或者,如果我将FromTo值指定为150和50,则Duration为1秒,矩形宽度将为100 - WPF将自行计算动画完成50%。

I was testing different ways to use animation like Triggers, EventTrigger in style, and VisualStateGroups but no success. 我正在测试使用Triggers,EventTrigger样式和VisualStateGroups等动画的不同方法,但没有成功。

Here is a XAML sample that shows my problem. 这是一个显示我的问题的XAML示例。 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. 如果你想亲眼看看我在说什么,把光标移到矩形上,等待1-2秒(第一个动画完成)然后移开光标。 After that, move the cursor over the rectangle for like 0.25 seconds and move it away. 之后,将光标移动到矩形上0.25秒并将其移开。 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 . 所以让我们想象另一个动画,从100到1100 ,持续时间为2秒 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但不是2秒但是对于:

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

If we stop at 900, duration will be: 如果我们停在900,持续时间将是:

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

And if we let animation finish normally it will be: 如果我们让动画正常完成它将是:

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

Unfortunately, Storyboards are compiled code, meaning you can't use binding in the elements to improve their abilities. 不幸的是,Storyboard是编译代码,意味着你不能在元素中使用绑定来提高他们的能力。

I've had to do something similar and the only way I found to achieve it was to access the storyboard property that needed to be dynamic at the time that I needed to touch it, and set the property as needed in code behind. 我必须做类似的事情,我发现实现它的唯一方法是访问我需要触摸它时需要动态的storyboard属性,并在后面的代码中根据需要设置属性。

So it's not ideal to have to touch and trigger from code behind, but hey look at it this way, it's not business logic, it's strictly presentation layer so code behind is typically acceptable practice. 因此,不得不从代码中触摸和触发它,但是嘿以这种方式看待它,它不是业务逻辑,它是严格的表示层,因此代码隐藏通常是可接受的实践。

So handle your trigger in code behind, calculate your numbers, update the storyboard properties and then start it. 因此,在后面的代码中处理您的触发器,计算您的数字,更新故事板属性然后启动它。 That's probably going to be your best option for now. 这可能是你现在最好的选择。

I would love to see storyboards become part of the logical tree with dependency properties to dynamically modify values, so maybe request the feature :). 我很乐意看到故事板成为具有依赖属性的逻辑树的一部分,以动态修改值,因此可能请求功能:)。

WPF provides the possibility to write custom animations. WPF提供了编写自定义动画的可能性。

You may create one with a MinSpeed property: 您可以使用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);
    }
}

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);
}

An alternative to a custom animation could be an attached property that is bound to the target element's ActualWidth , which dynamically adjusts the animation's Duration . 自定义动画的替代方法可以是绑定到目标元素的ActualWidth的附加属性,该属性动态调整动画的Duration

The attached property could look like this: 附加属性可能如下所示:

public static class AnimationEx
{
    public static readonly DependencyProperty DynamicDurationProperty =
        DependencyProperty.RegisterAttached(
            "DynamicDuration", typeof(double), typeof(AnimationEx),
            new PropertyMetadata(DynamicDurationPropertyChanged));

    public static double GetDynamicDuration(DoubleAnimation animation)
    {
        return (double)animation.GetValue(DynamicDurationProperty);
    }

    public static void SetDynamicDuration(DoubleAnimation animation, double value)
    {
        animation.SetValue(DynamicDurationProperty, value);
    }

    private static void DynamicDurationPropertyChanged(
        DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        var animation = o as DoubleAnimation;
        if (animation != null && animation.To.HasValue)
        {
            animation.Duration = TimeSpan.FromSeconds(
                Math.Abs((double)e.NewValue - animation.To.Value) / 100);
        }
    }
}

where there might be another attached property for the speed factor, which is hardcoded as 100 above. 其中可能有另一个附加属性的速度因子,硬编码为100以上。

The property would be used like this: 该属性将使用如下:

<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" To="50"
                    local:AnimationEx.DynamicDuration="{Binding Path=ActualWidth,
                        RelativeSource={RelativeSource AncestorType=Rectangle}}" />
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</Rectangle.Triggers>

A possible drawback might be the fact that the Duration is constantly reset when the ActualWidth changes. 可能的缺点可能是,当ActualWidth发生变化时,持续时间会不断重置。 However, I could not notify any visual effect. 但是,我无法通知任何视觉效果。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM