理解ControlTemplate中的VisualTransition

1. 前言

VisualTransition是控件模板中的重要组成部分,无论是自定义控件或者修改控件样式都会接触到VisualTransition。明明这么重要,博客园上好像都没多少关于VisualTransition的主题。

2. 什么是VisualTransition

VisualTransition动画定义VisualState之前切换时的过渡行为,包括过渡时间和过渡动画。

VisualTransition的类定义如下:

[ContentProperty(Name = "Storyboard")] 
public class VisualTransition : DependencyObject, IVisualTransition 
{ public VisualTransition(); 
// 摘要: 
//     获取或设置要转换为的 Windows.UI.Xaml.VisualState 的名称。 
public string To { get; set; } 
// 
// 摘要: 
//     获取或设置在发生转换时运行的 Windows.UI.Xaml.Media.Animation.Storyboard。 
public Storyboard Storyboard { get; set; } 
// 
// 摘要: 
//     获取或设置应用于生成的动画的缓动函数。 
public EasingFunctionBase GeneratedEasingFunction { get; set; } 
// 
// 摘要: 
//     获取或设置从一种状态转换到另一种状态所花的时间,以及任何隐式过渡动画应作为过渡行为的一部分运行的时间 
public Duration GeneratedDuration { get; set; } 
// 
// 摘要: 
//     获取或设置要转换的 Windows.UI.Xaml.VisualState 的名称。 
public string From { get; set; }
}

3.为什么使用VisualTransition

虽然自WPF4以来VisualTransition一直都存在,但很多人还是习惯这样写VisualState:

<VisualStateGroup x:Name="CommonStates"> 
<VisualState x:Name="Normal" /> 
<VisualState x:Name="PointerOver"> 
<Storyboard> 
<DoubleAnimation Storyboard.TargetProperty="Opacity"  
Storyboard.TargetName="PointOverElement"  
Duration="0"  
To="1" /> 
</Storyboard> 
</VisualState> 
<VisualState x:Name="Pressed"> 
<Storyboard> 
<DoubleAnimation Storyboard.TargetProperty="Opacity"  
Storyboard.TargetName="PressElement"  
Duration="0"  
To="1" /> 
</Storyboard> 
</VisualState> 
<VisualState x:Name="Disabled" /> 
</VisualStateGroup>

正确的做法应该是这样:

<VisualStateGroup x:Name="CommonStates"> 
<VisualStateGroup.Transitions> 
<VisualTransition To="PointerOver"> 
<Storyboard> 
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)"  
Storyboard.TargetName="PointOverElement"> 
<EasingDoubleKeyFrame KeyTime="0"  
Value="0" /> 
<EasingDoubleKeyFrame KeyTime="0:0:2"  
Value="1"> 
<EasingDoubleKeyFrame.EasingFunction> 
<CubicEase EasingMode="EaseOut" /> 
</EasingDoubleKeyFrame.EasingFunction> 
</EasingDoubleKeyFrame> 
</DoubleAnimationUsingKeyFrames> 
</Storyboard> 
</VisualTransition> 
<VisualTransition To="Pressed"> 
<Storyboard> 
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)"  
Storyboard.TargetName="PressElement"> 
<EasingDoubleKeyFrame KeyTime="0"  
Value="0" /> 
<EasingDoubleKeyFrame KeyTime="0:0:2"  
Value="1"> 
<EasingDoubleKeyFrame.EasingFunction> 
<CubicEase EasingMode="EaseOut" /> 
</EasingDoubleKeyFrame.EasingFunction> 
</EasingDoubleKeyFrame> 
</DoubleAnimationUsingKeyFrames> 
</Storyboard> 
</VisualTransition> 
<VisualTransition To="Disabled"> 
<Storyboard Completed="Storyboard_Completed">
</Storyboard> 
</VisualTransition> 
</VisualStateGroup.Transitions> 
<VisualState x:Name="Normal" /> 
<VisualState x:Name="PointerOver"> 
<Storyboard> 
<DoubleAnimation Storyboard.TargetProperty="Opacity"  
Storyboard.TargetName="PointOverElement"  
Duration="0"  
To="1" /> 
</Storyboard> 
</VisualState> 
<VisualState x:Name="Pressed"> 
<Storyboard> 
<DoubleAnimation Storyboard.TargetProperty="Opacity"  
Storyboard.TargetName="PressElement"  
Duration="0"  
To="1" /> 
</Storyboard> 
</VisualState> 
<VisualState x:Name="Disabled" /> 
</VisualStateGroup>

可以看到VisualState中的Storyboard只用于定义VisualState的最终可视状态,而在VIsualState间转换时用户看到的是VisualTransition 中定义的Storyboard。但这样的话两处的Storyboard不就重复了?带着这个疑问很多年,微软终于给出了另一种方案VisualState.Setters:

<VisualStateGroup x:Name="CommonStates"> 
<VisualStateGroup.Transitions>
 ... 
</VisualStateGroup.Transitions> 
<VisualState x:Name="Normal" /> 
<VisualState x:Name="PointerOver"> 
<VisualState.Setters> 
<Setter Target="PointOverElement.(UIElement.Opacity)"  
Value="1" /> 
</VisualState.Setters> 
</VisualState> 
<VisualState x:Name="Pressed"> 
<VisualState.Setters> 
<Setter Target="PressElement.(UIElement.Opacity)"  
Value="1" /> 
</VisualState.Setters> 
</VisualState> 
<VisualState x:Name="Disabled" /> 
</VisualStateGroup>

这样VisualState的做法就十分清晰明了:

  • 代码使用VisualStateManager控制控件当前的VisualState;
  • VisualState.Setters定义这个VisualState最终在UI上如何呈现;
  • VisualState间的过渡动画由VisualTransition定义;

4. 怎么使用VisualTransition

4.1 隐式转换

不使用Storyboard的VisualTransition称为隐式转换:

<VisualStateGroup.Transitions > 
<VisualTransition GeneratedDuration="0:0:3"/> 
</VisualStateGroup.Transitions>

如上面这段XAML中的VisualTransition ,它指定VisualStateGroup中所有VisualState之间的过渡时间都是3秒,在这3秒中VisualState中的Double、Point和Color使用默认的线性插值方式进行动画转换。而其它值,如Visibility,则不可以使用隐式转换。

这段XAML在Blend中对应“状态”面板里VisualStateGroup的“默认过渡”。

隐式转换可以进一步设置其它属性,如以下XAML:

<VisualStateGroup.Transitions> 
<VisualTransition To="PointerOver"  
GeneratedDuration="0:0:3"> 
<VisualTransition.GeneratedEasingFunction> 
<ExponentialEase EasingMode="EaseOut" /> 
</VisualTransition.GeneratedEasingFunction> 
</VisualTransition> <VisualTransition From="PointerOver"  
To="Pressed"  
GeneratedDuration="0:0:3"> 
<VisualTransition.GeneratedEasingFunction> 
<ExponentialEase EasingMode="EaseOut" /> 
</VisualTransition.GeneratedEasingFunction> 
</VisualTransition> 
</VisualStateGroup.Transitions>

这段XAML中VisualTransition指定了以下三种属性:

  • From和To,转换的旧状态和新状态,可以单独指定。

  • 动画的缓动函数。

4.2 使用Storyboard

当隐式转换不能满足需求,可以使用Storyboard指定转换的动画。这时Storyboard不需要设置FillBehavior="HoldEnd",因为Storyboard结束后将保持VisualState设置的最终状态。

<VisualTransition To="PointerOver"> 
<Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty=
"(UIElement.Visibility)"  
Storyboard.TargetName="PointOverElement"> 
<DiscreteObjectKeyFrame KeyTime="0"> 
<DiscreteObjectKeyFrame.Value> 
<Visibility>Visible</Visibility> 
</DiscreteObjectKeyFrame.Value> 
</DiscreteObjectKeyFrame> 
</ObjectAnimationUsingKeyFrames> 
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=
"(UIElement.Opacity)"  
Storyboard.TargetName="PointOverElement"> 
<EasingDoubleKeyFrame KeyTime="0"  
Value="0" /> 
<EasingDoubleKeyFrame KeyTime="0:0:2"  
Value="1"> 
<EasingDoubleKeyFrame.EasingFunction> 
<CubicEase EasingMode="EaseOut" /> 
</EasingDoubleKeyFrame.EasingFunction> 
</EasingDoubleKeyFrame> 
</DoubleAnimationUsingKeyFrames> 
</Storyboard> 
</VisualTransition> 
<VisualTransition To="Pressed"> 
<Storyboard> 
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty=
"(UIElement.Visibility)"  
Storyboard.TargetName="PressElement"> 
<DiscreteObjectKeyFrame KeyTime="0"> 
<DiscreteObjectKeyFrame.Value> 
<Visibility>Visible</Visibility> 
</DiscreteObjectKeyFrame.Value> 
</DiscreteObjectKeyFrame> 
</ObjectAnimationUsingKeyFrames> 
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=
"(UIElement.Opacity)"  Storyboard.TargetName="PressElement"> 
<EasingDoubleKeyFrame KeyTime="0"  
Value="0" /> 
<EasingDoubleKeyFrame KeyTime="0:0:2"  
Value="1"> 
<EasingDoubleKeyFrame.EasingFunction> 
<CubicEase EasingMode="EaseOut" /> 
</EasingDoubleKeyFrame.EasingFunction> 
</EasingDoubleKeyFrame> 
</DoubleAnimationUsingKeyFrames> 
</Storyboard> 
</VisualTransition>
 

5. 为什么有时候VisualTransition没有生效

ControlTemplate在VisualState之间切换是靠下面这个函数控制的:

// 
// 摘要: 
//     通过按名称请求新的 Windows.UI.Xaml.VisualState 来在两个状态之间转换控件。 
// 
// 参数: 
//   control: 
//     要进行状态过渡的控件。 
// 
//   stateName: 
//     要过渡到的状态。 
// 
//   useTransitions: 
//     如果使用 Windows.UI.Xaml.VisualTransition 在各状态之间转换,则为 **true**。 如果跳过使用转换并直接转到请求的状态,则为 
//     **false**。 默认值为 **false**。 
// // 返回结果: 
//     如果控件成功转换到新状态或者已经在使用该状态,则为 **true**;否则为 **false**。 
public static bool GoToState(Control control, string stateName, bool useTransitions);

如果useTransitions这个参数为false,则VisualState之间切换时不会使用VisualTransition。在控件加载模板时(即调用OnApplyTemplate()函数时)通常会这样做,因为控件在呈现时通常都不需要做动画。

另外,VisualStateManager.GoToState不会使控件重复进入某个状态,即如果控件已处于PointerOver的VisualState,再次调用VisualStateManager.GoToState(this, PointerOverState, useTransitions)不会触发任何操作,也不会重复触发动画。

6. 结语

除了VisualState.Setters,这篇文章的内容基本和WPF通用。

上次被批评写得太复杂了,这次本来写了很多,为了文章简单易懂删了一半,希望对理解VisualTransition有帮助。

7. 参考

VisualTransition Class (Windows)

VisualTransition Class (Windows.UI.Xaml) - UWP app developer Microsoft Docs

8. 源码

AnimationTest


作者:dino.c 
出处:http://www.cnblogs.com/dino623/ 
说明:欢迎转载并请标明来源和作者。如有错漏请指出,谢谢。