WPF的动画实现方式

过去实现动画的经典方法是建立一个定时器,然后根据其频率循环调用回调函数或者一个事件处理函数。在这个函数中可以手动更新目标属性,直到达到最终值,这时可以停止定时器。这就是基于计时器的动画。WPF中也提供了DispatcherTimer类型的定时器,可以通过该类实现这样的方案。如下代码定义了一动画改变长方形的宽度:
<Window x:Class="DispatcherTimer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="7*"/>
            <RowDefinition Height="3*"/>
        </Grid.RowDefinitions>
        <Rectangle Grid.Row="0" x:Name="rectangle" Width="30" Height="100" Fill="Blue">           
        </Rectangle>
        <Button Grid.Row="1" x:Name="buttonStart" Width="100" Height="30" Click="buttonStart_Click">
            开始动画
        </Button>
    </Grid>
</Window>
C#代码为:
public partial class MainWindow : Window
    {
        private double maxWidth = 200;  //最大的宽度
        private double startWidth = 0;  //开始的宽度
        public MainWindow()
        {
            InitializeComponent();
            startWidth = rectangle.Width;
        }

        private void buttonStart_Click(object sender, RoutedEventArgs e)
        {
            //创建DispatcherTimer对象
            System.Windows.Threading.DispatcherTimer tmr = new System.Windows.Threading.DispatcherTimer();
            //设置间隔时间
            tmr.Interval = TimeSpan.FromSeconds(0.01);
            //绑定函数
            tmr.Tick += new EventHandler(tmr_Tick);
            tmr.Start();//启动计时器
        }

        void tmr_Tick(object sender, EventArgs e)
        {
            rectangle.Width += 1;
            if (rectangle.Width >= maxWidth)
            {
                rectangle.Width = startWidth;
                (sender as System.Windows.Threading.DispatcherTimer).Stop();
            }
        }
    }

当单击开始动画这个按钮的时候,长方形的宽度会逐渐变宽,效果图如下:

                  

现在,WPF提供了一种基于帧的动画实现方式,由CompositonTarget类来完成。它提供了一个回调函数(Rendering的事件处理函数),WPF会在每次界面刷新时调用该回调函数。CompositionTarget的刷新频率与窗体保持一致,因此很难人工控制动画的快慢。在动画中,可以使用移动的元素、颜色变化、变换等制作平滑的效果。WPF使动画的制作非常简单,还可以连续改变任意依赖属性的值。动画的主要元素有:

1.时间轴——定义了值随时间的变化方式。有不同类型的时间轴,可用于改变不同类型的值。所有时间轴的基类都是Timeline。为了连续改变double的值,可以使用DoubleAnimation类。Int32Animation类是int值的动画类。PointAnimation类用于连续改变点,ColorAnimation类用于连续改变颜色。

2.故事板——用于合并动画。Storyboard派生自基类TimelineGroup,TimelineGroup又派生自基类Timeline。使用Storyboard类可以合并所有应连接在一起的动画。

3.触发器——通过触发器可以启动和停止动画。我们可以创建事件触发器,当事件发生时,事件触发器就会激活。

如果想要顺利地使用WPF动画,需要满足以下条件:

1.改变的属性必须是依赖属性。

2.应用动画的属性所属的类必须派生自DependencyObject,而且实现了IAnimation接口。由于WPF绝大多数类都会派生自DependencyObject,所以实现IAnimation接口主要有3个类,即UIElement、ContentElement和Animatable,应用动画属性所属的类必然派生自这3个类。

3.该属性的类型必须是可以应用动画的类型,如Double、Int、Point或者Color。例如Window类型就不是一个可以应用动画的类型,WPF针对22种基本类型提供了相应的动画类。如果希望一些类型可以应用动画,则需要扩展动画类。

一、时间轴

时间轴定义了值随时间变化的方式。下面的示例连续改变椭圆的大小。在代码中,DoubleAnimation用来改变椭圆的宽度,ColorAnimation用来连续改变填充椭圆的颜色。把Ellipse类的Triggers属性设置为EventTrigger,椭圆加载时,就激活用EventTrigger的RoutedEvent属性定义的事件触发器。BeginStoryboard是启动故事板的触发器动作。这个动画在3秒内把椭圆的宽度从100改为300,在之后的3秒再恢复原状。

<Ellipse Height="50" Width="100">
            <Ellipse.Fill>
                <SolidColorBrush x:Name="ellipseBrush" Color="SteelBlue"/>
            </Ellipse.Fill>
            <Ellipse.Triggers>
                <EventTrigger RoutedEvent="Ellipse.Loaded">
                    <EventTrigger.Actions>
                        <BeginStoryboard>
                            <Storyboard Duration="00:00:06" RepeatBehavior="Forever">
                                <DoubleAnimation Storyboard.TargetProperty="(Ellipse.Width)" 
                                                 Duration="0:0:3" AutoReverse="True" FillBehavior="Stop"
                                                 RepeatBehavior="Forever" AccelerationRatio="0.9" 
                                                 DecelerationRatio="0.1" From="100" To="300">
                                    <DoubleAnimation.EasingFunction>
                                        <BounceEase EasingMode="EaseInOut"/>
                                    </DoubleAnimation.EasingFunction>
                                </DoubleAnimation>
                                <ColorAnimation Storyboard.TargetName="ellipseBrush" Storyboard.TargetProperty="(SolidColorBrush.Color)"
                                                Duration="0:0:3" AutoReverse="True" FillBehavior="Stop"
                                                RepeatBehavior="Forever" From="Yellow" To="Red"/>
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger.Actions>
                </EventTrigger>
            </Ellipse.Triggers>
        </Ellipse>
如下图为动画效果:

下面的示例说明了如何在样式中定义动画。在Windows资源中,有一个用于按钮的AnimationButtonStyle样式。在模板中定义一个矩形outline,这个模型使用很细的笔触,把其宽度设置为0.4.该模板为IsMouseOver属性定义了一个属性触发器。当鼠标移过按钮时,就应用这个按钮的EnterActions属性。启动动作的是BeginStoryboard,它是一个触发器动作,可以包含并启动Storyboard元素。当IsMouseOver事件被触发时,这个按钮四周的边框粗细就会平滑地增加一个指定的值,这个值是由By属性指定。由于设置了AutoReverse="True",当动画结束时,边框的粗细将重置为其初始值。

<Window x:Class="动画.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <Style x:Key="AnimatedButtonStyle" TargetType="{x:Type Button}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type Button}">
                        <Grid>
                            <Rectangle Name="outline" RadiusX="9" RadiusY="9" Stroke="Black"
                                       Fill="{TemplateBinding Background}" StrokeThickness="1.6"></Rectangle>
                            <ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Center"/>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Trigger.EnterActions>
                                    <BeginStoryboard>
                                        <Storyboard>
                                            <DoubleAnimation Duration="0:0:0.3" AutoReverse="True" Storyboard.TargetProperty="(Rectangle.StrokeThickness)"
                                                             Storyboard.TargetName="outline" By="1.2"/>
                                        </Storyboard>
                                    </BeginStoryboard>
                                </Trigger.EnterActions>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid>      
        <Button Style="{StaticResource ResourceKey=AnimatedButtonStyle}" Width="200" Height="100" Content="Click Me!" Margin="152,12,151,199" />
    </Grid>
</Window>
根据Timeline的类型,还可以使用其它的一些属性。例如,使用DoubleAnimation,可以为动画的开始和结束设置From和To属性,还可以设置By属性,用Bound属性的当前值启动动画,该属性递增由By属性指定的值。

二、非线性动画

定义非线性动画的一种方式是设置AccelerationRatio和DecelerationRatio,指定动画在开始和结束的速度。几个动画类有EasingFunction属性,这个属性接受一个实现了IEasingFunction接口的对象。通过这个接口,缓动函数对象可以定义值随着时间如何变化动画效果。有几个缓动函数可用于创建非线性动画,如ExponentialEase,它给动画使用指数公式;QuadraticEase、CubicEase、QuarticEase和QuinticEase的指数分别是2、3、4、5,PowerEase的指数是可以配置的。特别有趣的是SineEase,它使用正弦曲线,BounceEase创建弹跳效果,ElasticEase用弹簧的来回震荡来模拟动画值。要在XAML中指定这种缓动效果时,具体方法是把该缓动效果添加到动画的EasingFunction属性中。添加不同的函数,就会看到不同的有趣的效果。如下代码:

      <DoubleAnimation.EasingFunction>
           <BounceEase EasingMode="EaseInOut"/>
      </DoubleAnimation.EasingFunction>

三、事件触发器

事件触发器在事件发生时激活,这种事件的例子包括控件的Load事件、按钮的Click事件,以及MouseMove事件等。先看代码,下面的例子通过形状创建一个笑脸,然后给这个笑脸创建一个动画,一旦点击按钮时,这个笑脸的眼睛就会移动。

<Window x:Class="会动的笑脸.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <DockPanel>
            <DockPanel.Triggers>
                <EventTrigger RoutedEvent="Button.Click" SourceName="buttonBeginMoveEyes">
                    <BeginStoryboard x:Name="beginMoveEyes">
                        <Storyboard>
                            <DoubleAnimation RepeatBehavior="Forever" DecelerationRatio="0.8"
                                             AutoReverse="True" By="6" Duration="0:0:1" 
                                             Storyboard.TargetName="eyeLeft"
                                             Storyboard.TargetProperty="(Canvas.Left)"/>
                            <DoubleAnimation RepeatBehavior="Forever"
                                             AutoReverse="True" By="6" Duration="0:0:5" 
                                             Storyboard.TargetName="eyeLeft"
                                             Storyboard.TargetProperty="(Canvas.Top)"/>
                            <DoubleAnimation RepeatBehavior="Forever" DecelerationRatio="0.8"
                                             AutoReverse="True" By="-6" Duration="0:0:3" 
                                             Storyboard.TargetName="eyeRight"
                                             Storyboard.TargetProperty="(Canvas.Left)"/>
                            <DoubleAnimation RepeatBehavior="Forever" DecelerationRatio="0.8"
                                             AutoReverse="True" By="6" Duration="0:0:6" 
                                             Storyboard.TargetName="eyeRight"
                                             Storyboard.TargetProperty="(Canvas.Top)"/>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
                <EventTrigger RoutedEvent="Button.Click" SourceName="buttonStopMoveEyes">
                    <StopStoryboard BeginStoryboardName="beginMoveEyes"/>
                </EventTrigger>
                <EventTrigger RoutedEvent="Button.Click" SourceName="buttonResize">
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation RepeatBehavior="2" AutoReverse="True"
                                             Storyboard.TargetName="scale1"
                                             Storyboard.TargetProperty="(ScaleTransform.ScaleX)"
                                             From="0.1" To="6" Duration="0:0:5">
                                <DoubleAnimation.EasingFunction>
                                    <ElasticEase/>
                                </DoubleAnimation.EasingFunction>
                            </DoubleAnimation>
                            <DoubleAnimation RepeatBehavior="2" AutoReverse="True"
                                             Storyboard.TargetName="scale1"
                                             Storyboard.TargetProperty="(ScaleTransform.ScaleY)"
                                             From="0.1" To="6" Duration="0:0:5">
                                <DoubleAnimation.EasingFunction>
                                    <BounceEase/>
                                </DoubleAnimation.EasingFunction>
                            </DoubleAnimation>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </DockPanel.Triggers>
            <StackPanel Orientation="Vertical" DockPanel.Dock="Top">
                <Button x:Name="buttonBeginMoveEyes" Content="Start Move Eyes" Margin="5"/>
                <Button x:Name="buttonStopMoveEyes" Content="Stop Move Eyes" Margin="5"/>
                <Button x:Name="buttonResize" Content="Resize" Margin="5"/>
            </StackPanel>
            <Canvas>
                <Canvas.LayoutTransform>
                    <ScaleTransform x:Name="scale1" ScaleX="1" ScaleY="1"/>
                </Canvas.LayoutTransform>
                <Ellipse Canvas.Left="10" Canvas.Top="10" Width="100" Height="100"
                         Stroke="Blue" StrokeThickness="4" Fill="Yellow"/>
                <Ellipse Canvas.Left="30" Canvas.Top="12" Width="60" Height="30">
                    <Ellipse.Fill>
                        <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                            <GradientStop Offset="0.1" Color="DarkGreen"/>
                            <GradientStop Offset="0.7" Color="Transparent"/>
                        </LinearGradientBrush>
                    </Ellipse.Fill>
                </Ellipse>
                <Ellipse Canvas.Left="30" Canvas.Top="35" Width="25" Height="20" 
                         Stroke="Blue" StrokeThickness="3" Fill="White"/>
                <Ellipse x:Name="eyeLeft" Canvas.Left="40" Canvas.Top="43" 
                         Width="6" Height="5" Fill="Black"/>
                <Ellipse Canvas.Left="65" Canvas.Top="35" Width="25" Height="20"
                         Stroke="Blue" StrokeThickness="3" Fill="White"/>
                <Ellipse x:Name="eyeRight" Canvas.Left="75" Canvas.Top="43" Width="6"
                         Height="5" Fill="Black"/>
                <Path Name="mouth" Stroke="Blue" StrokeThickness="4" Data="M 40,74 Q 57,95 80,74"/>
            </Canvas>
        </DockPanel>
    </Grid>
</Window>
在上面代码中,动画在DockPanel.Triggers部分中定义,这里没有使用属性触发器,而使用了事件触发器。RoutedEvent和SourceName属性定义了buttonBeginMoveEyes按钮,一旦该按钮的Click事件发生,就激活第一个事件触发器。触发器动作由BeginStoryboard元素定义,它启动所包含的Storyboard。BeginStoryboard定义了一个名称,它用于控制故事板的暂停、继续和停止动作。Storyboard元素包含4个动画,前两幅动画连续改变左眼,后两幅动画连续改变右眼。第1幅和第3幅动画改变眼睛的Canvas.Left位置,第2幅和第第4幅改变Canvas.Top。动画在x和y方向上有不同的时间值,使用指定的重复行为使眼睛的运动更有趣。一旦buttonStopMoveEyes按钮的Click事件发生,就激活第2个事件触发器,在这里,故事板用StopStoryboard元素停止,该元素引用了起始故事板beginMoveEyes。第3个个事件触发器通过单击buttonResize按钮来激活,在这幅动画中改变了Canvas元素的变换。这个故事板也使用前面介绍的EaseFunction。

如下图为上述代码的两个效果:

<点击Start Move Eyes之后的效果>


<点击Resize按钮之后的效果>



四、关键帧动画

如前所述,使用加速比、减速比和缓函数,就可以用非线性的方式 制作动画。如果需要为动画指定几个值,就可以使用关键帧动画。与正常的动画一样,关键帧动画也有不同的动画类型,它们可以改变不同类型的属性。DoubleAnimationUsingKeyFrames是双精度类型的关键帧动画。其它关键帧动画类型有Int32AnimationUsingKeyFrames、PointAnimationUsingKdyFrames、ColorAnimationUsingKeyFrames、SizeAnimationUsingKeyFrames和ObjectAnimationUsingKeyFrames。

<Window x:Class="关键帧动画.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Canvas>
            <Ellipse Fill="Red" Canvas.Top="20" Canvas.Left="20" Width="25" Height="25" >
                <Ellipse.RenderTransform>
                    <TranslateTransform X="50" Y="50" x:Name="ellipseMove" />
                </Ellipse.RenderTransform>
                <Ellipse.Triggers>
                    <EventTrigger RoutedEvent="Ellipse.Loaded">
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="X" 
                                                               Storyboard.TargetName="ellipseMove">
                                    <LinearDoubleKeyFrame KeyTime="0:0:2" Value="30"/>
                                    <DiscreteDoubleKeyFrame KeyTime="0:0:4" Value="80"/>
                                    <SplineDoubleKeyFrame KeySpline="0.5,0.0 0.9,0.0" KeyTime="0:0:10" Value="300"/>
                                    <LinearDoubleKeyFrame KeyTime="0:0:20" Value="150"/>
                                </DoubleAnimationUsingKeyFrames>
                                <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="Y" 
                                                               Storyboard.TargetName="ellipseMove">
                                    <SplineDoubleKeyFrame KeySpline="0.5,0.0 0.9,0.0" KeyTime="0:0:2" Value="50"/>
                                    <EasingDoubleKeyFrame KeyTime="0:0:20" Value="300">
                                        <EasingDoubleKeyFrame.EasingFunction>
                                            <BounceEase/>
                                        </EasingDoubleKeyFrame.EasingFunction>
                                    </EasingDoubleKeyFrame>
                                </DoubleAnimationUsingKeyFrames>
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>
                </Ellipse.Triggers>
            </Ellipse>
        </Canvas>
    </Grid>
</Window>
上述代码中使用了两个关键帧动画。第一幅使用一个LinearKeyFrame、一个DiscreteDoubleKeyFrame和一个SplineDoubleKeyFrame,第二幅是一个EasingDoubleKeyFrame。LinearDoubleKeyFrame使对应值线性变化,KeyTime属性定义了动画应在何时达到Value属性的值。这里LinearDoubleKeyFrame用3秒时间使X属性的值到达30,DiscreteDoubleKeyFrame在4秒后立即改变为新值。SplineDoubleKeyFrame使用贝塞尔曲线,其中的两个控制点由KeySpline属性指定。EasingDoubleKeyFrame是.Net4中新增的一个帧类,它支持设置缓函数(如BounceEase)来控制动画。







©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页