WPF 教程 - 第 2 部分:编写自定义动画类






4.85/5 (51投票s)
本文介绍如何对没有关联动画类的属性应用动画
引言
当 Christian 和 Nish 去年 7 月开始我们的 WPF 系列时,我们并没有计划在第 1 部分和第 2 部分之间间隔 9 个月。我们都忙于各种项目,并且一直拖延编写下一部分。好了,现在我们已经准备好 WPF 系列的第 2 部分了,正如他们所说——迟做总比不做好。在本文中,我们将讨论如何对没有关联动画类的属性应用动画,我们将特别关注使用 GridLength
属性对 Grid
控件的列和行进行动画处理。
网格列和行动画处理的麻烦
在我们开始为 GridLength
设置动画之前,我们先看看它与为 Width
或 Opacity
等属性设置动画有何不同。虽然我们希望您对 WPF 中的动画工作方式有基本的了解,但让我们简要地了解一下常规动画的工作方式,其中“常规”是指为具有关联动画类的属性设置动画。
常规动画如何工作?
动画的典型用法涉及在特定持续时间内通过线性插值更改属性(通常是依赖项属性)。例如,以下 Xaml 展示了如何将按钮的不透明度从完全不透明动画到完全透明。
<Button Name="button1">
One
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="button2"
Storyboard.TargetProperty="Opacity"
From="1" To="0" Duration="0:0:2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
我们可以指定双精度值 1 和 0 作为 Opacity
属性的开始值和停止值。DoubleAnimation
类专门用于处理 double
类型的属性(Opacity
、Width
、Length
等都是 double
类型)。如果您查看 System.Windows.Media.Animation
命名空间,您会发现还有其他专门的动画类用于处理其他常见类型的值,例如 Boolean
、Char
、 Byte
、Color
、Point
等。接下来让我们看看 Grid
的列和行维度为何不能以这种方式进行动画处理。
为什么常规动画不能与网格一起工作?
ColumnDefinition
和 RowDefinition
类分别具有 Width
和 Height
属性,类型为 GridLength
。GridLength
是一个 struct
,其目的是支持基于 Star
的单位以及基于像素的单位。Star
单位将维度指定为总可用空间的加权比例。因此,如果您有两个列,第一个的宽度为 {*}
,第二个的宽度为 {3*}
,则第一个列将占用包含面板总宽度的 25%,而第二个列将占用剩余的 75% 空间。这为我们使用网格提供了很大的灵活性,我们不需要指定硬编码值——这还有一个额外的好处,那就是将来更容易添加行和列。
由于 Grid
使用 GridLength
作为尺寸,其副作用是,我们无法将库中任何内置动画类与它一起使用,因为它们都没有打算支持 GridLength
。但这并不意味着我们对此无能为力。我们可以(也将会)编写一个专门处理 GridLength
类型单位的动画类。
为 GridLength 编写自定义动画类
我们的目标是编写一个 GridLengthAnimation
类,它将支持基于 GridLength
属性的动画。为了使示例简单明了,我们只支持 From
和 To
属性,而不支持 By
等其他类(如 DoubleAnimation
)支持的属性。添加 By
属性会非常简单,这留给读者作为练习(应该不会超过几分钟)。
我们从 AnimationTimeline
派生一个类,它表示生成值的时间线(在我们的例子中,我们将生成 From
和 To
范围之间的 GridLength
值)。
namespace GridAnimationDemo
{
internal class GridLengthAnimation : AnimationTimeline
{
我们必须重写 TargetPropertyType
属性,该属性在 AnimationTimeline
中是 abstract
。这是一个 get
-only 属性,它返回将在支持值范围内进行动画处理的属性的类型。我们的实现很简单,返回 GridLength
对象的类型。
public override Type TargetPropertyType
{
get
{
return typeof(GridLength);
}
}
AnimationTimeLine
有一个 protected
构造函数,因此任何派生自它的动画对象都必须间接创建。动画类间接派生自 Freezable
,它定义了具有两种状态的对象——可变(未冻结)和不可变(冻结)。此类需要实现(覆盖)CreateInstanceCore
方法,该方法将用于构造动画(可冻结)对象。 CreateInstanceCore
将由 GetCurrentValueAsFrozenCore
调用,以返回当前对象的冻结克隆(该对象当时可能处于冻结状态,也可能不处于冻结状态)。同样,我们的实现非常简单。
protected override System.Windows.Freezable CreateInstanceCore()
{
return new GridLengthAnimation();
}
接下来,我们将添加两个依赖项属性来处理 From
和 To
属性。有关 MSDN 之外依赖项属性的精彩论述,请阅读 WPF 专家和 MVP Josh Smith 关于此主题的博客文章: Josh Smith 的依赖项属性。实现很简单,这里没有什么特别要做的。
static GridLengthAnimation()
{
FromProperty = DependencyProperty.Register("From", typeof(GridLength),
typeof(GridLengthAnimation));
ToProperty = DependencyProperty.Register("To", typeof(GridLength),
typeof(GridLengthAnimation));
}
public static readonly DependencyProperty FromProperty;
public GridLength From
{
get
{
return (GridLength)GetValue(GridLengthAnimation.FromProperty);
}
set
{
SetValue(GridLengthAnimation.FromProperty, value);
}
}
public static readonly DependencyProperty ToProperty;
public GridLength To
{
get
{
return (GridLength)GetValue(GridLengthAnimation.ToProperty);
}
set
{
SetValue(GridLengthAnimation.ToProperty, value);
}
}
现在剩下的就是覆盖 GetCurrentValue
并返回正在动画的属性的当前动画值。
public override object GetCurrentValue(object defaultOriginValue,
object defaultDestinationValue, AnimationClock animationClock)
{
double fromVal = ((GridLength)GetValue(GridLengthAnimation.FromProperty)).Value;
double toVal = ((GridLength)GetValue(GridLengthAnimation.ToProperty)).Value;
if (fromVal > toVal)
{
return new GridLength((1 - animationClock.CurrentProgress.Value) *
(fromVal - toVal) + toVal, GridUnitType.Star);
}
else
{
return new GridLength(animationClock.CurrentProgress.Value *
(toVal - fromVal) + fromVal, GridUnitType.Star);
}
}
我们所做的是根据 AnimationClock
对象的当前值(介于 0 和 1 之间)计算并返回一个渐变值。我们通过使用接受 GridUnitType
作为第二个参数的构造函数来创建一个 GridLength
对象,我们为此指定 Star
。就是这样——我们的 GridLengthAnimation
类已准备就绪,我们现在将看看它如何投入使用。
类用法
让我们看看如何通过程序代码和 Xaml 使用该类。
示例应用程序
示例项目有一个包含三行两列的网格,每个单元格都有一张图片。请注意,屏幕截图中显示并可在项目 zip 中找到的所有六张照片均由 Nish 拍摄,这些图片是免版税的,读者可以以任何合法方式重复使用。您可以单击六张图片中的任何一张,该单元格将动画填充窗口,而其他单元格将缩小尺寸直至消失。如果您单击最大化的图片,将发生反向动画——当前图片将恢复到其原始尺寸,其他单元格将同时明显增大,直到它们返回到起始位置。GridLengthAnimation
类在演示项目中通过程序代码使用,如下所示。
void image_MouseDown(object sender, MouseButtonEventArgs e)
{
Image image = sender as Image;
if (image != null)
{
int col = Grid.GetColumn(image);
int row = Grid.GetRow(image);
for (int indexRow = 0; indexRow < mainGrid.RowDefinitions.Count;
indexRow++)
{
if (indexRow != row)
{
GridLengthAnimation gla = new GridLengthAnimation();
gla.From = new GridLength(bSingleImageMode
? 0 : 1, GridUnitType.Star);
gla.To = new GridLength(bSingleImageMode
? 1 : 0, GridUnitType.Star); ;
gla.Duration = new TimeSpan(0, 0, 2);
mainGrid.RowDefinitions[indexRow].BeginAnimation(
RowDefinition.HeightProperty, gla);
}
}
for (int indexCol = 0;
indexCol < mainGrid.ColumnDefinitions.Count; indexCol++)
{
if (indexCol != col)
{
GridLengthAnimation gla = new GridLengthAnimation();
gla.From = new GridLength(bSingleImageMode
? 0 : 1, GridUnitType.Star);
gla.To = new GridLength(bSingleImageMode
? 1 : 0, GridUnitType.Star);
gla.Duration = new TimeSpan(0, 0, 2);
mainGrid.ColumnDefinitions[indexCol].BeginAnimation(
ColumnDefinition.WidthProperty, gla);
}
}
}
bSingleImageMode = !bSingleImageMode;
}
请注意,虽然演示使用了程序代码(因为它需要动态地将动画应用于点击的图像单元格),但您也可以像使用任何其他动画类一样从 Xaml 中使用它。另请注意,在示例代码中,我们如何迭代行和列并一个接一个地运行动画。对于更复杂的场景,您会想要创建一个 StoryBoard
并让所有动画并行运行,而不是像我们上面那样一个接一个地运行。
从 Xaml 使用
以下是一些示例 Xaml,展示了如何从 Xaml 使用 GridLengthAnimation
类来为网格的列宽设置动画。
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Name="Col0" Width="*"/>
<ColumnDefinition Name="Col1" Width="*"/>
</Grid.ColumnDefinitions>
<Button Name="button1">
One
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<proj:GridLengthAnimation
Storyboard.TargetName="Col1"
Storyboard.TargetProperty="Width"
From="*" To="2*" Duration="0:0:2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
<Button Name="button2" Grid.Column="1">Two</Button>
</Grid>
历史
- 2007 年 4 月 12 日 - 文章首次发布于 The Code Project