WPF学习(12)动画
本篇来学习WPF的动画。什么是动画?动画就是一系列帧。在WPF中,动画就是在一段时间内修改依赖属性值的行为,它是基于时间线Timeline的。有人会说,要动画干嘛,华而不实,而且添加了额外的资源消耗而影响性能。尽管如此,适当的使用动画却可以使你的程序富有更好的表现力和交互性。更加可喜的是,WPF提供了丰富的动画支持,大部分的动画都可以直接通过XAML来呈现而不用去写繁琐的cs代码。在System.Windows.Media.Animation命名空间中,我们发现了许多的类,大体可归类为这么三种:基于线性内插算法动画(简单动画)、基于KeyFrame动画和基于路径动画。下面来分别介绍这三种动画:
首先以图来说明类的继承层次:
这张粗糙的图只是以Double类型的动画为例,因为它是比较全的,全面提到的三种动画类它都具备。支持KeyFrame的类型最多,其次是线性内插,最后是路径,只有三种,除了Double外,还有Matrix和Point。
1.基于线性内插算法动画(简单动画)
基于线性内插动画是在一个开始值到一个结束值之间以逐步增加的方式来改变属性值。有这么几个重要的属性:
From:开始值,当忽略该属性时,动画默认从其使用者属性值开始,所以其使用者属性在执行动画前要被赋值
To:结束值,当忽略该属性时,动画默认从其使用者属性值结束,所以其使用者属性在执行动画前要被赋值
By:递增值,和To一样和From搭配使用,当然可以用To来代替它
Duration:时间间隔,动画的执行时间,可以将一个TimeSpan值直接给它赋值,会自动隐式转换
AutoReverse:在到达结束值后,是否以相反的顺序回到From值
RepeatBehavior:重复行为,默认不重复,可设置其为Forever来重复动画,也可以通过构造器来设置次数和Timespan
FillBehavior:在活动周期结束后的行为方式,有HoldEnd(默认值)和Stop两个值,HoldEnd表示在动画结束后保持该属性值,而Stop则表示在动画结束后回到原来值(不是From值)
例子如下:
private void Button_Click(object sender, RoutedEventArgs e) { Button button = (Button)sender; DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.From = 200; widthAnimation.To = 100; //widthAnimation.AutoReverse = true; widthAnimation.Duration = TimeSpan.FromSeconds(5); widthAnimation.FillBehavior = FillBehavior.Stop; //RepeatBehavior = RepeatBehavior.Forever; widthAnimation.Completed += widthAnimation_Completed; button.BeginAnimation(Button.WidthProperty, widthAnimation); } void widthAnimation_Completed(object sender, EventArgs e) { //to do ... }
我们再来通过Trigger在Xaml页面举个例子:
<Window x:Class="AnimationDemo.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300"> <Window.Resources> <Style TargetType="Grid" x:Key="gridKey"> <Style.Triggers> <EventTrigger RoutedEvent="Grid.MouseEnter"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="RenderTransform.Children[0].ScaleX" From="0" To="1" Duration="0:0:2" AccelerationRatio="1" /> <DoubleAnimation Storyboard.TargetProperty="RenderTransform.Children[0].ScaleY" From="0" To="1" Duration="0:0:2" AccelerationRatio="1" /> <DoubleAnimation Storyboard.TargetProperty="RenderTransform.Children[1].Angle" From="70" To="0" Duration="0:0:2" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Style.Triggers> </Style> </Window.Resources> <Grid> <Grid x:Name="grid" Background="Coral" Style="{StaticResource gridKey}"> <Grid.RenderTransform> <TransformGroup> <ScaleTransform /> <RotateTransform /> </TransformGroup> </Grid.RenderTransform> <TextBlock TextWrapping="Wrap"> 烽火绵延,战场泣杀,谁留锋刃惹人嘲。 弑红双眸,血刃乱划,拼得素衣染泪颊。 阴风低吼,暴雨怒下,徒留艳红干不涸。 只盼苟活,还能归家,再不问誓死拼杀。 此生惟愿与你共活,难得执手画青丝沧桑,不管红尘凡事俗争,只此一生为你描眉点唇。 任苍天笑我懦弱,我依旧充耳不顾,为你甘愿卸甲耕种,只盼安稳走完一生。 如若我此去再无音信,愿你重披嫁衣,让他代我此生疼你若宝。 如若我此去再无归期,愿你冲洗记忆,让他重新入你心底。 我只不甘,再不见你如斯容颜。 我只不愿,再见你眼眸带泪颜。 我只不叹,再不见你怀中展颜。 我只不再,再见盼来世重逢颜。 风烟轻擦,我已杳无牵挂,独余你,是我心底最不愿触及的殇。 </TextBlock> </Grid> </Grid> </Window>
效果如下:
2.基于KeyFrame动画
基于KeyFrame动画可以很方便地实现多个分段和不规则移动的动画。虽然前面的线性内插动画也可通过BeginTime构建多个连续动画来实现,但是显得很复杂。基于KeyFrame动画是由多个段构成的,每一段表示动画的开始值和最终值或者中间值。主要有一个×××KeyFrameCollection类型的KeyFrames属性,它是一个×××KeyFrame的集合。×××KeyFrame有一个KeyTime类型的KeyTime属性和一个Object类型的Value属性,后者表示关键帧的目标值,前者是到达关键帧的目标值的时间。拿Double类型来说吧,DoubleAnimationUsingKeyFrames有个DoubleKeyFrameCollection类型的KeyFrames属性,是DoubleKeyFrame
抽象类型的集合,DoubleKeyFrame类型又有这么几个子类:
LinearDoubleKeyFrame:通过使用线性内插(线性关键帧),可以在前一个关键帧的Double值及其自己的Value值之间进行动画处理
DiscreteDoubleKeyFrame:通过使用离散内插(离散关键帧),可以在前一个关键帧的Double值及其自己的Value值之间进行动画处理
SplineDoubleKeyFrame:通过使用样条内插(样条关键帧),有KeySpline类型的KeySpline属性,它表示关键帧进度的三次方贝塞尔曲线
EasingDoubleKeyFrame:通过使用缓动关键帧,将EasingFunction和关键帧动画关联,有IEasingFunction类型的EasingFunction属性
例子如下:
<Window x:Class="AnimationDemo.Window2" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window2" Height="300" Width="300"> <Window.Resources> <Style TargetType="Grid" x:Key="gridKey"> <Style.Triggers> <EventTrigger RoutedEvent="Grid.MouseEnter"> <BeginStoryboard> <Storyboard> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="RenderTransform.Children[0].ScaleX" AccelerationRatio="1"> <DoubleAnimationUsingKeyFrames.KeyFrames> <DoubleKeyFrameCollection> <LinearDoubleKeyFrame KeyTime="0:0:0" Value="0" /> <LinearDoubleKeyFrame KeyTime="0:0:2" Value="1" /> </DoubleKeyFrameCollection> </DoubleAnimationUsingKeyFrames.KeyFrames> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="RenderTransform.Children[0].ScaleY" AccelerationRatio="1"> <DoubleKeyFrameCollection> <LinearDoubleKeyFrame KeyTime="0:0:0" Value="0" /> <LinearDoubleKeyFrame KeyTime="0:0:2" Value="1" /> </DoubleKeyFrameCollection> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="RenderTransform.Children[1].Angle" AccelerationRatio="1"> <DoubleKeyFrameCollection> <LinearDoubleKeyFrame KeyTime="0:0:0" Value="70" /> <LinearDoubleKeyFrame KeyTime="0:0:2" Value="50" /> <SplineDoubleKeyFrame KeyTime="0:0:4" Value="30"> <SplineDoubleKeyFrame.KeySpline> <KeySpline ControlPoint1="0.3,0.7" ControlPoint2="0.7,0.3" /> </SplineDoubleKeyFrame.KeySpline> </SplineDoubleKeyFrame> <EasingDoubleKeyFrame KeyTime="0:0:6" Value="0"> <EasingDoubleKeyFrame.EasingFunction> <BounceEase /> </EasingDoubleKeyFrame.EasingFunction> </EasingDoubleKeyFrame> </DoubleKeyFrameCollection> </DoubleAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Style.Triggers> </Style> </Window.Resources> <Grid> <Grid x:Name="grid" Background="Coral" Style="{StaticResource gridKey}"> <Grid.RenderTransform> <TransformGroup> <ScaleTransform /> <RotateTransform /> </TransformGroup> </Grid.RenderTransform> <TextBlock TextWrapping="Wrap"> 烽火绵延,战场泣杀,谁留锋刃惹人嘲。 弑红双眸,血刃乱划,拼得素衣染泪颊。 阴风低吼,暴雨怒下,徒留艳红干不涸。 只盼苟活,还能归家,再不问誓死拼杀。 此生惟愿与你共活,难得执手画青丝沧桑,不管红尘凡事俗争,只此一生为你描眉点唇。 任苍天笑我懦弱,我依旧充耳不顾,为你甘愿卸甲耕种,只盼安稳走完一生。 如若我此去再无音信,愿你重披嫁衣,让他代我此生疼你若宝。 如若我此去再无归期,愿你冲洗记忆,让他重新入你心底。 我只不甘,再不见你如斯容颜。 我只不愿,再见你眼眸带泪颜。 我只不叹,再不见你怀中展颜。 我只不再,再见盼来世重逢颜。 风烟轻擦,我已杳无牵挂,独余你,是我心底最不愿触及的殇。 </TextBlock> </Grid> </Grid> </Window>
3.基于Path的动画
基于Path的动画是一种很灵活的执行动画的方式,它一般被用于沿着一条路径来移动可视对象。它主要有一个PathGeometry类型的PathGeometry属性和PathAnimationSource类型的Source属性,前者表示指定用于生成此动画输出值的几何图形,后者表示指定PathGeometry属性的方位,默认值为PathAnimationSource.X,表示指定在前进过程中沿动画序列路径的 X 坐标偏移量。
<Window x:Class="AnimationDemo.Window3" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window3" Height="500" Width="600"> <Window.Resources> <PathGeometry x:Key="pathKey"> <PathGeometry.Figures> <PathFigure IsClosed="True"> <PathFigure.Segments> <LineSegment Point="300,2" IsStroked="True" IsSmoothJoin="True"/> <LineSegment Point="300,80" IsStroked="True" IsSmoothJoin="True"/> <LineSegment Point="2,80" IsStroked="True" IsSmoothJoin="True"/> </PathFigure.Segments> </PathFigure> </PathGeometry.Figures> </PathGeometry> <Style x:Key="ImgKey" TargetType="Image"> <Style.Triggers> <EventTrigger RoutedEvent="Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimationUsingPath Storyboard.TargetProperty="(Canvas.Left)" PathGeometry="{StaticResource pathKey}" Duration="0:0:5" RepeatBehavior="Forever" Source="X"> </DoubleAnimationUsingPath> <DoubleAnimationUsingPath Storyboard.TargetProperty="(Canvas.Top)" PathGeometry="{StaticResource pathKey}" Duration="0:0:5" RepeatBehavior="Forever" Source="Y"> </DoubleAnimationUsingPath> </Storyboard> </BeginStoryboard> </EventTrigger> </Style.Triggers> </Style> </Window.Resources> <Canvas> <Path Stroke="Red" StrokeThickness="1" Data="{StaticResource pathKey}" Canvas.Left="10" Canvas.Top="10" /> <Image x:Name="img" Canvas.Left="15" Canvas.Top="15" Style="{StaticResource ImgKey}"> <Image.Source> <DrawingImage> <DrawingImage.Drawing> <GeometryDrawing Brush="LightSteelBlue"> <GeometryDrawing.Pen> <Pen Brush="Black" Thickness="1" /> </GeometryDrawing.Pen> <GeometryDrawing.Geometry> <EllipseGeometry Center="10,10" RadiusX="3" RadiusY="3" /> </GeometryDrawing.Geometry> </GeometryDrawing> </DrawingImage.Drawing> </DrawingImage> </Image.Source> </Image> </Canvas> </Window>
4.动画在XAML中的载体--Storyboard
正如开始所说,大部分基于属性的动画都是可以直接在XAML中表达的,但是它需要一个载体,或者叫一个容器,它就是Storyboard,它是动画和希望应用动画的属性的桥梁。Storyboard经常被BeginStoryboard包装作为EventTrigger的TriggerAction,这里BeginStoryboard是一个密封类,而不是FrameworkElement里面的那个方法,它表示一个触发器操作,该操作可启动Storyboard 并将其动画分发给动画的目标对象和属性。Storyboard继承自ParallelTimeline,意味着它可以并行运行多个子时间线,而且它具有控制动画播放的能力,例如Pause、Resume、Skip和Stop等。它用TargetName附加属性表示动画的作用者,用TargetProperty附加属性来指定TargetName的希望改变的属性。
<Window x:Class="AnimationDemo.Window4" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window4" Height="300" Width="300"> <Window.Resources> <Style TargetType="ListBoxItem"> <Style.Triggers> <EventTrigger RoutedEvent="MouseEnter"> <BeginStoryboard HandoffBehavior="Compose"> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="FontSize" BeginTime="0:0:0.5" Duration="0:0:0.2" To="18"/> </Storyboard> </BeginStoryboard> </EventTrigger> <EventTrigger RoutedEvent="MouseLeave"> <BeginStoryboard HandoffBehavior="Compose"> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="FontSize" BeginTime="0:0:0.5" Duration="0:0:0.2" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Style.Triggers> </Style> </Window.Resources> <Grid> <ListBox> <ListBoxItem>Jello</ListBoxItem> <ListBoxItem>Taffy</ListBoxItem> <ListBoxItem>Jim</ListBoxItem> <ListBoxItem>Lily</ListBoxItem> </ListBox> </Grid> </Window>
5.基于Frame的动画
前面介绍的都是基于属性的动画,还有一种比较的动画,既是基于帧的动画,只需要订阅CompositionTarget的静态Rendering事件即可,它在呈现组合树中的对象之前发生,从而为每帧获取内容。
Xaml代码:
<Window x:Class="AnimationDemo.Window7" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window7" Height="300" Width="300"> <Grid Margin="3"> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <StackPanel Orientation="Horizontal"> <Button Margin="3" Padding="3" Click="cmdStart_Clicked">Start</Button> <Button Margin="3" Padding="3" Click="cmdStop_Clicked">Stop</Button> </StackPanel> <Canvas Name="canvas" Grid.Row="1" Margin="3"></Canvas> </Grid> </Window>
cs代码:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; namespace AnimationDemo { /// <summary> /// Window7.xaml 的交互逻辑 /// </summary> public partial class Window7 : Window { public Window7() { InitializeComponent(); } private bool rendering = false; private void cmdStart_Clicked(object sender, RoutedEventArgs e) { if (!rendering) { ellipses.Clear(); canvas.Children.Clear(); CompositionTarget.Rendering += RenderFrame; rendering = true; } } private void cmdStop_Clicked(object sender, RoutedEventArgs e) { StopRendering(); } private void StopRendering() { CompositionTarget.Rendering -= RenderFrame; rendering = false; } private List<EllipseInfo> ellipses = new List<EllipseInfo>(); private double accelerationY = 0.1; private int minStartingSpeed = 1; private int maxStartingSpeed = 50; private double speedRatio = 0.1; private int minEllipses = 20; private int maxEllipses = 100; private int ellipseRadius = 10; private void RenderFrame(object sender, EventArgs e) { if (ellipses.Count == 0) { // Animation just started. Create the ellipses. int halfCanvasWidth = (int)canvas.ActualWidth / 2; Random rand = new Random(); int ellipseCount = rand.Next(minEllipses, maxEllipses + 1); for (int i = 0; i < ellipseCount; i++) { Ellipse ellipse = new Ellipse(); ellipse.Fill = Brushes.LimeGreen; ellipse.Width = ellipseRadius; ellipse.Height = ellipseRadius; Canvas.SetLeft(ellipse, halfCanvasWidth + rand.Next(-halfCanvasWidth, halfCanvasWidth)); Canvas.SetTop(ellipse, 0); canvas.Children.Add(ellipse); EllipseInfo info = new EllipseInfo(ellipse, speedRatio * rand.Next(minStartingSpeed, maxStartingSpeed)); ellipses.Add(info); } } else { for (int i = ellipses.Count - 1; i >= 0; i--) { EllipseInfo info = ellipses[i]; double top = Canvas.GetTop(info.Ellipse); Canvas.SetTop(info.Ellipse, top + 1 * info.VelocityY); if (top >= (canvas.ActualHeight - ellipseRadius * 2 - 10)) { // This circle has reached the bottom. // Stop animating it. ellipses.Remove(info); } else { // Increase the velocity. info.VelocityY += accelerationY; } if (ellipses.Count == 0) { // End the animation. // There's no reason to keep calling this method // if it has no work to do. StopRendering(); } } } } } public class EllipseInfo { public Ellipse Ellipse { get; set; } public double VelocityY { get; set; } public EllipseInfo(Ellipse ellipse, double velocityY) { VelocityY = velocityY; Ellipse = ellipse; } } }