CustomAnimation自定义动画

clipboard.png

实现效果:
1、X轴上各种缓动动画效果
2、自定义动画代码实现
文件组织:

clipboard.png

一、界面代码

  1. 界面容器为Grid方格下使用Canvas,Canvas承载TextBlock和Button
  2. 动画开始的操作有两种方式,一种为点击Go按钮执行所有动画,一种为单独单击按钮会执行相应按钮动画+第一个Linear按钮动画。
  3. 演示板作为静态资源共享,统一在页面加载触发器,触发条件为上面的2种操作方式。

主要BounceEase、ElasticEase、circle按钮布置代码:

<Canvas Grid.Row="2">
    <TextBlock Canvas.Left="85" Canvas.Top="-16" FontSize="108" Foreground="LightGray">Bounce!</TextBlock>
    <Button Name="easeInButtonBounce" Canvas.Top="10">
      <Button.RenderTransform>
        <TranslateTransform X="0" Y="0"/>
      </Button.RenderTransform>
        Beginning!
    </Button>
    <Button Name="easeOutButtonBounce" Canvas.Top="40">
      <Button.RenderTransform>
        <TranslateTransform X="0" Y="0"/>
      </Button.RenderTransform>
        End!
    </Button>
    <Button Name="easeInOutButtonBounce" Canvas.Top="70">
      <Button.RenderTransform>
        <TranslateTransform X="0" Y="0"/>
      </Button.RenderTransform>
        Both!
    </Button>
</Canvas>

主要演示板资源代码:
弹跳BounceDoubleAnimation注册属性有:EdgeBehavior

<Storyboard x:Key="BounceEaseInTimeline">
  <customAnimations:BounceDoubleAnimation 
      From="0" To="500" Duration="0:0:5" EdgeBehavior="EaseIn" Storyboard.TargetName="easeInButtonBounce" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"/>
</Storyboard>

<Storyboard x:Key="BounceEaseOutTimeline">
  <customAnimations:BounceDoubleAnimation From="0" To="500" Duration="0:0:5" EdgeBehavior="EaseOut" Storyboard.TargetName="easeOutButtonBounce" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"/>
</Storyboard>

<Storyboard x:Key="BounceEaseInOutTimeline">
  <customAnimations:BounceDoubleAnimation From="0" To="500" Duration="0:0:5" EdgeBehavior="EaseInOut" Storyboard.TargetName="easeInOutButtonBounce" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"/>
</Storyboard>

弹簧动画:ElasticDoubleAnimation

<Storyboard x:Key="ElasticEaseInOutTimeline">
  <customAnimations:ElasticDoubleAnimation From="0" To="500" Duration="0:0:5" EdgeBehavior="EaseInOut" Storyboard.TargetName="easeInOutButtonElastic" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"/>
</Storyboard>

圆圈BounceEase动画:
ps:其中注释部分是可以省掉而不影响实现效果。

  1. Circle1动画为沿X轴绕圈运动
  2. Circle2d动画为在X轴上一点原地绕圈运动,然后平移到终点,注意动画X值点与时间线配合。
<Storyboard x:Key="CircleTimeline">
  <DoubleAnimation Duration="0:0:5" From="0" To="500" Storyboard.TargetName="circleButton" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"/>
  <!--<DoubleAnimation Duration="0:0:1.5" From="0" Storyboard.TargetName="circleButton" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"/>-->
  <ParallelTimeline BeginTime="0:0:0" Duration="0:0:4.5">
    <customAnimations:CircleAnimation RepeatBehavior="Forever" Duration="0:0:1" Radius="40" Direction="YDirection" Storyboard.TargetName="circleButton" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"/>
    <customAnimations:CircleAnimation RepeatBehavior="Forever" Duration="0:0:1" Radius="40" Direction="XDirection" Storyboard.TargetName="circleButton" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"/>
  </ParallelTimeline>
  <!--<ParallelTimeline BeginTime="0:0:4.5">
    <DoubleAnimation To="500" Duration="0:0:0.5" Storyboard.TargetName="circleButton" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"/>
    <DoubleAnimation To="0.0" Duration="0:0:0.5" Storyboard.TargetName="circleButton" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"/>
  </ParallelTimeline>-->

</Storyboard>

<Storyboard x:Key="CircleTimeline2">
  <DoubleAnimation Duration="0:0:1.5" From="0" To="290" Storyboard.TargetName="circleButton2" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"/>
  <!--<DoubleAnimation Duration="0:0:1.5" From="0" Storyboard.TargetName="circleButton2" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"/>-->
  <ParallelTimeline BeginTime="0:0:1.5" Duration="0:0:2">
    <customAnimations:CircleAnimation RepeatBehavior="Forever" Duration="0:0:0.25" Radius="40" Direction="YDirection" Storyboard.TargetName="circleButton2" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"/>
    <customAnimations:CircleAnimation RepeatBehavior="Forever" Duration="0:0:0.25" Radius="40" Direction="XDirection" Storyboard.TargetName="circleButton2" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"/>
  </ParallelTimeline>
  <ParallelTimeline BeginTime="0:0:3.5">
    <DoubleAnimation To="500" Duration="0:0:1.5" Storyboard.TargetName="circleButton2" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"/>
    <!--<DoubleAnimation To="0.0" Duration="0:0:0.5" Storyboard.TargetName="circleButton2" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"/>-->
  </ParallelTimeline>
</Storyboard>

  </Window.Resources>

统一触发代码:

<Window.Triggers>
    <EventTrigger RoutedEvent="Button.Click" SourceName="goButton">
      <BeginStoryboard Storyboard="{StaticResource LinearTimeline}"/>
        <BeginStoryboard Storyboard="{StaticResource AccelTimeline}"/>
        <BeginStoryboard Storyboard="{StaticResource DecelTimeline}"/>
        <BeginStoryboard Storyboard="{StaticResource AccelDecelTimeline}"/>
        <BeginStoryboard Storyboard="{StaticResource BounceEaseInTimeline}"/>
        
<EventTrigger RoutedEvent="ButtonBase.Click" SourceName="linearButton">
    <BeginStoryboard Storyboard="{StaticResource LinearTimeline}"/>
</EventTrigger>

后台代码:
本实例自定义实现缓动函数EasingFunctionBase的一部分具体子效果,在xaml示例中还可以设置各项缓动效果的属性参数,详细见第2章。
BounceAnimation弹球动画自定义:

  1. Bounce自定义弹球算法:此处不详述,只知道有个大概方法,获取特定公式函数值,然后根据EaseMode来增减量返回
  2. .Net中BounceEase与此原理相识,都可以设置Bounces、Bounciness参数
protected override double GetCurrentValueCore(
    double defaultOriginValue,
    double defaultDestinationValue,
    AnimationClock clock)
{
    double returnValue;
    var start = From ?? defaultOriginValue;
    var delta = To - start ?? defaultOriginValue - start;

    switch (EdgeBehavior)
    {
        case EdgeBehaviorEnum.EaseIn:
            returnValue = EaseIn(clock.CurrentProgress.Value, start, delta, Bounciness, Bounces);
            break;
        case EdgeBehaviorEnum.EaseOut:
            returnValue = EaseOut(clock.CurrentProgress.Value, start, delta, Bounciness, Bounces);
            break;
        default:
            returnValue = EaseInOut(clock.CurrentProgress.Value, start, delta, Bounciness, Bounces);
            break;
    }
    return returnValue;
}

protected override Freezable CreateInstanceCore() => new BounceDoubleAnimation();

private static double EaseOut(double timeFraction, double start, double delta, double bounciness, int bounces)
{
    // math magic: The cosine gives us the right wave, the timeFraction is the frequency of the wave, 
    // the absolute value keeps every value positive (so it "bounces" off the midpoint of the cosine 
    // wave, and the amplitude (the exponent) makes the sine wave get smaller and smaller at the end.
    var returnValue = Math.Abs(Math.Pow((1 - timeFraction), bounciness)
                               *Math.Cos(2*Math.PI*timeFraction*bounces));
    returnValue = delta - (returnValue*delta);
    returnValue += start;
    return returnValue;
}

private static double EaseIn(double timeFraction, double start, double delta, double bounciness, int bounces)
{
    // math magic: The cosine gives us the right wave, the timeFraction is the amplitude of the wave, 
    // the absolute value keeps every value positive (so it "bounces" off the midpoint of the cosine 
    // wave, and the amplitude (the exponent) makes the sine wave get bigger and bigger towards the end.
    var returnValue = Math.Abs(Math.Pow((timeFraction), bounciness)
                               *Math.Cos(2*Math.PI*timeFraction*bounces));
    returnValue = returnValue*delta;
    returnValue += start;
    return returnValue;
}

private static double EaseInOut(double timeFraction, double start, double delta, double bounciness, int bounces)
{
    double returnValue;

    // we cut each effect in half by multiplying the time fraction by two and halving the distance.
    if (timeFraction <= 0.5)
    {
        returnValue = EaseIn(timeFraction*2, start, delta/2, bounciness, bounces);
    }
    else
    {
        returnValue = EaseOut((timeFraction - 0.5)*2, start, delta/2, bounciness, bounces);
        returnValue += delta/2;
    }
    return returnValue;
}

ElasticDoubleAnimation弹簧动画自定义

  1. 弹簧动画,结构方式与上面类似,其属性参与类似ElasticEaseh缓动函数
  2. 算法如下:
protected override double GetCurrentValueCore(double defaultOriginValue, double defaultDestinationValue,
    AnimationClock clock)
{
    double returnValue;
    var start = From ?? defaultOriginValue;
    var delta = To - start ?? defaultOriginValue - start;
    switch (EdgeBehavior)
    {
        case EdgeBehaviorEnum.EaseIn:
            returnValue = EaseIn(clock.CurrentProgress.Value, start, delta, Springiness, Oscillations);
            break;
        case EdgeBehaviorEnum.EaseOut:
            returnValue = EaseOut(clock.CurrentProgress.Value, start, delta, Springiness, Oscillations);
            break;
        default:
            returnValue = EaseInOut(clock.CurrentProgress.Value, start, delta, Springiness, Oscillations);
            break;
    }
    return returnValue;
}

protected override Freezable CreateInstanceCore() => new ElasticDoubleAnimation();

private static double EaseOut(double timeFraction, double start, double delta, double springiness,
    double oscillations)
{
    // math magic: The cosine gives us the right wave, the timeFraction * the # of oscillations is the 
    // frequency of the wave, and the amplitude (the exponent) makes the wave get smaller at the end
    // by the "springiness" factor. This is extremely similar to the bounce equation.
    var returnValue = Math.Pow((1 - timeFraction), springiness)
                      *Math.Cos(2*Math.PI*timeFraction*oscillations);
    returnValue = delta - (returnValue*delta);
    returnValue += start;
    return returnValue;
}

private static double EaseIn(double timeFraction, double start, double delta, double springiness,
    double oscillations)
{
    // math magic: The cosine gives us the right wave, the timeFraction * the # of oscillations is the 
    // frequency of the wave, and the amplitude (the exponent) makes the wave get smaller at the beginning
    // by the "springiness" factor. This is extremely similar to the bounce equation. 
    var returnValue = Math.Pow((timeFraction), springiness)
                      *Math.Cos(2*Math.PI*timeFraction*oscillations);
    returnValue = returnValue*delta;
    returnValue += start;
    return returnValue;
}

private static double EaseInOut(double timeFraction, double start, double delta, double springiness,
    double oscillations)
{
    double returnValue;

    // we cut each effect in half by multiplying the time fraction by two and halving the distance.
    if (timeFraction <= 0.5)
    {
        return EaseIn(timeFraction*2, start, delta/2, springiness, oscillations);
    }
    returnValue = EaseOut((timeFraction - 0.5)*2, start, delta/2, springiness, oscillations);
    returnValue += (delta/2);
    return returnValue;
}
}

CircleAnimation圆圈动画自定义:

  1. 与.NET的CircleEase完全不同,后者无设置参数,而前者有X、Y方向、半径大小属性参数。
  2. 另外是原地圆圈运动,所有不像上述自定义属性From/To等
  3. 同时确定X、Y轴方向动画形成了原地圆圈动画,且看其简约的代码:
/// <summary>
///     CircleAnimation: calculates polar coordinates as a function of time.
///     Use two of these (XDirection and YDirection) to move an element in an elliptical manner
/// </summary>
public class CircleAnimation : DoubleAnimationBase
{
    public enum DirectionEnum
    {
        XDirection,
        YDirection
    }

    public static readonly DependencyProperty DirectionProperty =
        DependencyProperty.Register("Direction", typeof (DirectionEnum), typeof (CircleAnimation),
            new PropertyMetadata(DirectionEnum.XDirection));

    public static readonly DependencyProperty RadiusProperty =
        DependencyProperty.Register("Radius", typeof (double), typeof (CircleAnimation),
            new PropertyMetadata((double) 10));

    /// <summary>
    ///     distance from origin to polar coordinate
    /// </summary>
    public double Radius
    {
        get { return (double) GetValue(RadiusProperty); }
        set
        {
            if (value > 0.0)
            {
                SetValue(RadiusProperty, value);
            }
            else
            {
                throw new ArgumentException("a radius of " + value + " is not allowed!");
            }
        }
    }

    /// <summary>
    ///     are we measuring in the X or Y direction?
    /// </summary>
    public DirectionEnum Direction
    {
        get { return (DirectionEnum) GetValue(DirectionProperty); }
        set { SetValue(DirectionProperty, value); }
    }

    protected override double GetCurrentValueCore(double defaultOriginValue, double defaultDestinationValue,
        AnimationClock clock)
    {
        var time = clock.CurrentProgress.Value;

        // math magic: calculate new coordinates using polar coordinate equations. This requires two 
        // animations to be wired up in order to move in a circle, since we don't make any assumptions
        // about what we're animating (e.g. a TranslateTransform). 
        var returnValue = Direction == DirectionEnum.XDirection
            ? Math.Cos(2*Math.PI*time)
            : Math.Sin(2*Math.PI*time);

        // Need to add the defaultOriginValue so that composition works.
        return returnValue*Radius + defaultOriginValue;
    }

    protected override Freezable CreateInstanceCore() => new CircleAnimation();
}

BackDoubleAnimation后退下动画效果

  1. 同理BackDoubleAnimation自定义动画与BackEase参数不一样,前者只有Amplitude/Suppression,h后者只有Amplitudesh属性。
  2. 完整代码如下:
/// <summary>
///     BackDoubleAnimation: goes in the opposite direction first
/// </summary>
public class BackDoubleAnimation : DoubleAnimationBase
{
    public enum EdgeBehaviorEnum
    {
        EaseIn,
        EaseOut,
        EaseInOut
    }

    public static readonly DependencyProperty EdgeBehaviorProperty =
        DependencyProperty.Register("EdgeBehavior", typeof (EdgeBehaviorEnum), typeof (BackDoubleAnimation),
            new PropertyMetadata(EdgeBehaviorEnum.EaseIn));

    public static readonly DependencyProperty AmplitudeProperty =
        DependencyProperty.Register("Amplitude", typeof (double), typeof (BackDoubleAnimation),
            new PropertyMetadata(4.0));

    public static readonly DependencyProperty SuppressionProperty =
        DependencyProperty.Register("Suppression", typeof (double), typeof (BackDoubleAnimation),
            new PropertyMetadata(2.0));

    public static readonly DependencyProperty FromProperty =
        DependencyProperty.Register("From",
            typeof (double?),
            typeof (BackDoubleAnimation),
            new PropertyMetadata(null));

    public static readonly DependencyProperty ToProperty =
        DependencyProperty.Register("To",
            typeof (double?),
            typeof (BackDoubleAnimation),
            new PropertyMetadata(null));

    /// <summary>
    ///     which side gets the effect
    /// </summary>
    public EdgeBehaviorEnum EdgeBehavior
    {
        get { return (EdgeBehaviorEnum) GetValue(EdgeBehaviorProperty); }
        set { SetValue(EdgeBehaviorProperty, value); }
    }

    /// <summary>
    ///     how much backwards motion is there in the effect
    /// </summary>
    public double Amplitude
    {
        get { return (double) GetValue(AmplitudeProperty); }
        set { SetValue(AmplitudeProperty, value); }
    }

    /// <summary>
    ///     how quickly the effect drops off vs. the entire timeline
    /// </summary>
    public double Suppression
    {
        get { return (double) GetValue(SuppressionProperty); }
        set { SetValue(SuppressionProperty, value); }
    }

    /// <summary>
    ///     Specifies the starting value of the animation.
    /// </summary>
    public double? From
    {
        get { return (double?) GetValue(FromProperty); }
        set { SetValue(FromProperty, value); }
    }

    /// <summary>
    ///     Specifies the ending value of the animation.
    /// </summary>
    public double? To
    {
        get { return (double?) GetValue(ToProperty); }
        set { SetValue(ToProperty, value); }
    }

    protected override double GetCurrentValueCore(double defaultOriginValue, double defaultDestinationValue,
        AnimationClock clock)
    {
        double returnValue;
        var start = From ?? defaultOriginValue;
        var delta = To - start ?? defaultOriginValue - start;
        switch (EdgeBehavior)
        {
            case EdgeBehaviorEnum.EaseIn:
                returnValue = EaseIn(clock.CurrentProgress.Value, start, delta, Amplitude, Suppression);
                break;
            case EdgeBehaviorEnum.EaseOut:
                returnValue = EaseOut(clock.CurrentProgress.Value, start, delta, Amplitude, Suppression);
                break;
            default:
                returnValue = EaseInOut(clock.CurrentProgress.Value, start, delta, Amplitude, Suppression);
                break;
        }
        return returnValue;
    }

    protected override Freezable CreateInstanceCore() => new BackDoubleAnimation();

    private static double EaseOut(double timeFraction, double start, double delta, double amplitude,
        double suppression)
    {
        var frequency = 0.5;

        // math magic: The sine gives us the right wave, the timeFraction * 0.5 (frequency) gives us only half 
        // of the full wave, the amplitude gives us the relative height of the peak, and the exponent makes the 
        // effect drop off more quickly by the "suppression" factor. 
        var returnValue = Math.Pow((timeFraction), suppression)
                          *amplitude*Math.Sin(2*Math.PI*timeFraction*frequency) + timeFraction;
        returnValue = (returnValue*delta);
        returnValue += start;
        return returnValue;
    }

    private static double EaseIn(double timeFraction, double start, double delta, double amplitude,
        double suppression)
    {
        var frequency = 0.5;

        // math magic: The sine gives us the right wave, the timeFraction * 0.5 (frequency) gives us only half 
        // of the full wave (flipped by multiplying by -1 so that we go "backwards" first), the amplitude gives 
        // us the relative height of the peak, and the exponent makes the effect start later by the "suppression" 
        // factor. 
        var returnValue = Math.Pow((1 - timeFraction), suppression)
                          *amplitude*Math.Sin(2*Math.PI*timeFraction*frequency)*-1 + timeFraction;
        returnValue = (returnValue*delta);
        returnValue += start;
        return returnValue;
    }

    private static double EaseInOut(double timeFraction, double start, double delta, double amplitude,
        double suppression)
    {
        double returnValue;

        // we cut each effect in half by multiplying the time fraction by two and halving the distance.
        if (timeFraction <= 0.5)
        {
            return EaseIn(timeFraction*2, start, delta/2, amplitude, suppression);
        }
        returnValue = EaseOut((timeFraction - 0.5)*2, start, delta/2, amplitude, suppression);
        returnValue += (delta/2);
        return returnValue;
    }
}

李志玮
22 声望34 粉丝

求索~~


引用和评论

0 条评论