【WPF学习】第五十章 故事板

  正如上一章介绍,WPF动画经过一组动画类(Animation类)表示。使用少数几个熟悉设置相关信息,如开始值、结束值以及持续时间。这显然使得它们很是适合于XAML。不是很清晰的时:如何为特定的事件和属性关联动画,以及如何在正确的时间触发动画。html

  在全部声明式动画中都会用到以下两个要素:ide

  •   故事板。故事板是BeginAnimation()方法的XAML等价物。经过故事板将动画指定到合适的元素和属性。
  •   事件触发器。事件触发器响应属性变化或事件(如按钮的Click事件),并控制故事板。例如,为了开始动画,事件触发器必须开始故事板。

1、故事板学习

  故事板是加强的事件线,可用来分组多个动画,并且具备控制动画播放的能力——暂停、中止以及改变播放位置。然而,Storyboard类提供的最基本功能是,可以使用TargetProperty和TargetName属性指向某个特定属性和特定元素。换句话说,故事板在动画和但愿应用动画的属性之间架起了一座桥梁。动画

  下面的标记演示了如何定义用于管理DoubleAnimation的故事板:this

<Storyboard TargetName="cmdGrow" TargetProperty="Width">
     <DoubleAnimation From="160" To="300" Duration="0:0:5"></DoubleAnimation>
</Storyboard>

  TargetName和TargetProperty都是附加属性。这意味着能够直接将他们应用于动画,以下所示:编码

<Storyboard >
     <DoubleAnimation Storyboard.TargetName="cmdGrow" Storyboard.TargetProperty="Width" From="160" To="300" Duration="0:0:5">    
     </DoubleAnimation>
</Storyboard>

  上面的语法更经常使用,由于经过这种语法可在同一个故事板中放置几个动画,而且每一个动画可用于不一样的元素和属性。spa

  定义故事板是建立动画的第一步。为让故事板实际运行起来,还须要有事件触发器。设计

2、事件触发器3d

  在“【WPF学习】第三十七章 触发器 ”时第一次提到事件触发器。样式提供了一种将事件触发器关联到元素的方法。然而,可在以下4个位置定义事件触发器:code

  •   在样式中(Styles.Triggers集合)
  •   在数据目标中(DataTemplate.Triggers集合)
  •   在控件模板中(ControlTemplate.Triggers集合)
  •   直接在元素中定义事件触发器(FrameworkElement.Triggers集合)

  当建立事件触发器时,须要制定开始出发其的路由事件和由触发器执行的一个或多个动做。对于动画,最经常使用的动做是BeginStoryboard,该动做至关于调用BeginAnimation()方法。

  下面的示例使用按钮的Triggers集合为Click事件关联某个动画。当单击按钮时,该动画增加按钮:

<Button Margin="10" Name="cmdGrow" Height="40" Width="160" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button.Triggers>
            <EventTrigger RoutedEvent="Button.Click">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetProperty="Width" To="300" Duration="0:0:5">
                        </DoubleAnimation>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Button.Triggers>
        <Button.Content> Click and Make Me Grow </Button.Content>
    </Button>

  Storyboard.TargetProperty属性指定了但愿改变的属性(在这个示例中是Width属性)。若是没有提供类的名称,故事板使用其父元素,在此使用的是但愿扩展的按钮。若是但愿设置附加属性(如Canvas.Left或Canvas.Top),须要在括号中封装整个属性,以下所示:

<DoubleAnimation Storyboard.TargetName="(Canvas.Top)" .../>

  在这个示例中需不须要使用Storyboard.TargetName属性。当忽略该属性时,故事板使用父元素,在此是按钮。

  在这个示例中使用的声明式方法和前面演示的只使用代码的方法存在以下区别:To值被硬编码为300个单位,而不是相对于包含按钮的窗口的尺寸设置。若是但愿使用窗口宽度,须要使用数据绑定表达式,以下所示:

<DoubleAnimation Storyboard.TargetProperty="Width" To="{Binding ElementName=cmdGrow, Path=Width}" Duration="0:0:5">
</DoubleAnimation>

  这仍不能准确地获得所但愿的结果。在此,按钮从当前尺寸增大到窗口的完整宽度。只使用代码的方法使用一种简单的计算,将按钮扩大到比整个窗口宽度小30个单位的值。但XAML不支持内联计算。一种解决方法是构建可以自动完成工做的IValueConverter接口。以下所示的示例:

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows.Data; namespace Animation { public class ArithmeticConverter : IValueConverter { private const string ArithmeticParseExpression = "([+\\-*/]{1,1})\\s{0,}(\\-?[\\d\\.]+)"; private Regex arithmeticRegex = new Regex(ArithmeticParseExpression); public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (value is double && parameter != null) { string param = parameter.ToString(); if (param.Length > 0) { Match match = arithmeticRegex.Match(param); if (match != null && match.Groups.Count == 3) { string operation = match.Groups[1].Value.Trim(); string numericValue = match.Groups[2].Value; double number = 0; if (double.TryParse(numericValue, out number)) // this should always succeed or our regex is broken
 { double valueAsDouble = (double)value; double returnValue = 0; switch (operation) { case "+": returnValue = valueAsDouble + number; break; case "-": returnValue = valueAsDouble - number; break; case "*": returnValue = valueAsDouble * number; break; case "/": returnValue = valueAsDouble / number; break; } return returnValue; } } } } return null; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } }
ArithmeticConverter
<Window x:Class="Animation.XamlAnimation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Animation" Title="XamlAnimation" Height="300" Width="300">
    <Window.Resources>
        <local:ArithmeticConverter x:Key="converter"></local:ArithmeticConverter>
    </Window.Resources>
    <Button Padding="10" Name="cmdGrow" Height="40" Width="160" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button.Triggers>
            <EventTrigger RoutedEvent="Button.Click">
                <EventTrigger.Actions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetProperty="Width" To="{Binding ElementName=window,Path=Width,Converter={StaticResource converter},ConverterParameter=-30}" Duration="0:0:5"></DoubleAnimation>
                            <DoubleAnimation Storyboard.TargetProperty="Height" To="{Binding ElementName=window,Path=Height,Converter={StaticResource converter},ConverterParameter=-50}" Duration="0:0:5"></DoubleAnimation>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger.Actions>
            </EventTrigger>
        </Button.Triggers>
        <Button.Content> Click and Make Me Grow </Button.Content>
    </Button>
</Window>
XamlAnimation

  使用样式关联触发器

  FrameworkElement.Triggers集合有点奇怪,它仅支持事件触发器。其余触发器集合(Style.Triggers、DataTemplate.Triggers与ControlTemplate.Triggers)的功能更强大,他们支持三种基本类型的WPF触发器:属性触发器、数据触发器以及事件触发器。

  使用事件触发器是关联动画的最经常使用方式,但并非惟一的选择。若是使用位于样式、数据模板或控件模板中的Triggers集合,还可建立当属性值发生变化时进行响应的属性触发器。例如,下面的样式复制了前面显示的示例。当IsPressed属性为true时,该样式触发一个故事板:

<Window.Resources>

        <Style x:Key="GrowButtonStyle">
            <Style.Triggers>
                <Trigger Property="Button.IsPressed" Value="True">
                    <Trigger.EnterActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation Storyboard.TargetProperty="Width" To="250" Duration="0:0:5"></DoubleAnimation>
                            </Storyboard>
                        </BeginStoryboard>
                    </Trigger.EnterActions>
                </Trigger>
            </Style.Triggers>
        </Style>

    </Window.Resources>

  可以使用两种方式为属性触发器关联动做。可以使用Trigger.EnterActions设置当属性改变到指定的数值时但愿执行的动做(在上面的示例中,当IsPressed属性值变为true时),也可使用Trigger.ExitActions设置当属性改变回原来的数值时执行的动做(当IsPressed属性的值变回false时)。这是一种封装一堆互补动画的简便方法。

  下面的按钮使用上面显示的样式:

<Button Padding="10" Name="cmdGrow" Height="40" Width="160" Style="{StaticResource GrowButtonStyle}" HorizontalAlignment="Center" VerticalAlignment="Center"> Click and Make Me Grow </Button>

  请记住,不见得在样式中使用属性触发器。也可以使用事件触发器,就像在前面介绍的那样。最后,不见得以与使用样式的按钮相分离的方式定义样式(也可以使用内联样式设置Button.Style属性)。可是这种两部分相分离的方法更经常使用,而且提供了为多个元素应用相同的灵活性。

3、重叠动画

  故事板提供了改变处理重叠动画方式的能力——换句话说,决定第二个动画什么时候被应用到已经具备一个正在运行的动画的属性上。可以使用BeginStoryboard.HandoffBehavior属性改变处理重叠动画的方式。

  一般,当两个动画相互重叠时,第二个动画会当即覆盖第一个动画。这种行为就是所谓的“快照并替换”(由HandoffBehavior枚举中的SnapshotAndReplace值表示)。当第二个动画开始时,第二个动画获取属性当前值(基于第一个动画)的快照,中止动画,并用新动画替换第一个动画。

  另外一个HandoffBehavior选项是Compose,这种方式将第二个动画融合到第一个动画的时间线中。例如,分析ListBox示例的修改版本,当缩小按钮时使用HandoffBehavior.Compose:

<EventTrigger RoutedEvent="ListBoxItem.MouseLeave">
     <EventTrigger.Actions>
          <BeginStoryboard HandoffBehavior="Compose">
               <Storyboard>
                   <DoubleAnimation Storyboard.TargetProperty="FontSize" BeginTime="0:0:0.5" Duration="0:0:0.2"></DoubleAnimation>
                </Storyboard>
            </BeginStoryboard>
      </EventTrigger.Actions>
</EventTrigger>

  如今,若是将鼠标移到ListBoxItem对象上,而后在移开,将看到不一样的行为。当鼠标移开项时,项会继续扩张,这种行为很是明显,知道第二个动画到达其0.5秒得开始时间延迟,而后,第二个动画会缩小按钮。若是不使用Compose行为,在第二个动画开始以前的0.5秒得时间间隔内,按钮会处于等待状态,并固定为当前尺寸。

  使用组合的HandoffBehavior行为须要更大开销。这是由于当第二个动画开始时,用于运行原来动画的时钟不能被释放。相反,这个时钟会继续保持存活,知道ListBoxItem对象被垃圾回收或为相同的属性应用新的动画为止。

4、同步的动画

  Storyboard类间接地继承自TimelineGroup类,因此Storyboard类能包含多个动画,最使人高兴的是,这些动画能够做为一组进行管理——这意味着他们在同一时间开始。

  为查看这个一个示例,分析下面的故事板。它开始两个动画,一个动画用于按钮的Width属性,而另外一个动画用于按钮的Height属性。由于动画被分组到故事板中,它们共同增长按钮的尺寸,因此可获得比在代码中经过简单地屡次调用BeginAnimation()方法获得的效果更趋向同步的效果。

<EventTrigger RoutedEvent="Button.Click">
       <EventTrigger.Actions>
            <BeginStoryboard>
                 <Storyboard>
                     <DoubleAnimation Storyboard.TargetProperty="Width" To="300" Duration="0:0:5"></DoubleAnimation>
                     <DoubleAnimation Storyboard.TargetProperty="Height" To="300" Duration="0:0:5"></DoubleAnimation>
                  </Storyboard>
             </BeginStoryboard>
       </EventTrigger.Actions>
</EventTrigger>

  在这个示例中,两个动画具备相同的持续时间,但这并非必须的,对于在不一样时间结束的动画,惟一须要考虑的是它们的FillBehavior行为。若是一个动画的FillBehavior属性被设置为HoldEnd,它会保持值直到故事板中全部的动画都结束。若是故事板的FillBehavior属性是HoldEnd,最后那个动画的值将被永久保存(直到使用新的动画替换这个动画或手动删除了这个动画)。

  上一章列出的Timeline类的属性开始变得特别有用。例如,可经过SpeedRatio属性使故事板中的某个动画比其余动画更快,也可使用BeginTime属性相对于一个动画来编译另外一个动画的开始时间,使该动画在特定的时间点开始。

5、控制播放

  到目前位置,已在事件触发器中使用了一个动做——加载动画的BeginStoryboard动做。然而,一旦建立故事板,就能够用在其余动做控制故事板。这些工做类都继承自ControllableStoryboardAction类,下表列出了这些类。

表 控制故事板的动做类

   帮助文档中没有记载会妨碍使用这些动做的内容。为成功地执行这些动做,必须在同一个Triggers集合中定义全部触发器。若是将BeginStoryboard动做的触发器和PauseStoryboard动做的触发器放置到不一样集合中,PauseStoryboard动做就没法工做。为查看须要使用的设计,分析示例是有帮助的。

  例如,分析下图中显示的窗口。该窗口使用一个网格在彻底相同的位置精确地重叠了两个Image元素。最初,只有最顶部的图像可见。但当动画运行是,该图像从1到0逐渐地增长透明度,最终使夜间的场景彻底盖过白天场景。效果就像是图像从白天变换到黑夜,就像连续的随时间流逝的照片。

   下面的标记定义了包含两个图像的Grid控件:

<Grid>
       <Image Source="night.jpg"></Image>
       <Image Source="day.jpg" Name="imgDay"></Image>
</Grid>

  下面是从一幅图像淡入到另外一幅图像的动画:

<DoubleAnimation Storyboard.TargetName="imgDay" Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:10"></DoubleAnimation>

  为增长这个示例的趣味性,还在底部提供了几个用于控制动画播放的按钮。使用这些按钮,可执行典型的媒体播放器动做,如暂停、恢复播放以及中止(可添加其余按钮来改变速度系数以及挑选特定的时间)。

  下面的标记定义了这些按钮:

<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center">
            <Button Name="cmdStart">Start</Button>
            <Button Name="cmdPause">Pause</Button>
            <Button Name="cmdResume">Resume</Button>
            <Button Name="cmdStop">Stop</Button>
            <Button Name="cmdMiddle">Move To Middle</Button>
</StackPanel>

  一般,可选择在每一个按钮的Triggers集合中放置事件触发器。然而,在前面已解释过,对于动画这种方法不能工做。最简单的解决方法是在一个地方定义全部事件触发器,例如,在包含元素的Triggers集合中,使用EventTrigger.SourceName属性关联这些事件触发器。只要SourceName属性和为按钮设置的Name属性相匹配,触发器就会应用到恰当的按钮上。

  这个示例中,可以使用包含这些按钮的StackPanel面板的Triggers集合。然而,使用顶级元素(在这个示例中是窗口)的Triggers集合一般最简单。这样,就可在用户界面中将按钮移到不一样的位置,而不会禁用他们的功能。

<Window.Triggers>
        <EventTrigger SourceName="cmdStart" RoutedEvent="Button.Click">
            <BeginStoryboard Name="fadeStoryboardBegin">
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="imgDay" Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:10"></DoubleAnimation>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
        <EventTrigger SourceName="cmdPause" RoutedEvent="Button.Click">
            <PauseStoryboard BeginStoryboardName="fadeStoryboardBegin">
            </PauseStoryboard>
        </EventTrigger>
        <EventTrigger SourceName="cmdResume" RoutedEvent="Button.Click">
            <ResumeStoryboard BeginStoryboardName="fadeStoryboardBegin"></ResumeStoryboard>
        </EventTrigger>
        <EventTrigger SourceName="cmdStop" RoutedEvent="Button.Click">
            <StopStoryboard BeginStoryboardName="fadeStoryboardBegin"></StopStoryboard>
        </EventTrigger>
        <EventTrigger SourceName="cmdMiddle" RoutedEvent="Button.Click">
            <SeekStoryboard BeginStoryboardName="fadeStoryboardBegin" Offset="0:0:5"></SeekStoryboard>
        </EventTrigger>
    </Window.Triggers>

  注意,必须为BeginStoryboard动做指定名称(在这个示例中,名称是fadeStoryboardBegin)。其余触发器经过为BeginStoryboardName属性指定这个名称,链接到相同的故事板。

  当使用故事板动做时将遇到限制。他们提供的属性(如SeekStoryboard.Offset和SetStoryboardSpeedRatio.SpeedRatio属性)不是依赖性项属性,这会限制使用数据绑定表达式。例如,不能自动读取Slider.Value属性值并将其应用到SetStoryboardSpeedRatio.SpeedRatio动做,由于SpeedRatio属性不接受数据绑定表达式。可能认为经过使用Storyboard对象的SpeedRatio属性来解决这个问题。但这是行不一样的,当动画开始时,读取SpeedRatio值并建立一个动画时钟。此后,即便改变了SpeedRatio属性的值,动画也仍会保持正常的速度。

  若是但愿动态调整速度或位置,惟一的解决方法是使用代码。Storyboard类中的方法提供了与故事板触发器相同的功能,包括Begin()、Pause()、Resume()、Seek()、Stop()、SkipToFill()、SetSpeedRatio()以及Remove()方法。

  要访问Storyboard对象,必须在标记中设置其Name属性:

<Storyboard Name="fadeStoryboard">

  如今只须要编写恰当的事件处理程序,并使用Storyboard对象的方法(请记住,简单地改变故事板的属性(好比SpeedRatio)是没有任何效果的,它们仅配置当动画开始时将要使用的设置)。

  当拖动Slider控件上的滑块时,下面的事件处理程序会进行响应。该事件处理程序获取滑动条的值(范围是0~3),并使用该数值应用新的速率:

private void sldSpeed_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { fadeStoryboard.SetSpeedRatio(this, sldSpeed.Value); }

  注意,SetSpeedRatio()方法须要两个参数。第一个参数是顶级动画容器(在这个示例中,是指当前窗口)。全部故事板方法都须要这个引用。第二个参数是新的速率。

6、监视动画进度

  上一节显示的动画播放器仍缺乏一个在大多数媒体播放器中都具备的功能——肯定当前位置的能力。为使这个动画播放器更加精致,可添加一些文原本显示时间的流逝,并添加进度条来指示动画只需的速度。下图显示了使用这两个细节的动画播放器的修改版。

   添加这些细节至关简单。首先须要使用TextBlock元素显示时间,然后须要使用ProgressBar控件显示图形进度条,可能认为,可以使用数据绑定表达式设置TextBlock值和ProgressBar内容,但这是行不一样的。由于从故事板中检索当前动画时钟相关的惟一方式是使用方法,如GetCurrentTime()和GetCurrentProgress()。没法从属性中获取相同的信息。

  最简单的解决方法是响应下表中列出的某个故事板事件。

表 故事板事件

  名    称    说    明
Completed 动画已经到达终点
CurrentGlobalSpeedInvalidated 速度发生了变化,或者动画被暂停、从新开始、中止或移到某个新的位置。当动画时钟反转时(在可反转动画的终点),以及当动画加速和减速时,也会引起该事件
CurrentStateInvalidated 动画已经开始或结束
CurrentTimeInvalidated 动画时钟已经向前移动了一个步长,正在更改动画。当动画开始、中止或结束时也会引起该事件
RemoveRequested 动画正在被移除。使用动画的属性随后会返回为原来的值

  这个示例须要使用CurrentTimeInvalidated事件,每次向前移动动画时钟都会引起该事件(一般,每秒移动60此,但若是执行的代码须要更长时间,可能会丢失时钟刻度)。

  当引起CurrentTimeInvalidated事件时,发送者是Clock对象(Clock类位于System.Windows.Media.Animation名称空间)。能够经过Clock对象检索当前时间,当前时间使用TimeSpan对象表示;而且可检索当前进度,当前进度使用0~1之间的数值表示。

  下面的代码更新标签和进度条:

private void storyboard_CurrentTimeInvalidated(object sender, EventArgs e) { // Sender is the clock that was created for this storyboard.
            Clock storyboardClock = (Clock)sender; if (storyboardClock.CurrentProgress == null) { lblTime.Text = "[[ stopped ]]"; progressBar.Value = 0; } else { lblTime.Text = storyboardClock.CurrentTime.ToString(); progressBar.Value = (double)storyboardClock.CurrentProgress; } }

 

原文出处:https://www.cnblogs.com/Peter-Luo/p/12380975.html

相关文章
相关标签/搜索