在Windows 8中,Animation角色的重要性超出了我们的想象,因为它也算是Windows 8流畅性体验的一部分。在我看来,Animation要做的好,有利于整体的布局,自定义控件,以及控件的行为的设计等等,都是为了用户体验。
通过定义ControlTemplate,我们可以为完全重新定义我们控件外观,而ControlTemplate中最重要的部分就是用来承载它外观的Visual Tree,这个模板必须包含如下的代码,即在发生适当的条件时,控件的外观应该是如何表现的。比如Button的Pressed状态的发生等等,虽然改变Button的外表状态不像是我们想的那种花时间,复杂的动画改变状态(比如以后会介绍的ObjectAnimation),但是也归为了Animation功能。
WinRT的Animation是基于时间的动画,举个例子,当一个线程既要跑动画,又要做其他工作而导致动画丢失了一些执行时间时,基于帧(Frame-based)的动画会继续从它上次离开的地方继续执行,而基于时间(Time-based)的动画,则根据现在实际的时间,来做原来计划中这个时间点应该做的效果。个人推断,一种极端是若是线程很busy,而分配的动画时间可能是2秒,那么线程若做别的事情超过2秒,动画将会一步到位,没有2秒的过程。
基础动画入门
下面对TextBlock的FontSize进行动态改变,这样TextBock的字体就会根据我们预设的方案改变,注意,其实Animation只是我们的控制方案,也就是Control,它的目标是某个属性,而不是整个动画怎么移动,有点像指挥家,它只负责指挥,具体的动作还是通过框架的渲染系统根据属性的改变而进行重新绘制,进而达到动画的效果. 一般情况下,我们的动画控制被当作是一种资源,放在Xaml中Root元素的Resources中.一个简单的Animation包括一个Storyboard和一个XXXAnimation,如下代码片段:
<Page … > <Page.Resources> <Storyboard x:Key="storyboard"> <DoubleAnimation Storyboard.TargetName="txtblk" Storyboard.TargetProperty="FontSize" EnableDependentAnimation="True" From="1" To="144" Duration="0:0:3" /> </Storyboard> </Page.Resources> <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBlock Name="txtblk" Text="Animated Text" Grid.Row="0" FontSize="48" HorizontalAlignment="Center" VerticalAlignment="Center" /> <Button Content="Trigger!" Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" Click="OnButtonClick" /> </Grid> </Page>
public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); } void OnButtonClick(object sender, RoutedEventArgs args) { (this.Resources["storyboard"] as Storyboard).Begin(); } }
在上述代码中,DoubleAnimation的目标类型是Double(它的类名修饰说明了这一切),也就是说,你可以控制某个控件的Double属性(必须是依赖属性),让它跟着你的”剧本”走.那”剧本”就是DoubleAnimation,而剧本的内容就是它的一些个属性.你将会看到,WinRT还提供了对Point,Color和Object(第一次看见对Object,好像它是我们唯一需要使用的,因为任何的类都是继承于它,事实是我们使用它只是为了重新给某个属性的赋值而已,一般情况就设定两个值交替使用)。
WinRT要求,一个Animation对象(比如DoubleAnimation)必须包含在Storyboard下。相反,一个Storyboard可以包含多并发执行动画的Animation对象,而它的职责就是确保它们正确,同步地执行。
上面的代码中的TargetName和TargetProperty是Storyboard定义的附加属性(attached properties),你可以在Animation对象中设置它所关心的对象名称以及对象的那个需要动画的属性。
如前面所说的,Animation只对某个值(依赖属性)感兴趣,而其实对动画的实现不感兴趣,或者说,你有计划地改变某个值完全可以不是为了动画,所以,Animation一般在其他的线程下执行(非UI线程),为的是UI线程可以响应用户的操作。然而,我们当前的操作是为了动画,大家知道,在非UI线程中不能直接去修改在UI线程中创建的控件属性(抛异常),因为它的改变会引起布局的重新刷新,即非UI线程直接修改了界面,这显然是不可取的。WinRT不希望animation运行在UI线程中,但是为了让动画代码能在UI线程下运行,你必须将DoubleAnimation的EnableDependentAnimation设置为True,来告诉WinRT这个动画是依赖于UI线程才能达到代码目的的(dependent on the user interface thread)。
剩下的From ,To,Duration属性就非常容易理解,即在3秒的时间内,FrontSize从1变到144.Duration的格式是”时:分:秒”或“时:分”或“时”,且对于秒来说,可以是小数。
当你还没按下Trigger按钮,那么文本的字大小是预设的48-pixel,一旦OnButtonClick触发了,那么动画便随着Begin()的调用而启动,然后这个文本的文字大小瞬间变成1(From的值),然后3秒后,线性地变到144(To的值).所谓线性的,那就是每秒变大的速度是一样的,如下计算:
v(单位pixel/second,p/s)=(144-1)/3=48-1/3(p/s)
字体大小(pixel) | 时间(s) |
1+v*0=1 | 0 |
1+v*1=48-2/3 | 1 |
1+v*2=96-1/3 | 2 |
1+v*3=144 | 3 |
你可以随时地触发Begin,即使它还在执行,也会重新从1pixel开始变大。
其他属性对Animation变化的影响
以DoubleAnimation为例,在之前的代码中,动画结束后,字体的大小不是恢复到48(预设值),而是被重新回归到To的值,也就是1pixel,就好像它准备从头再来一遍一样。控制这种行为的是Animation的FillBehavior,它是一个枚举值,默认是HoldEnd,也就是我们碰到的情况,若是改成Stop,那么我们的文本的值将会回归到48,也就是停止(stop)再次循环.修改代码如下:
<Storyboard x:Key="storyboard"> <DoubleAnimation Storyboard.TargetName="txtblk" Storyboard.TargetProperty="FontSize" EnableDependentAnimation="True" FillBehavior="Stop" From="1" To="144" Duration="0:0:3" /> </Storyboard>
我们除了可以使用From,To进行控制Double的始末值,还可以单用其中一个,或者使用By属性。
几种情况看代码:
<Storyboard x:Key="storyboard"> <DoubleAnimation Storyboard.TargetName="txtblk" Storyboard.TargetProperty="FontSize" EnableDependentAnimation="True" From="1" Duration="0:0:3" /> </Storyboard>
缺省To,这种情况相当于To值为当前预设的48pixel。
<Storyboard x:Key="storyboard"> <DoubleAnimation Storyboard.TargetName="txtblk" Storyboard.TargetProperty="FontSize" EnableDependentAnimation="True" To="144" Duration="0:0:3" /> </Storyboard>
缺省From,相当于From值为预设值48pixel。
其实以上的From和To都是nullable类型,系统通过这样来判定它们是否有被赋值,若没有,则结合当前的预设值进行动画。
<Storyboard x:Key="storyboard"> <DoubleAnimation Storyboard.TargetName="txtblk" Storyboard.TargetProperty="FontSize" EnableDependentAnimation="True" By=”100” Duration="0:0:3" /> </Storyboard>
使用By,那么就从当前的48pixel在3秒内增加100pixel,而且当你下次再开始动画时,它的值将从148开始增加到248,也就FillBehavior的HoldEnd不起作用,但是若设置成Stop,则它还是会回到48。
若你想让你的动画原路返回(对于以上的例子,相当于To变成1,From变成144,也是在3秒的时间内完成) ,那么可以将Animation的AutoReverse设置为true。这样,整个的动画就会花费6秒的时间:
<Storyboard x:Key="storyboard"> <DoubleAnimation Storyboard.TargetName="txtblk" Storyboard.TargetProperty="FontSize" EnableDependentAnimation="True" AutoReverse="True" From="1" To="144" Duration="0:0:3" /> </Storyboard>
以上是定义一个周期的,那么我们也可以通过设置Animation的RepeatBehavior来定义周期的数目(或者定义总的执行时间),以下的代码将执行3个周期,一个周期的定义是从1变到144,再从144变到1,一个周期6秒,总时间18秒:
<Storyboard x:Key="storyboard"> <DoubleAnimation Storyboard.TargetName="txtblk" Storyboard.TargetProperty="FontSize" EnableDependentAnimation="True" AutoReverse="True" RepeatBehavior="3x" From="1" To="144" Duration="0:0:3" /> </Storyboard>
RepeatBehavior也可以定义一个时间段(duration),比如我们可以让周期为6秒的动画执行7.5秒,那么6秒一个周期后,它又继续重新开始执行1.5秒,也就是final 的大小是1+v*1.5=72.5,它停止的时的大小将会保持在72.5,当然你还是可以使用FillBehavior设置成Stop来跳回到48。
<Storyboard x:Key="storyboard"> <DoubleAnimation Storyboard.TargetName="txtblk" Storyboard.TargetProperty="FontSize" EnableDependentAnimation="True" AutoReverse="True" RepeatBehavior="0:0:7.5" From="1" To="144" Duration="0:0:3" /> </Storyboard>
RepeateBehavior还可以接受无数次的周期,只要输入”Forever”就可以:
<Storyboard x:Key="storyboard"> <DoubleAnimation Storyboard.TargetName="txtblk" Storyboard.TargetProperty="FontSize" EnableDependentAnimation="True" AutoReverse="Forever" RepeatBehavior="0:0:7.5" From="1" To="144" Duration="0:0:3" /> </Storyboard>
预约开始时间,是的,对于Animation还可以约定它什么时候开始动画,比如从现在开始1.5s后启动动画:
<Storyboard x:Key="storyboard"> <DoubleAnimation Storyboard.TargetName="txtblk" Storyboard.TargetProperty="FontSize" EnableDependentAnimation="True" BeginTime="0:0:1.5" From="1" To="144" Duration="0:0:3" /> </Storyboard>
到目前为止,我们所有的DoubleAnimation动画都是有固定的速度V的,也就是是线性的。那么,一种创建非线性动画的方法,就是设置DoubleAnimation的EasingFunction(缓动函数)属性的属性元素,可以设置的值是继承自EasingFunctionBase的11个类,比如:
<Storyboard x:Key="storyboard"> <DoubleAnimation Storyboard.TargetName="txtblk" Storyboard.TargetProperty="FontSize" EnableDependentAnimation="True" From="1" To="144" Duration="0:0:3" > <DoubleAnimation.EasingFunction> <ElasticEase /> </DoubleAnimation.EasingFunction> </DoubleAnimation> </Storyboard>
所谓的缓动函数,就是让动画不局限于一种物理状态(比如匀速),比如拥有加速度,而且是接近自然的,放松不死板的运动方式。当运行以上代码时,发现文本像是一个垂直于屏幕的弹簧,再你按下以后弹了好4次以后再停止,在此期间,时间还是3秒,第一次的最大值已经超过了144,然后第二次,第三次,最后停止在To的值上,即144,所以这符合对弹簧的模拟。
EasingFunctionBase 定义了一个EasingMode属性,当然也被其11个子类所继承。它的默认值是EasingMode.EaseOut,它的含义是动画效果开始的时候是线性地完成从1到144的变化(但是时间小于3秒,显然是为了后续的效果时间,但是总时间不会大于3秒),然后再进行“弹簧”的效果。你也可以将它的值设置为EaseIn,即效果在开始动画的时候演示,或者是用EaseInOut,在动画的开始和结束都进行特效。注意:个人体验这段 代码,使用EaseIn的时候,会触发"Invalid attribute value for property FontSize."的异常,导致程序中断。
一些基于EasingFunctionBase 的子类也定义了自己的属性,比如上面提到的ElasticEase类,它可以控制来回震荡的次数,即Oscillations属性,默认值是3。你可以设置它的值为10,那么它会来回弹10次然后停止,但是时间还是3秒。那么有控制次数的,也就有控制弹跳的效果的,即弹跳的程度,那么就是Springiness属性,它的中文意思是青春期,可以说,它的值越小,也就越“年轻”,所以弹跳的“动作幅度”也就会越大,默认值也是3。尝试如下的代码体验:
<Storyboard x:Key="storyboard"> <DoubleAnimation Storyboard.TargetName="txtblk" Storyboard.TargetProperty="FontSize" EnableDependentAnimation="True" From="1" To="144" Duration="0:0:3" > <DoubleAnimation.EasingFunction> <ElasticEase Oscillations="10" Springiness="0"/> </DoubleAnimation.EasingFunction> </DoubleAnimation> </Storyboard>
之前提到的,诸如DoubleAnimation的对象必须包含在Storyboard对象中,有趣的是,其实这两个类继承自同一个类,即TimeLine,类继承图如下:
Object DependencyObject Timeline Storyboard DoubleAnimation …
Storyboard定义了TimelineColletion类型的Children属性,以及附加属性TargetName和TargetProperty,除此之外,还定义了控制动画的过程的函数,如pause(暂停),resume(恢复)。
到目前为止,我们看见的AutoReverse,BeginTime,Duration,FillBehavior和RepeatBehavior都是定义在Timeline,那么由于这些是依赖属性,可想而知,在Storyboard上设置这些值以后,它的Children中的各种Animation就可以共享这些值。
Timeline也定义了一个名叫SpeedRatio的属性(Double类型),从名称可知是控制速度动画速度的,将原来的速度和它相乘后得出的速度就是最终速度,显然,这会影响时间,我们可以把要变化的值的范围看成是距离(等长的情况),那么时间和速度就成反比了(默认值当然就是1啦),运行如下代码:
<DoubleAnimation Storyboard.TargetName="txtblk" Storyboard.TargetProperty="FontSize" EnableDependentAnimation="True" From="1" To="144" Duration="0:0:3" SpeedRatio="0.5" > </DoubleAnimation>
你可以计算它的花费时间是6秒。在平时使用时,这个值一般放置在Storyboard(当然,本意是在哪个Timeline中都可以)
,这样的目的其实就是为了可以控制所有在Storyboard上的Animation,因为我们设计的时候,这些Animation将会以一个整体的效果出现。那么当我们在Storyboard和Animation中同时设置这个属性时,会发生什么情况呢,如运行一下的代码:
<Storyboard x:Key="storyboard" SpeedRatio="0.5"> <DoubleAnimation Storyboard.TargetName="txtblk" Storyboard.TargetProperty="FontSize" EnableDependentAnimation="True" From="1" To="144" Duration="0:0:3" SpeedRatio="2" > </DoubleAnimation> </Storyboard>
我们在Storyboard中设置速度为0.5,而在DoubleAnimation中设置速度为2,那么它的结果就是整个速度为1,也就是和将这个两个值去掉一样的效果,花费还是3秒。
Timeline还有一个Completed的事件,也就是当动画完成时会触发的事件,你可以在恰当的时候使用它。
接下去实践一下完全在C#代码中构建Animation,首先定义UI界面:
<Page …> <Page.Resources> <Style TargetType="Button"> <Setter Property="Content" Value="Trigger!" /> <Setter Property="FontSize" Value="48" /> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="Margin" Value="12" /> </Style> </Page.Resources> <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"> <Grid HorizontalAlignment="Center" VerticalAlignment="Center"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Button Grid.Row="0" Grid.Column="0" Click="OnButtonClick" /> <Button Grid.Row="0" Grid.Column="1" Click="OnButtonClick" /> <Button Grid.Row="0" Grid.Column="2" Click="OnButtonClick" /> <Button Grid.Row="1" Grid.Column="0" Click="OnButtonClick" /> <Button Grid.Row="1" Grid.Column="1" Click="OnButtonClick" /> <Button Grid.Row="1" Grid.Column="2" Click="OnButtonClick" /> <Button Grid.Row="2" Grid.Column="0" Click="OnButtonClick" /> <Button Grid.Row="2" Grid.Column="1" Click="OnButtonClick" /> <Button Grid.Row="2" Grid.Column="2" Click="OnButtonClick" /> </Grid> </Grid> </Page>
我们没有在XAML中定义任何关于Animation的信息,但是都在代码中添加。以上定义了9个独立的Button对象,我们在Click它的时候将对该Button中的Fontsize进行变化。构建的策略讨论,你可以为每个Button构建一次的Storyboard和DoubleAniamtion,然后在重复地使用它们,这里的条件是重复使用时,animation的target应该是同一个对象,也就是不能借给别人用,那么第二种策略就是每次使用新的Storyboard和DoubleAniamtion,简单的方法当然就是第二种,代码如下:
void OnButtonClick(object sender, RoutedEventArgs args) { DoubleAnimation anima = new DoubleAnimation { EnableDependentAnimation = true, To = 96, Duration = new Duration(new TimeSpan(0, 0, 1)), AutoReverse = true, RepeatBehavior = new RepeatBehavior(3) }; Storyboard.SetTarget(anima, sender as Button); Storyboard.SetTargetProperty(anima, "FontSize"); Storyboard storyboard = new Storyboard(); storyboard.Children.Add(anima); storyboard.Begin(); }
和XAML中定义有点区别的是,由于XAML里只能通过为UI元素命名而相互引用,所以是使用Storyboard.SetTargetName来赋予动画的对象,但是在代码中,我们不需要知道它叫什么,而是关心它是什么,所以使用SetTarget,直接传递对象给它就可以。
还有,我们使用Duration控制动画的时间,那么可以使用一个接受TimeSpan对象的构造函数,那么我们在碰到Forever以及默认值(1秒)时,可以使用Duration的静态属性,一个是用Duration.Forever,一个是用Duration.Automatic。
当我们单击了Button时,它因为字体大小的改变而体积发生改变,那么Grid就需要重新计算如何分配每个单元格的大小,当我们多按几个Button时,你就会发现有趣的现象了。
Animation第一部分结束语:这是我看原版的Windows Programming 6th Release Preview电子版的度数笔记,是翻译和自己体会的结合。