65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (51投票s)

2007年4月12日

CPOL

6分钟阅读

viewsIcon

236791

downloadIcon

7130

本文介绍如何对没有关联动画类的属性应用动画

引言

当 Christian 和 Nish 去年 7 月开始我们的 WPF 系列时,我们并没有计划在第 1 部分和第 2 部分之间间隔 9 个月。我们都忙于各种项目,并且一直拖延编写下一部分。好了,现在我们已经准备好 WPF 系列的第 2 部分了,正如他们所说——迟做总比不做好。在本文中,我们将讨论如何对没有关联动画类的属性应用动画,我们将特别关注使用 GridLength 属性对 Grid 控件的列和行进行动画处理。

网格列和行动画处理的麻烦

在我们开始为 GridLength 设置动画之前,我们先看看它与为 WidthOpacity 等属性设置动画有何不同。虽然我们希望您对 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 类型的属性(OpacityWidthLength 等都是 double 类型)。如果您查看 System.Windows.Media.Animation 命名空间,您会发现还有其他专门的动画类用于处理其他常见类型的值,例如 BooleanChar ByteColorPoint 等。接下来让我们看看 Grid 的列和行维度为何不能以这种方式进行动画处理。

为什么常规动画不能与网格一起工作?

ColumnDefinitionRowDefinition 类分别具有 WidthHeight 属性,类型为 GridLengthGridLength 是一个 struct,其目的是支持基于 Star 的单位以及基于像素的单位。Star 单位将维度指定为总可用空间的加权比例。因此,如果您有两个列,第一个的宽度为 {*},第二个的宽度为 {3*},则第一个列将占用包含面板总宽度的 25%,而第二个列将占用剩余的 75% 空间。这为我们使用网格提供了很大的灵活性,我们不需要指定硬编码值——这还有一个额外的好处,那就是将来更容易添加行和列。

由于 Grid 使用 GridLength 作为尺寸,其副作用是,我们无法将库中任何内置动画类与它一起使用,因为它们都没有打算支持 GridLength。但这并不意味着我们对此无能为力。我们可以(也将会)编写一个专门处理 GridLength 类型单位的动画类。

为 GridLength 编写自定义动画类

我们的目标是编写一个 GridLengthAnimation 类,它将支持基于 GridLength 属性的动画。为了使示例简单明了,我们只支持 FromTo 属性,而不支持 By 等其他类(如 DoubleAnimation)支持的属性。添加 By 属性会非常简单,这留给读者作为练习(应该不会超过几分钟)。

我们从 AnimationTimeline 派生一个类,它表示生成值的时间线(在我们的例子中,我们将生成 FromTo 范围之间的 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();
}

接下来,我们将添加两个依赖项属性来处理 FromTo 属性。有关 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
© . All rights reserved.