アニメーションのヒントとテクニック

WPF でアニメーションを扱うときに、アニメーションのパフォーマンスを向上させて不満を解消するためのヒントとテクニックがいくつかあります。

一般的な問題

スクロール バーまたはスライダーの位置をアニメーション化するとフリーズする

FillBehaviorHoldEnd (既定値) であるアニメーションを使ってスクロール バーまたはスライダーの位置をアニメーション化すると、ユーザーはスクロール バーまたはスライダーを移動できなくなります。 原因は、アニメーションが終了しても、ターゲット プロパティの基底値をまだオーバーライドしているためです。 アニメーションによってプロパティの現在の値がオーバーライドされないようにするには、それを削除するか、FillBehaviorStop に指定します。 詳細と例については、「ストーリーボードを使用してアニメーション化した後にプロパティを設定する」をご覧ください。

アニメーションの出力のアニメーション化の効果がない

別のアニメーションの出力であるオブジェクトをアニメーション化することはできません。 たとえば、ObjectAnimationUsingKeyFrames を使用して RectangleFillRadialGradientBrush から SolidColorBrush にアニメーション化する場合、RadialGradientBrushSolidColorBrush のいずれのプロパティもアニメーション化することはできません。

プロパティをアニメーション化した後にその値を変更できない

場合によっては、アニメーションが完了した後でも、アニメーション化の後にプロパティの値を変更できないように見えることがあります。 原因は、アニメーションが終了しても、プロパティの基底値をまだオーバーライドしているためです。 アニメーションによってプロパティの現在の値がオーバーライドされないようにするには、それを削除するか、FillBehaviorStop に指定します。 詳細と例については、「ストーリーボードを使用してアニメーション化した後にプロパティを設定する」をご覧ください。

タイムラインを変更しても効果がない

ほとんどの Timeline プロパティはアニメーション化可能で、データ バインドできますが、アクティブな Timeline のプロパティ値を変更しても効果がないように見えます。 これは、Timeline が開始されると、タイミング システムが Timeline のコピーを作成し、それを使用して Clock オブジェクトを作成するためです。 元を変更しても、システムのコピーには影響しません。

Timeline に変更を反映させるには、クロックを再生成し、それを使って、以前に作成されたクロックを置き換える必要があります。 クロックは、自動的には再生成されません。 タイムラインの変更を適用するいくつかの方法を次に示します。

  • タイムラインが Storyboard であるか、それに属している場合は、BeginStoryboard または Begin メソッドを使用してストーリーボードを再適用することで、変更を反映させることができます。 これには、アニメーションが再起動されるという副作用があります。 コードでは、Seek メソッドを使用して、ストーリーボードを前の位置に戻すことができます。

  • BeginAnimation メソッドを使用してプロパティにアニメーションを直接適用した場合は、もう一度 BeginAnimation メソッドを呼び出して、変更されたアニメーションを渡します。

  • クロック レベルで直接操作している場合は、新しいクロック セットを作成して適用し、それらを使って、生成されたクロックの以前のセットを置換します。

タイムラインとクロックについて詳しくは、「アニメーションとタイミング システムの概要」をご覧ください。

FillBehavior.Stop が期待どおりに動作しない

あるアニメーションを別のアニメーションに "引き渡す" ときなどに、FillBehavior プロパティを Stop に設定しても効果がないように見える場合があります。これは、HandoffBehavior の設定が SnapshotAndReplace になっているためです。

次の例では、CanvasRectangle、および TranslateTransform を作成します。 TranslateTransform は、RectangleCanvas の周りを移動するようにアニメーション化されます。

<Canvas Width="600" Height="200">
  <Rectangle 
    Canvas.Top="50" Canvas.Left="0" 
    Width="50" Height="50" Fill="Red">
    <Rectangle.RenderTransform>
      <TranslateTransform 
        x:Name="MyTranslateTransform" 
        X="0" Y="0" />
    </Rectangle.RenderTransform>
  </Rectangle>
</Canvas>

このセクションの例では、前のオブジェクトを使って、FillBehavior プロパティが想定どおり動作しないケースをいくつか示します。

複数のアニメーションでの FillBehavior="Stop" と HandoffBehavior

アニメーションが 2 番目のアニメーションで置き換えられるときに、FillBehavior プロパティを無視するように見えることがあります。 次の例を見てみましょう。ここでは、2 つの Storyboard オブジェクトを作成し、それらを使用して、前の例で示したものと同じ TranslateTransform をアニメーション化しています。

1 つ目の Storyboard である B1 は、TranslateTransformX プロパティを 0 から 350 にアニメーション化します。これにより、四角形は 350 ピクセル右に移動します。 アニメーションが継続時間の最後に達し、再生が停止すると、X プロパティは元の値である 0 に戻ります。 その結果、四角形は右に 350 ピクセル移動し、元の位置に移動します。

<Button Content="Start Storyboard B1">
  <Button.Triggers>
    <EventTrigger RoutedEvent="Button.Click">
      <BeginStoryboard>
        <Storyboard x:Name="B1">
          <DoubleAnimation 
            Storyboard.TargetName="MyTranslateTransform"
            Storyboard.TargetProperty="X"
            From="0" To="350" Duration="0:0:5"
            FillBehavior="Stop"
            />
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Button.Triggers>
</Button>

2 つ目の Storyboard である B2 も、同じ TranslateTransformX プロパティをアニメーション化します。 この Storyboard では、アニメーションの To プロパティのみが設定されているため、アニメーションは、アニメーション化するプロパティの現在の値を開始値として使用します。


<!-- Animates the same object and property as the preceding
     Storyboard. -->
<Button Content="Start Storyboard B2">
  <Button.Triggers>
    <EventTrigger RoutedEvent="Button.Click">
      <BeginStoryboard>
        <Storyboard x:Name="B2">
          <DoubleAnimation 
            Storyboard.TargetName="MyTranslateTransform"
            Storyboard.TargetProperty="X"
            To="500" Duration="0:0:5" 
            FillBehavior="Stop" />
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Button.Triggers>
</Button>

最初の Storyboard の再生中に 2 番目のボタンをクリックすると、次の動作が想定されます。

  1. アニメーションでは FillBehaviorStop に設定されているため、最初のストーリーボードが終了し、四角形は元の位置に戻されます。

  2. 2 つ目のストーリーボードが反映され、現在位置である 0 から 500 にアニメーション化します。

しかし、これは行われません。 代わりに、四角形は戻らずに、右に移動し続けます。 その理由は、2 番目のアニメーションは最初のアニメーションの現在の値を開始値として使い、その値から 500 までアニメーション化するためです。 SnapshotAndReplaceHandoffBehavior が使用されているために 2 番目のアニメーションが 1 番目を置き換えるときは、1 番目のアニメーションの FillBehavior は関係ありません。

FillBehavior と完了イベント

次の例では、StopFillBehavior が効果がないと思われるもう 1 つのシナリオを示します。 ここでも、例はストーリーボードを使用して、TranslateTransformX プロパティを 0 から 350 へとアニメーション化します。 ただし、今回はこの例では Completed イベントを登録します。

<Button Content="Start Storyboard C">
  <Button.Triggers>
    <EventTrigger RoutedEvent="Button.Click">
      <BeginStoryboard>
        <Storyboard Completed="StoryboardC_Completed">
          <DoubleAnimation 
            Storyboard.TargetName="MyTranslateTransform"
            Storyboard.TargetProperty="X"
            From="0" To="350" Duration="0:0:5"
            FillBehavior="Stop" />
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Button.Triggers>
</Button>

Completed イベント ハンドラーは、同じプロパティを現在の値から 500 へとアニメーション化する別の Storyboard を開始します。

private void StoryboardC_Completed(object sender, EventArgs e)
{

    Storyboard translationAnimationStoryboard =
        (Storyboard)this.Resources["TranslationAnimationStoryboardResource"];
    translationAnimationStoryboard.Begin(this);
}
Private Sub StoryboardC_Completed(ByVal sender As Object, ByVal e As EventArgs)

    Dim translationAnimationStoryboard As Storyboard = CType(Me.Resources("TranslationAnimationStoryboardResource"), Storyboard)
    translationAnimationStoryboard.Begin(Me)
End Sub

次に示すのは、2 番目の Storyboard をリソースとして定義するマークアップです。

<Page.Resources>
  <Storyboard x:Key="TranslationAnimationStoryboardResource">
    <DoubleAnimation 
      Storyboard.TargetName="MyTranslateTransform"
      Storyboard.TargetProperty="X"
      To="500" Duration="0:0:5" />
  </Storyboard>
</Page.Resources>

Storyboard を実行すると、TranslateTransformX プロパティは 0 から 350 へとアニメーション化され、完了すると 0 に戻り (FillBehavior 設定が Stop になっているため)、その後、0 から 500 へとアニメーション化されると予測されます。 そうではなく、TranslateTransform は 0 から 350 へ、それから 500 へとアニメーション化します。

これは、WPF でのイベントの発生順序が原因です。また、プロパティ値がキャッシュされ、プロパティが無効にされない限り再計算されないことも原因です。 Completed イベントはルート タイムライン (最初の Storyboard) によってトリガーされたため、最初に処理されます。 この時点で、X プロパティはまだ無効になっていないため、アニメーション化された値を返します。 2 番目の Storyboard は、キャッシュされた値を開始値として使用してアニメーション化を開始します。

パフォーマンス

アニメーションがページからの移動後も実行され続ける

実行中のアニメーションを含む Page から離れると、それらのアニメーションは Page がガベージ コレクションされるまで再生を続けます。 使っているナビゲーション システムによっては、離れた後のページが無期限にメモリに残り、その間、そのアニメーションでリソースが消費されることがあります。 これは、ページに常時実行 ("アンビエント") アニメーションが含まれる場合に最も顕著です。

このため、Unloaded イベントを使って、ページから離れるときにアニメーションを削除することをお勧めします。

アニメーションを削除する別の方法もあります。 次の手法を使って、Storyboard に属するアニメーションを削除できます。

次の手法は、アニメーションの開始方法に関係なく使用できます。

  • 特定のプロパティからアニメーションを削除するには、BeginAnimation(DependencyProperty, AnimationTimeline) メソッドを使用します。 アニメーション化するプロパティを 1 番目のパラメーターとして指定し、null を 2 番目として指定します。 これにより、すべてのアニメーション クロックがプロパティから削除されます。

プロパティをアニメーション化するさまざまな方法について詳しくは、「プロパティ アニメーションの手法の概要」をご覧ください。

HandoffBehavior を使うとシステム リソースが消費される

ComposeHandoffBehavior を使用してプロパティに StoryboardAnimationTimeline、または AnimationClock を適用すると、そのプロパティに以前関連付けられていたすべての Clock オブジェクトがシステム リソースを消費し続けます。タイミング システムは、これらのクロックを自動的には削除しません。

Compose を使って大量のクロックを適用するときのパフォーマンスの問題を回避するには、アニメーション化されたプロパティから、構成クロックを完了後に削除する必要があります。 クロックを削除する方法はいくつかあります。

  • プロパティからすべてのクロックを削除するには、アニメーション化されたオブジェクトの ApplyAnimationClock(DependencyProperty, AnimationClock) または BeginAnimation(DependencyProperty, AnimationTimeline) メソッドを使用します。 アニメーション化するプロパティを 1 番目のパラメーターとして指定し、null を 2 番目として指定します。 これにより、すべてのアニメーション クロックがプロパティから削除されます。

  • 特定の AnimationClock をクロックの一覧から削除するには、AnimationClockController プロパティを使用して ClockControllerを取得し、次に RemoveClockController メソッドを呼び出します。 これは通常、クロックの Completed イベント ハンドラーで実行されます。 ClockController によって制御できるのはルート クロックのみであることに注意してください。子クロックの Controller プロパティは null を返します。 クロックの有効期間が永久の場合は Completed イベントが呼び出されないことにも注意してください。 その場合は、ユーザーが Remove を呼び出すタイミングを決定する必要があります。

これは主に、有効期間が長いオブジェクトでのアニメーションの問題です。 オブジェクトがガベージ コレクションされる場合は、そのクロックも切断されて、ガベージ コレクションされます。

クロック オブジェクトについて詳しくは、「アニメーションとタイミング システムの概要」をご覧ください。

関連項目