shuhelohelo’s blog

Xamarin.Forms多めです.

Xamarin.FormsのVisualStateManagerについてのメモ

VisualStateManager

docs.microsoft.com

Visual State Manager(VSM)はコントロールの見た目を予め数種類定義しておき,それを切り替えることができるようにする機能です.

VSMにはデフォルトの定義があり,それはCommonStatesというグループ名で定義されている.

このCommonStatesグループには以下の4種類の見た目の定義が含まれる.これはVisualElementクラスを継承しているクラスであれば有効です.

  • Normal
  • Disabled
  • Focused
  • Selected

これらはそれぞれ,通常の見た目,利用不可のときの見た目,フォーカスがあたっているときの見た目,選択されているときの見た目,についての定義です.

何もしなければデフォルトの見た目が使用されますが,VSMを通じてこれらの定義を上書きすることで,求める見た目にすることができます.

例えば以下の例では,Entryに対してNormal,Forcus,Disableの3つの状態に対して見た目を変更しています.

        <Entry FontSize="18">
            <VisualStateManager.VisualStateGroups>
                <VisualStateGroup x:Name="CommonStates">
                    <VisualState x:Name="Normal">
                        <VisualState.Setters>
                            <Setter Property="BackgroundColor" Value="Lime"/>
                        </VisualState.Setters>
                    </VisualState>
                    <VisualState x:Name="Focused">
                        <VisualState.Setters>
                            <Setter Property="FontSize" Value="36"/>
                        </VisualState.Setters>
                    </VisualState>
                    <VisualState x:Name="Disabled">
                        <VisualState.Setters>
                            <Setter Property="BackgroundColor" Value="Pink"/>
                        </VisualState.Setters>
                    </VisualState>
                </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>
        </Entry>

これら基本的なVisualStateに加えてコントローラごとに固有のStateを持っていることもある.

Class States More Information
Button Pressed Button visual states
CarouselView DefaultItem, CurrentItem, PreviousItem, NextItem CarouselView visual states
ImageButton Pressed ImageButton visual states
VisualElement Normal, Disabled, Focused, Selected Common states

例えばButtonコントロールPressedというStateを追加で持っています.

同じ種類のコントロールに一律にVSMを定義したい場合

これはResourceDictionaryを使います.

例えば,ContentPage.Resources内に以下のようにVSMを定義します.この時,StyleのターゲットをEntryにしています.

    <ContentPage.Resources>
        <ResourceDictionary>
            <Style TargetType="Entry">
                <Setter Property="VisualStateManager.VisualStateGroups">
                    <VisualStateGroupList>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal">
                                <VisualState.Setters>
                                    <Setter Property="BackgroundColor" Value="Lime" />
                                </VisualState.Setters>
                            </VisualState>
                            <VisualState x:Name="Focused">
                                <VisualState.Setters>
                                    <Setter Property="FontSize" Value="36" />
                                </VisualState.Setters>
                            </VisualState>
                            <VisualState x:Name="Disabled">
                                <VisualState.Setters>
                                    <Setter Property="BackgroundColor" Value="Pink" />
                                </VisualState.Setters>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateGroupList>
                </Setter>
            </Style>
        </ResourceDictionary>
    </ContentPage.Resources>

これで,このページ内の全てのEntryにこの設定が適用されます.

以下の例には2つのEntryがありますが,それぞれDisableとNormalの定義が適用されます.

    <StackLayout>
        <!--  Place new controls here  -->
        <Label
            HorizontalOptions="Center"
            Text="Welcome to Xamarin.Forms!"
            VerticalOptions="CenterAndExpand" />
        <Entry
            FontSize="18"
            IsEnabled="False"
            Placeholder="Entry1" />
        <Entry
            FontSize="18"
            IsEnabled="True"
            Placeholder="Entry2" />
    </StackLayout>

f:id:shuhelohelo:20200302125108p:plain

Styleに名前をつけてコントロール側で指定する

ターゲットのコントロールの外側でStyleを名前付きで定義し,それをターゲットのコントロール側で名前で指定します.

    <ContentPage.Resources>
        <ResourceDictionary>
            <Style TargetType="Entry" x:Key="EntryStyle">
                <Setter Property="VisualStateManager.VisualStateGroups">
                    <VisualStateGroupList>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal">
                                <VisualState.Setters>
                                    <Setter Property="BackgroundColor" Value="Lime" />
                                </VisualState.Setters>
                            </VisualState>
                            <VisualState x:Name="Focused">
                                <VisualState.Setters>
                                    <Setter Property="FontSize" Value="36" />
                                </VisualState.Setters>
                            </VisualState>
                            <VisualState x:Name="Disabled">
                                <VisualState.Setters>
                                    <Setter Property="BackgroundColor" Value="Pink" />
                                </VisualState.Setters>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateGroupList>
                </Setter>
            </Style>
        </ResourceDictionary>
    </ContentPage.Resources>

コントロール側ではこんな感じです.

        <Entry Style="{StaticResource EntryStyle}"
            FontSize="18"
            IsEnabled="False"
            Placeholder="Entry1" />

コードビハインドから切り替える

さて,上記のCommonStatesに関しては規定の4つの状態に対して再定義をして見た目を変更しましたが,それ以外の何か任意のトリガー(何かしらのイベント)によって見た目を切り替えたい場合はどうしたら良いでしょうか.

これはC#コード側の出番です.

XAMLでVisualStateを定義しておいて,

        <StackLayout HorizontalOptions="Center" Orientation="Horizontal">
            <StackLayout.Resources>
                <ResourceDictionary>
                    <Style TargetType="Button">
                        <Setter Property="VisualStateManager.VisualStateGroups">
                            <VisualStateGroupList>
                                <VisualStateGroup x:Name="buttonState">
                                    <VisualState x:Name="SelectedState">
                                        <VisualState.Setters>
                                            <Setter Property="BackgroundColor" Value="Purple" />
                                            <Setter Property="TextColor" Value="White" />
                                        </VisualState.Setters>
                                    </VisualState>
                                    <VisualState x:Name="UnSelectedState">
                                        <VisualState.Setters>
                                            <Setter Property="BackgroundColor" Value="Transparent" />
                                            <Setter Property="TextColor" Value="Black" />
                                        </VisualState.Setters>
                                    </VisualState>
                                </VisualStateGroup>
                            </VisualStateGroupList>
                        </Setter>
                    </Style>
                </ResourceDictionary>
            </StackLayout.Resources>
            
            <Button
                BackgroundColor="Transparent"
                Clicked="Button_Clicked"
                Text="Button1" />
            <Button
                BackgroundColor="Transparent"
                Clicked="Button_Clicked"
                Text="Button2" />
            <Button
                BackgroundColor="Transparent"
                Clicked="Button_Clicked"
                Text="Button3" />

        </StackLayout>

それをC#コード側で以下のように適用します.

        private void Button_Clicked(object sender, EventArgs e)
        {
            //最後に選択したボタンがNullじゃない
            if (_lastbutton != null)
            {
                VisualStateManager.GoToState(_lastbutton, "UnSelectedState");
            }
            _lastbutton = (Button)sender;
            VisualStateManager.GoToState(_lastbutton, "SelectedState");
        }

このようにGoToStateメソッドを使って,対象のコントロールとそれに適用したいVisualStateの名前を指定します.

f:id:shuhelohelo:20200303004619p:plain

クリックしたボタンが紫色になります.ラジオボタンのような動作ですが,このようにスタイルを切り替えることができます.

CommonStatesのStateを再定義するとき,空でもいい

以下のようにNormalStateの中身は空だが問題ない.

これはNormalが適用される状態になったときに,デフォルトの値が適用される.

じゃあ,いらないじゃない?というと,そうではない.

この例は,Buttonが押されたときにボタンが赤くなるが,もしNormalの指定がなければ赤くなったまま元の色には戻らない.

明示的に空のNormalを定義することで,押されている間だけ赤くなり,離されたらもとの色に戻る動作となる.

            <Style TargetType="Button">
                <Setter Property="VisualStateManager.VisualStateGroups">
                    <VisualStateGroupList>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Pressed">
                                <VisualState.Setters>
                                    <Setter Property="BackgroundColor" Value="Red" />
                                </VisualState.Setters>
                            </VisualState>
                            <VisualState x:Name="Normal" />
                        </VisualStateGroup>
                    </VisualStateGroupList>
                </Setter>
            </Style>

Triggerとの比較

状態に応じてプロパティの値を変更する

Xamarin.Forms developers familiar with triggers are aware that triggers can also make changes to visuals in the user interface based on changes in a view's properties or the firing of events. However, using triggers to deal with various combinations of these changes can become quite confusing. Historically, the Visual State Manager was introduced in Windows XAML-based environments to alleviate the confusion resulting from combinations of visual states. With the VSM, the visual states within a visual state group are always mutually exclusive. At any time, only one state in each group is the current state.

ソースコード

github.com