[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的值在這三種狀態中轉換。

這三種狀態的外觀如下所示:

實際上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,可以看到在“狀態”窗口已經默認就有定義好的狀態。

編輯后結果如下:

<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>


從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與代碼分離原則及開放封閉原則。


免責聲明!

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



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