[UWP]理解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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM