从自定义DoubleAnimation开始
虽然是从从自定义DoubleAnimation开始,不过重点MS应该是在后面对Freezable DependencyProperty等的讨论~笔记的一部分,写的比较乱,不过思路还算清楚
下面尝试自定义一个DoubleAnimation
简单的准备,
一般的DoubleAnimation是线性的变换函数
横坐标表示时间的进度, 范围从0到1 ,这个值可以从Clock获得,纵坐标表示Double数值的进度,这里也定义为从0到1,乘以实际的总路径后就获得当前路径
可能某些时候需要复杂一点的变化,比如用二次曲线吧(其实这个可以用其他方法很简单的实现,这里只是讨论自定义Animation的问题)
假设曲线为二次曲线 (设横坐标为x,则曲线方程为y=x*x) 非常简单的一个例子
首先定义一个矩形
<Rectangle.RenderTransform>
<TranslateTransform X="0" Y="0"/>
</Rectangle.RenderTransform>
<Rectangle.Fill>
<LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">
<GradientStop Color="#FF000000" Offset="0"/>
<GradientStop Color="#FFFFFFFF" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
用DoubleAnimation让他动起来:
<EventTrigger RoutedEvent="Window.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="myRectangle" Storyboard.TargetProperty="RenderTransform.X"
Duration="0:0:3" From="0" To="180"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Window.Triggers>
下面开始自定义一个Animation。WPF提供了DoubleAnimationBase类,用于扩展。
先新建一个类, 取名QuadraticDoubleAnimation,继承于DoubleAnimationBase,并实现抽象函数
首先来实现CreateInstanceCore() 这里暂时返回一个本类的新的实例就行了
{
return new QuadraticDoubleAnimation();
}
然后实现GetCurrentValueCore(),前面提到过TimeClock会通过GetCurrentValue()方法获得当前的值,不过GetCurrentValue()方法除了完成计算值之外还会完成其他工作,因此不能够直接定义GetCurrentValue()方法,定义GetCurrentValueCore(),就行了.
我们首先计算整个delta值,也就是动画全程的距离,再通过y=x*x*delta计算并返回当前,很简单的两个语句
{
double delta = To - From;
double x=animationClock.CurrentProgress.Value;
return x * x * delta;
}
完成了这些,一切看起来都不错,应该可以运行了。在XAML添加应用,并把对DoubleAnimation的引用修改为对我们的QuadraticDoubleAnimation的引用。
在Window属性中加入
Storyboard的代码:
<myAnimations:QuadraticDoubleAnimation Storyboard.TargetName="myRectangle" Storyboard.TargetProperty="RenderTransform.X"
Duration="0:0:3" From="0" To="180"/>
</Storyboard>
运行。编译情况不妙,没通过,原来To和From属性没有实现。看看DoubleAnimation类和DoubleAnimationBase类的实现
注意到Duration这样的属性是找得到的,实际上Duration属性是早在TimeLine里就实现了(继承关系TimeLine->AnimationTimeline->DoubleAnimationBase)但是From/To包括By属性是在DoubleAnimation类实现的。
看来我们需要自己实现From/To/By这几个属性了,这里只实现From和To
这非常简单,我们很快就实现了两个属性:
public double From
{
get { return _from; }
set { _from = value; }
}
private double _to;
public double To
{
get { return _to; }
set { _to = value; }
}
F5运行,通过了,但是动画没有动起来。
这是为什么?经过跟踪,很容易发现GetCurrentValueCore()中To,From始终是0.0,想想也对,光定义了两个属性,这里,XAML在前台定义了一个QuadraticDoubleAnimation对象后,XAML中明明定义了这两个属性的值,为什么它们没有变化呢?。这很奇怪,因为在前台设置的Duration属性就返回到了后台。
通过查看TimeLine的元数据发现,Duration是通过DependencyProperty存在的,模仿一下:
代码改变如下:
{
get { return (double)GetValue(FromProperty); }
set { SetValue(FromProperty, value); }
}
public double To
{
get { return (double)GetValue(ToProperty); }
set { SetValue(ToProperty, value); }
}
GetValue和SetValue是通过ToProperty和FromProperty访问的,还需要注册两个属性名称:
DependencyProperty.Register("From",
typeof(double),
typeof(QuadraticDoubleAnimation),
new PropertyMetadata(null));
public static readonly DependencyProperty ToProperty =
DependencyProperty.Register("To",
typeof(double),
typeof(QuadraticDoubleAnimation),
new PropertyMetadata(null));
再运行,图像动起来了,速度先慢后快,按照我们的数学函数描述的方式运动,至此自定义动画成功
实际上程序中还有许多bug,比如当不指定From或者To的时候程序就会出错,From To应该为Double?类型~等,这里都省略了。。(先庆祝一下 呵呵)
最大的问题来了,为什么直接使用属性,不能达到效果,而使用DependencyProperty就可以了呢?这和XAML的工作方式有关系。
我们自定义的Animation对象在WPF中是一个Freezable(可冻结对象,假设这为对象A),默认情况下,它是没有被冻结的。也就是说它是可以被修改的,为了保证对它的修改不影响动画的正常进行,WPF会在开始动画的时候复制一个Animation对象(假设为对象B),对象B对编程者来说是不可知的,也就是说在动画进行的过程中,我们对Animation对象的任何操作实际上都是对对象A进行的操作。
下面我们来关注对象B的复制过程。对了,在这里:
{
return new QuadraticDoubleAnimation();
}
当Freezable需要被复制的时候,WPF就会调用这个方法来进行复制。所以,如果我们仅仅使用普通的属性定义方式,动画时候使用的QuadraticDoubleAnimation类实际上是一个刚刚初始化过的新的Animation类,当然没有包含From和To的数据。
当使用了DependencyProperty的时候情况又怎么样呢?Freezable被复制时,CreateInstanceCore函数返回了一个空类,然后Freezable会将所有DependencyProperty中的值复制到新的类中。(我确定了Freezable确实会这么做,不过我还不清楚这种“自动深度复制”是Freezable的特性还是和DependencyProperty的特性有关,看起来似乎像前者,这里我没有深究,希望有人指教一下)
理解了这些,我们也可以尝试一些方法避开使用DependencyProperty(不过MSDN中极力推荐我们使用DependencyProperty,其实这样确实是一种更加良好的设计,下面还会看到有其他原因)
首先可以在CreateInstanceCore函数中手动的加入复制To和From属性的语句,像这样:
{
QuadraticDoubleAnimation q = new QuadraticDoubleAnimation();
q.From = this.From;
q.To = 0; ;
return q;
}
运行程序,一切正常。
还有更简单的方法,当一个Freezable已经被冻结的时候,再使用它就不回去得到一个副本了(这对效率的提升是非常大的),所以只需要在Window的初始化的代码中将我们定义的QuadraticDoubleAnimation冻结就行了(或者在XAML中加入PresentationOptions:Freeze="True" 这需要事先声明xmlns:PresentationOptions="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options")
冻结之后,不使用DependecyProperty属性而只是用一般属性,运行程序,一切正常。
还没有完,在Freezable中我们了解到冻结之后属性不能再被改变,否则会抛出一个异常,我们来尝试一下。
加入一个按纽,Click事件处理函数如下:
{
MessageBox.Show(myAnimation.IsFrozen.ToString());
myAnimation.To = 300;
}
第一行确认myAnimation对象当前状态已经是被冻结了,第二行尝试改变它的值。
运行程序,点击按纽,弹出”True”,然后对象速度突然加快,To属性的更改生效了,这不是违反了Freezable的规则吗?
没错,这就是违反了Freezable的规则。这并不是WPF的Bug,而是我们事先已经违反了规则,Freezable的所有属性都需要定义成DependencyProperty,这样当改变属性是WPF才能进行判断,原来是我们先没有按规则出牌。
重新用DependencyProperty定义To和From两个属性,再运行程序,点按纽。
这下抛出了一个提示,To属性是只读属性。
所以我们还是按照规则使用DependencyProperty的好。
又有一个问题,刚才我们实现了一个挺酷的功能,在动画运行期实时改变了Animation的某个属性,这是在Animation没有冻结的情况下不大好办到的,不过我不推荐这样来改变属性,以下提供两种Animation没有被冻结且动画运行期改变属性的方法:
1 得到Animation的Clock(使用CreatClock方法),得到当前时间,使动画停止,改变值,然后从新开始动画,定位到同样的时间,这实际上建立了一个新的动画的实例,性能上会有损耗。
2 在CreatInstanceCore()方法中利用全局成员或者静态类或者其他什么方法,把创建的新的Animation的引用保存下来,需要的时候操作这个引用
涉及到的东西比较多比较杂,就懒得总结了~
问题讨论告一个段落,写了个小程序,了解了不少WPF的实现机制的东西。还是很好玩的 呵呵