[UWP]瞭解模板化控件(5):VisualState

1. 功能需求

使用TemplatePart實現上篇文章的兩個需求(Header爲空時隱藏HeaderContentPresenter,鼠標沒有放在控件上時HeaderContentPresent半透明),雖然功能已經實現,但這樣實現的話基本上也就別想擴展了。譬如開發者做不到通過繼承或修改ControlTemplate實現如下功能:

  • 半透明時的Opacity不是0.7,而是0.5。

  • 半透明和不透明之前切換時有漸變動畫。

當然也並不是不可以用代碼實現這些需求,只是會複雜很多。大部分的開發者都是對C#熟悉,對XAML陌生,很容易就選擇儘量使用C#實現全部功能,將所有功能集中在同一個地方並用熟悉的語言處理,當然也有這樣做的優點,不過既然在用XAML平臺,就應該儘可能利用XAML平臺UI和代碼分離的優點。

這篇文章用ContentView2示例講解VisualState如何實現上述的需求,ContentView2和上篇文章的ContentView一樣繼承自HeaderedContentControl。

2. VisualState

在實現需求前首先解釋VisualState的概念。

VisualState 指定控件處於特定狀態時的外觀。控件的代碼指定控件處於何種狀態,控件的ControlTemplate中根節點包含VisualStateManager.VisualStateGroups附加屬性,並在其中確定各個VisualState的外觀。

以CheckBox爲例,CheckBox基本上包含Unchecked、Checked、Indeterminate三種狀態,它通過IsChecked的值在這三種狀態中轉換。
38937-20170410215328376-1934315800.png

這三種狀態的外觀如下所示:
38937-20170410214843594-137007037.png

實際上Checkbox的VisualState複雜很多,這裏是簡化的模型。

3. 確定VisualState

要使用VisualState,首先要明確控件中包含哪些VisualState。在ContentView2中有兩組VisualState:

  • CommonStates: 默認是“Normal”,當鼠標進入控件時是“PointerOver”。

  • HeaderStates: 默認是“NoHeader”,當Header屬性的值不爲空時是“HasHeader”。

其中“CommonStates”、“HeaderStates”稱爲VisualStateGroup,“Normal”、“PointerOver”等稱爲VisualState。在同一個VisualStateGroup中的VisualState是互斥的,控件始終只能處於每組狀態中的一種。例如,控件只能處於NoHeader狀態,或者HasHeader狀態。

模板化控件可以使用TemplateVisualStateAttribute協定聲明它的VisualState,用於通知控件的使用者有這些VisualState可用。TemplateVisualStateAttribute是可選的,而且就算控件聲明瞭這些VisualState,ControlTemplate也可以不包含它們中的任何一個,並且不會引發異常。

ContentView2的TemplateVisualStateAttribute如下:

[TemplateVisualState(Name = NormalState, GroupName = CommonStates)]
[TemplateVisualState(Name = PointerOverState,GroupName =CommonStates)]
[TemplateVisualState(Name = NoHeaderState, GroupName = HeaderStates)]
[TemplateVisualState(Name = HasHeaderState, GroupName = HeaderStates)]
public class ContentView2 : HeaderedContentControl
{
    public const string CommonStates = "CommonStates";
    public const string NormalState = "Normal";
    public const string PointerOverState = "PointerOver";

    public const string HeaderStates = "HeaderStates";
    public const string NoHeaderState = "NoHeader";
    public const string HasHeaderState = "HasHeader";


}

4. VisualStateManager

VisualStateManager用於管理VisualState並操作它們之間的轉換。

public ContentView2()
{
    this.DefaultStyleKey = typeof(ContentView2);
}

private bool _isPointerEntered;

protected override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    UpdateVisualState(false);
}

protected override void OnPointerEntered(PointerRoutedEventArgs e)
{
    base.OnPointerEntered(e);
    _isPointerEntered = true;
    UpdateVisualState();
}

protected override void OnPointerExited(PointerRoutedEventArgs e)
{
    base.OnPointerExited(e);
    _isPointerEntered = false;
    UpdateVisualState();
}

protected override void OnHeaderChanged(object oldValue, object newValue)
{
    base.OnHeaderChanged(oldValue, newValue);
    UpdateVisualState();
}

internal virtual void UpdateVisualState(bool useTransitions = true)
{
    if (_isPointerEntered)
        VisualStateManager.GoToState(this, PointerOverState, useTransitions);
    else
        VisualStateManager.GoToState(this, NormalState, useTransitions);

    if (Header == null)
        VisualStateManager.GoToState(this, NoHeaderState, useTransitions);
    else
        VisualStateManager.GoToState(this, HasHeaderState, useTransitions);
}

ContentView2的其它代碼如上所示,在OnApplyTemplate、OnHeaderChanged及鼠標進入離開時使用VisualStateManager.GoToState(Control control, string stateName,bool useTransitions)更新VisualState。useTransitions這個參數指示是否使用 VisualTransition 進行狀態過渡,簡單來說即是VisualState之間切換時用不用VisualTransition裏面定義的動畫。

注意OnApplyTemplate中的這句代碼:UpdateVisualState(false)。控件在加載ControlTemplate時就需要確定它的狀態,一般這時候都不會使用過渡動畫。

VisualStateManager.GoToState不會使控件重複進入某個狀態,譬如如果控件已處於PointerOverState,再次調用VisualStateManager.GoToState(this, PointerOverState, useTransitions)不會觸發任何操作,也不會打斷正在執行的過渡動畫或重複觸發動畫。

到這裏爲止ContentView2.cs的工作已經完成,接下來就是XAML的責任了。

5. 使用Blend編輯ControlTemplate

使用Blend編輯ContentView2的空白ControlTemplate時,由於已經聲明瞭TemplateVisualStateAttribute,可以看到在“狀態”窗口已經默認就有定義好的狀態。

38937-20170312181707686-619538095.png

編輯後結果如下:

<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="CommonStates">
        <VisualStateGroup.Transitions>
            <VisualTransition GeneratedDuration="0:0:0.5">
                <VisualTransition.GeneratedEasingFunction>
                    <CubicEase EasingMode="EaseInOut" />
                </VisualTransition.GeneratedEasingFunction>
            </VisualTransition>
        </VisualStateGroup.Transitions>
        <VisualState x:Name="Normal">
            <VisualState.Setters>
                <Setter Target="HeaderContentPresenter.(UIElement.Opacity)"
                        Value="0.5" />
            </VisualState.Setters>
        </VisualState>
        <VisualState x:Name="PointerOver">
            <VisualState.Setters>
                <Setter Target="HeaderContentPresenter.(UIElement.Opacity)"
                        Value="1" />
            </VisualState.Setters>
        </VisualState>
    </VisualStateGroup>
    <VisualStateGroup x:Name="HeaderStates">
        <VisualState x:Name="NoHeader">
            <VisualState.Setters>
                <Setter Target="HeaderContentPresenter.(UIElement.Visibility)"
                        Value="Collapsed" />
            </VisualState.Setters>
        </VisualState>
        <VisualState x:Name="HasHeader" />
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

38937-20170313203936666-23131997.png

從XAML中可以看出VisualState子節點的Setter是關鍵所在,如PointerOver的VisualState通過Setter將HeaderContentPresenter的Opacity更改爲1,滿足了“當鼠標移動到控件控件上時,設置Header的Opacity=1”這個需求。

另外,VisualStateGroup.Transitions 節點定義了CommonStates在各個狀態之間切換時的過渡動畫。VisualStateManager.GoToState(this, PointerOverState, useTransitions) 中的參數useTransitions即是控制是否使用過渡動畫。示例中使用的過渡動畫爲CubicEase,過渡時間爲0.5秒。

需要注意的是不同VisualStateGroup之間儘量不要對同一個UI元素的同一個屬性進行操作,否則會引起衝突。

這個主題不會詳細講解使用Blend修改VisualState,因爲那會佔用很多篇幅。幸好Blend在這方面做得很容易上手,而且多年來基本操作都沒有變過,可以在網上找到很多這方面的文章。

6. 結論

很多時候VisualState方式並不會比TemplatePart方式少寫代碼,譬如ContentView2的代碼量就基本和ContentView一致,而XAML行數還更多。但VisualState的實現方式更靈活,更加符合UI與代碼分離原則及開放封閉原則。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章