<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)来控制动画。