Xamarin.Forms 可视状态管理器

使用可视状态管理器根据代码中设置的视觉状态对 XAML 元素进行更改。

可视状态管理器 (VSM) 提供了一种结构化方式,用于从代码对用户界面进行视觉更改。 在大多数情况下,应用程序的用户界面在 XAML 中定义,此 XAML 包含描述可视状态管理器如何影响用户界面视觉对象的标记。

VSM 引入了视觉状态的概念。 Xamarin.Forms 视图(如 Button)可以具有多种不同的视觉外观,具体取决于其基础状态,可以是禁用、按下,或具有输入焦点。 这些是按钮的状态。

视觉状态在“视觉状态组”中收集。 视觉状态组中的所有视觉状态都是互斥的。 视觉状态和视觉状态组均由简单的文本字符串标识。

Xamarin.Forms 可视状态管理器使用以下视觉状态定义一个名为“CommonStates”的视觉状态组:

  • "Normal"
  • "Disabled"
  • "Focused"
  • "Selected"

派生自 VisualElement 的所有类都支持此视觉状态组,它是 ViewPage 的基类。

还可以自行定义视觉状态组和视觉状态,本文将对此进行演示。

注意

熟悉触发器的 Xamarin.Forms 开发人员知道触发器还可以根据视图的属性或事件触发的更改对用户界面中的视觉对象进行更改。 但是,使用触发器处理这些更改的各种组合可能会变得相当混乱。 从历史上看,基于 Windows XAML 的环境中引入了可视状态管理器,以缓解视觉状态组合造成的混乱。 使用 VSM 时,视觉状态组中的视觉状态始终互斥。 在任何时候,每个组中只有一个状态是当前状态。

常见状态

可视状态管理器允许在 XAML 文件中包括标记,当视图正常、禁用或有输入焦点时,标记可改变视图的可视化外观。 这些称为常见状态

例如,假设页面上有一个 Entry 视图,而你希望 Entry 的视觉外观按以下方式更改:

  • 禁用 Entry 时,Entry 背景应为粉红色。
  • 正常情况下,Entry 的背景应为绿黄色。
  • Entry 具有输入焦点时,应扩展至正常高度的两倍。

可以将 VSM 标记附加到单个视图,也可以在样式中定义 VSM 标记(如果适用于多个视图)。 接下来的两个部分介绍了这些方法。

视图上的 VSM 标记

若要将 VSM 标记附加到 Entry 视图,请先将 Entry 分隔为开始标记和结束标记:

<Entry FontSize="18">

</Entry>

之所以给它一个明确的字体大小,是因为其中一个状态将使用 FontSize 属性将 Entry 中的文本大小加倍。

接下来,在这些标记之间插入 VisualStateManager.VisualStateGroups 标记:

<Entry FontSize="18">
    <VisualStateManager.VisualStateGroups>

    </VisualStateManager.VisualStateGroups>
</Entry>

VisualStateGroups 是由 VisualStateManager 类定义的附加可绑定属性。 (有关附加的可绑定属性的详细信息,请参阅“附加属性”文章。)这就是 VisualStateGroups 属性附加到 Entry 对象的方式。

VisualStateGroups 属性类型为 VisualStateGroupList,是 VisualStateGroup 对象的集合。 在 VisualStateManager.VisualStateGroups 标记中,为要包括的每个视觉状态组插入一对 VisualStateGroup 标记:

<Entry FontSize="18">
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="CommonStates">

        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
</Entry>

请注意,VisualStateGroup 标记具有指示组名称的 x:Name 特性。 VisualStateGroup 类定义一个可以改用的 Name 属性:

<VisualStateGroup Name="CommonStates">

可以在同一元素中使用 x:NameName,但不能同时使用两者。

VisualStateGroup 类定义了一个名为 States 的属性,该属性是 VisualState 对象的集合。 StatesVisualStateGroups内容属性,因此可以直接在 VisualStateGroup 标记之间包括 VisualState 标记。 (内容属性在“基本 XAML 语法”一文中进行了讨论。)

下一步是为该组中的每个视觉状态添加一对标签。 还可以使用 x:NameName 进行标识:

<Entry FontSize="18">
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="CommonStates">
            <VisualState x:Name="Normal">

            </VisualState>

            <VisualState x:Name="Focused">

            </VisualState>

            <VisualState x:Name="Disabled">

            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
</Entry>

VisualState 会定义一个名为 Setters 的属性,该属性是 Setter 对象的集合。 这些与 Style 对象中使用的 Setter 对象相同。

Setters 不是 VisualState 的内容属性,因此必须包括 Setters 属性的属性元素标记:

<Entry FontSize="18">
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="CommonStates">
            <VisualState x:Name="Normal">
                <VisualState.Setters>

                </VisualState.Setters>
            </VisualState>

            <VisualState x:Name="Focused">
                <VisualState.Setters>

                </VisualState.Setters>
            </VisualState>

            <VisualState x:Name="Disabled">
                <VisualState.Setters>

                </VisualState.Setters>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
</Entry>

现在可以在每个 Setters 标记对之间插入一个或多个 Setter 对象。 这些是定义前面所述的视觉状态的 Setter 对象:

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

每个 Setter 标记都指示该状态为当前状态时某特定属性的值。 Setter 对象引用的任何属性都必须由可绑定属性提供支持。

与此类似的标记是示例程序中“视图上的 VSM”页面的基础。 该页包含三个 Entry 视图,但只有第二个视图附加了 VSM 标记:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:VsmDemos"
             x:Class="VsmDemos.MainPage"
             Title="VSM Demos">

    <StackLayout>
        <StackLayout.Resources>
            <Style TargetType="Entry">
                <Setter Property="Margin" Value="20, 0" />
                <Setter Property="FontSize" Value="18" />
            </Style>

            <Style TargetType="Label">
                <Setter Property="Margin" Value="20, 30, 20, 0" />
                <Setter Property="FontSize" Value="Large" />
            </Style>
        </StackLayout.Resources>

        <Label Text="Normal Entry:" />
        <Entry />
        <Label Text="Entry with VSM: " />
        <Entry>
            <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.Triggers>
                <DataTrigger TargetType="Entry"
                             Binding="{Binding Source={x:Reference entry3},
                                               Path=Text.Length}"
                             Value="0">
                    <Setter Property="IsEnabled" Value="False" />
                </DataTrigger>
            </Entry.Triggers>
        </Entry>
        <Label Text="Entry to enable 2nd Entry:" />
        <Entry x:Name="entry3"
               Text=""
               Placeholder="Type something to enable 2nd Entry" />
    </StackLayout>
</ContentPage>

请注意,第二个 Entry 还具有 DataTrigger 作为其 Trigger 集合的一部分。 这会导致 Entry 被禁用,直到有内容键入到第三个 Entry 中。 下面是在 iOS、Android 和通用 Windows 平台 (UWP) 上运行的启动页面:

视图上的 VSM:已禁用

当前视觉状态为“已禁用”,因此第二个 Entry 的背景在 iOS 和 Android 屏幕上为粉红色。 禁用 Entry 时,Entry 的 UWP 实现不允许设置背景色。

在第三个 Entry 中输入一些文本时,第二个 Entry 将切换到“正常”状态,背景现在为青绿色:

视图上的 VSM:普通

到达第二个 Entry 时,它会获取输入焦点。 它会切换到“Focused”状态,并扩展到原高度的两倍:

视图上的 VSM:已设定焦点

请注意,当 Entry 获取输入焦点时,它不会保留青绿色背景。 当视觉状态管理器在视觉状态之间切换时,上一状态设置的属性为未设置。 请记住,视觉状态是相互排斥的。 “Normal”状态并不仅仅意味着已启用 Entry。 这意味着 Entry 已启用并且不具有输入焦点。

如果希望 Entry 具有“Focused”状态的青绿色背景,请另将 Setter 添加到该视觉状态:

<VisualState x:Name="Focused">
    <VisualState.Setters>
        <Setter Property="FontSize" Value="36" />
        <Setter Property="BackgroundColor" Value="Lime" />
    </VisualState.Setters>
</VisualState>

为了使这些 Setter 对象正常工作,VisualStateGroup 必须包含该组中所有状态的 VisualState 对象。 如果视觉状态没有任何 Setter 对象,请将其作为空标记包含:

<VisualState x:Name="Normal" />

样式中的可视状态管理器标记

通常需要在两个或多个视图之间共享相同的可视状态管理器标记。 在这种情况下,需要将标记放在 Style 定义中。

下面是视图上的 VSM 页中 Entry 元素的现有隐式 Style

<Style TargetType="Entry">
    <Setter Property="Margin" Value="20, 0" />
    <Setter Property="FontSize" Value="18" />
</Style>

VisualStateManager.VisualStateGroups 附加的可绑定属性添加 Setter 标记:

<Style TargetType="Entry">
    <Setter Property="Margin" Value="20, 0" />
    <Setter Property="FontSize" Value="18" />
    <Setter Property="VisualStateManager.VisualStateGroups">

    </Setter>
</Style>

Setter 的内容属性是 Value,因此可以直接在这些标记中指定 Value 属性的值。 该属性的类型为 VisualStateGroupList

<Style TargetType="Entry">
    <Setter Property="Margin" Value="20, 0" />
    <Setter Property="FontSize" Value="18" />
    <Setter Property="VisualStateManager.VisualStateGroups">
        <VisualStateGroupList>

        </VisualStateGroupList>
    </Setter>
</Style>

在这些标记中,可以包括一个或多个 VisualStateGroup 对象:

<Style TargetType="Entry">
    <Setter Property="Margin" Value="20, 0" />
    <Setter Property="FontSize" Value="18" />
    <Setter Property="VisualStateManager.VisualStateGroups">
        <VisualStateGroupList>
            <VisualStateGroup x:Name="CommonStates">

            </VisualStateGroup>
        </VisualStateGroupList>
    </Setter>
</Style>

VSM 标记的其余部分与之前相同。

下面是 显示完整 VSM 标记的“样式中的 VSM”页面:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="VsmDemos.VsmInStylePage"
             Title="VSM in Style">
    <StackLayout>
        <StackLayout.Resources>
            <Style TargetType="Entry">
                <Setter Property="Margin" Value="20, 0" />
                <Setter Property="FontSize" Value="18" />
                <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" />
                                    <Setter Property="BackgroundColor" Value="Lime" />
                                </VisualState.Setters>
                            </VisualState>
                            <VisualState x:Name="Disabled">
                                <VisualState.Setters>
                                    <Setter Property="BackgroundColor" Value="Pink" />
                                </VisualState.Setters>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateGroupList>
                </Setter>
            </Style>

            <Style TargetType="Label">
                <Setter Property="Margin" Value="20, 30, 20, 0" />
                <Setter Property="FontSize" Value="Large" />
            </Style>
        </StackLayout.Resources>

        <Label Text="Normal Entry:" />
        <Entry />
        <Label Text="Entry with VSM: " />
        <Entry>
            <Entry.Triggers>
                <DataTrigger TargetType="Entry"
                             Binding="{Binding Source={x:Reference entry3},
                                               Path=Text.Length}"
                             Value="0">
                    <Setter Property="IsEnabled" Value="False" />
                </DataTrigger>
            </Entry.Triggers>
        </Entry>
        <Label Text="Entry to enable 2nd Entry:" />
        <Entry x:Name="entry3"
               Text=""
               Placeholder="Type something to enable 2nd Entry" />
    </StackLayout>
</ContentPage>

现在,此页上的所有 Entry 视图都以相同的方式响应其视觉状态。 另请注意,“Focused”状态现在包括第二个 Setter,当具有输入焦点时,每个 Entry 都会有青绿色背景:

样式中的 VSM

Xamarin.Forms 中的视觉状态

下表列出了在 Xamarin.Forms 中定义的视觉状态:

状态 更多信息
Button Pressed 按钮视觉状态
CheckBox IsChecked CheckBox 视觉状态
CarouselView CarouselView 视觉状态
ImageButton Pressed ImageButton 视觉状态
RadioButton %> RadioButton 视觉状态
Switch %> Switch 视觉状态
VisualElement 常见状态

可以通过名为 CommonStates 的视觉状态组访问每个状态。

此外,CollectionView 会实现 Selected 状态。 有关详细信息,请参阅“更改所选项颜色”。

在多个元素上设置状态

在前面的示例中,视觉状态附加到单个元素并针对单个元素进行操作。 但是,也可以创建附加到单个元素的视觉状态,但对同一范围内的其他元素设置属性。 这避免了在运行状态的每个元素上重复视觉状态。

Setter 类型具有 string 类型的 TargetName 属性,该属性表示视觉状态的 Setter 将操作的目标元素。 定义 TargetName 属性时,Setter 会将 TargetName 中定义的元素的 Property 设置为 Value

<Setter TargetName="label"
        Property="Label.TextColor"
        Value="Red" />

在此示例中,名为 labelLabel 将其 TextColor 属性设置为 Red。 设置 TargetName 属性时,必须在 Property 中指定属性的完整路径。 因此,若要在 Label 上设置 TextColor 属性,请将 Property 指定为 Label.TextColor

注意

Setter 对象引用的任何属性都必须由可绑定属性提供支持。

示例中的“具有资源库 TargetName 的 VSM”页面演示如何从单个视觉状态组在多个元素上设置状态。 XAML 文件由包含 Label 元素、EntryButtonStackLayout 组成:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="VsmDemos.VsmSetterTargetNamePage"
             Title="VSM with Setter TargetName">
    <StackLayout Margin="10">
        <Label Text="What is the capital of France?" />
        <Entry x:Name="entry"
               Placeholder="Enter answer" />
        <Button Text="Reveal answer">
            <VisualStateManager.VisualStateGroups>
                <VisualStateGroup x:Name="CommonStates">
                    <VisualState x:Name="Normal" />
                    <VisualState x:Name="Pressed">
                        <VisualState.Setters>
                            <Setter Property="Scale"
                                    Value="0.8" />
                            <Setter TargetName="entry"
                                    Property="Entry.Text"
                                    Value="Paris" />
                        </VisualState.Setters>
                    </VisualState>
                </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>
        </Button>
    </StackLayout>
</ContentPage>

VSM 标记是附加到 StackLayout 的。 有两个互斥状态,名为“Normal”和“Pressed”,每个状态都包含 VisualState 标记。

未按下 Button 时,“Normal”状态处于活动状态,并且可以输入对问题的响应:

具有资源库 TargetName 的 VSM:普通状态

按下 Button 时,“Pressed”状态会变为活动状态:

具有资源库 TargetName 的 VSM:已按下状态

“Pressed”VisualState 会指定当按下 Button 时,其 Scale 属性将从默认值 1 更改为 0.8。 此外,已命名为 entryEntry 会将其 Text 属性设置为 Paris。 因此,结果是,当按下 Button 时,将将其重新缩放为略小,而 Entry 会显示巴黎。 然后,当松开 Button 时,会重新缩放为默认值 1,且 Entry 会显示以前输入的任何文本。

重要

在指定 TargetName 属性的 Setter 元素中,当前不支持属性路径。

定义自己的视觉状态

派生自 VisualElement 的每个类都支持通用状态“Normal”、“Focused”和“Disabled”。 此外,该 CollectionView 类还支持“Selected”状态。 在内部,VisualElement 类将检测它何时处于启用或禁用状态,或者处于聚焦或未聚焦状态,并调用静态 VisualStateManager.GoToState 方法:

VisualStateManager.GoToState(this, "Focused");

这是唯一可在 VisualElement 类中找到的可视状态管理器代码。 由于会根据派生自 VisualElement 的每个类为每个对象调用 GoToState,因此可以将可视状态管理器与任何 VisualElement 对象一起使用来响应这些更改。

有趣的是,视觉状态组“CommonStates”的名称未在 VisualElement 中显式引用。 组名称不是可视状态管理器的 API 的一部分。 在到目前为止显示的两个示例程序中,可以将组的名称从“CommonStates”更改为任何其他程序,程序仍将有效。 组名称只是该组中状态的一般说明。 这是一个隐含的理解,即在任何组中,视觉状态是互斥的:在任何时候当前都有且仅会有一种状态。

如果要实现自己的视觉状态,则需要从代码调用 VisualStateManager.GoToState。 通常,你将从页面类的代码隐藏文件进行此调用。

示例中的“VSM 验证”页面演示如何将可视状态管理器与输入验证结合使用。 XAML 文件包含一个含有两个 Label 元素、一个 Entry 和一个 ButtonStackLayout

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="VsmDemos.VsmValidationPage"
             Title="VSM Validation">
    <StackLayout x:Name="stackLayout"
                 Padding="10, 10">
            <VisualStateManager.VisualStateGroups>
                <VisualStateGroup Name="ValidityStates">
                    <VisualState Name="Valid">
                        <VisualState.Setters>
                            <Setter TargetName="helpLabel"
                                    Property="Label.TextColor"
                                    Value="Transparent" />
                            <Setter TargetName="entry"
                                    Property="Entry.BackgroundColor"
                                    Value="Lime" />
                        </VisualState.Setters>
                    </VisualState>
                    <VisualState Name="Invalid">
                        <VisualState.Setters>
                            <Setter TargetName="entry"
                                    Property="Entry.BackgroundColor"
                                    Value="Pink" />
                            <Setter TargetName="submitButton"
                                    Property="Button.IsEnabled"
                                    Value="False" />
                        </VisualState.Setters>
                    </VisualState>
                </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>
        <Label Text="Enter a U.S. phone number:"
               FontSize="Large" />
        <Entry x:Name="entry"
               Placeholder="555-555-5555"
               FontSize="Large"
               Margin="30, 0, 0, 0"
               TextChanged="OnTextChanged" />
        <Label x:Name="helpLabel"
               Text="Phone number must be of the form 555-555-5555, and not begin with a 0 or 1" />
        <Button x:Name="submitButton"
                Text="Submit"
                FontSize="Large"
                Margin="0, 20"
                VerticalOptions="Center"
                HorizontalOptions="Center" />
    </StackLayout>
</ContentPage>

VSM 标记是附加到 StackLayout 的(命为 stackLayout)。 有两个互斥状态,名为“Valid”和“Invalid”,每个状态都包含 VisualState 标记。

如果 Entry 不包含有效的电话号码,则当前状态为“Invalid”,因此 Entry 具有粉红色背景,第二个 Label 是可见的,并且禁用了 Button

VSM 验证:状态无效

输入有效的电话号码时,当前状态将变为“有效”。 Entry 获得绿黄色背景,第二个 Label 消失,Button 现在已启用:

VSM 验证:有效状态

代码隐藏文件负责处理来自 Entry 的 TextChanged 事件。 处理程序使用正则表达式来确定输入字符串是否有效。 名为 GoToState 的代码隐藏文件中的方法会调用 stackLayout 的静态 VisualStateManager.GoToState 方法:

public partial class VsmValidationPage : ContentPage
{
    public VsmValidationPage()
    {
        InitializeComponent();

        GoToState(false);
    }

    void OnTextChanged(object sender, TextChangedEventArgs args)
    {
        bool isValid = Regex.IsMatch(args.NewTextValue, @"^[2-9]\d{2}-\d{3}-\d{4}$");
        GoToState(isValid);
    }

    void GoToState(bool isValid)
    {
        string visualState = isValid ? "Valid" : "Invalid";
        VisualStateManager.GoToState(stackLayout, visualState);
    }
}

另请注意,会从构造函数调用 GoToState 方法以初始化状态。 应始终存在当前状态。 但是,代码中没有任何对视觉状态组名称的引用,尽管为了清晰起见,XAML 中将其称为 "ValidationStates"。

请注意,代码隐藏文件只需要考虑定义视觉状态的页面上的对象,并为此对象调用 VisualStateManager.GoToState。 这是因为这两种视觉状态都针对页面上的多个对象。

你可能想知道:如果代码隐藏文件必须引用定义视觉状态的页面上的对象,为什么代码隐藏文件不能直接访问此对象和其他对象? 它当然可以。 但是,使用 VSM 的优点是,你可以控制视觉元素在 XAML 中对不同状态的反应方式,从而将所有 UI 设计保留在一个位置。 这可以避免通过直接从代码隐藏访问视觉元素来设置视觉外观。

视觉状态触发器

视觉状态支持状态触发器,状态触发器是一组专用的触发器,用于定义应用 VisualState 的条件。

状态触发器添加到 VisualStateStateTriggers 集合。 此集合可以包含一个或多个状态触发器。 当此集合中的任何状态触发器处于活动状态时,便会应用 VisualState

使用状态触发器来控制视觉对象状态时,Xamarin.Forms 使用以下优先规则来确定哪个触发器(以及相应的 VisualState)处于活动状态:

  1. 任何派生自 StateTriggerBase 的触发器。
  2. 因满足 MinWindowWidth 条件而激活的 AdaptiveTrigger
  3. 因满足 MinWindowHeight 条件而激活的 AdaptiveTrigger

如果多个触发器同时处于活动状态(例如,两个自定义触发器),则标记中声明的第一个触发器优先。

有关状态触发器的详细信息,请参阅状态触发器

使用可视状态管理器进行自适应布局

在手机上运行的 Xamarin.Forms 应用程序通常可以在纵向或横向纵横比中查看,并且假定许多不同的大小和纵横比,则可以调整桌面上运行的 Xamarin.Forms 程序的大小。 设计良好的应用程序可能会针对这些不同的页面或窗口外形规格以不同的方式显示其内容。

此方法有时称为自适应布局。 由于自适应布局只涉及程序的视觉对象,因此它是可视状态管理器的理想应用。

一个简单的示例是一个应用程序,它显示影响应用程序内容的按钮的小型集合。 在纵向模式下,这些按钮可能显示在页面顶部的水平行中:

VSM 自适应布局:竖屏

在横向模式下,按钮数组可能会移到一侧,并显示在列中:

VSM 自适应布局:横屏

从上到下,程序在通用 Windows 平台、Android 和 iOS 上运行。

示例中的“VSM 自适应布局”页面定义了一个名为“OrientationStates”的组,其中包含两个名为“竖屏”和“横屏”的可视状态。 (更复杂的方法可能是基于多个不同的页面或窗口宽度进行的。)

VSM 标记会出现在 XAML 文件中的四个位置。 名为 mainStackStackLayout 包含菜单和内容,这是一个 Image 元素。 此 StackLayout 在纵向模式下应具有垂直方向,在横向模式下应具有水平方向:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="VsmDemos.VsmAdaptiveLayoutPage"
             Title="VSM Adaptive Layout">

    <StackLayout x:Name="mainStack">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup Name="OrientationStates">
                <VisualState Name="Portrait">
                    <VisualState.Setters>
                        <Setter Property="Orientation" Value="Vertical" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState Name="Landscape">
                    <VisualState.Setters>
                        <Setter Property="Orientation" Value="Horizontal" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>

        <ScrollView x:Name="menuScroll">
            <VisualStateManager.VisualStateGroups>
                <VisualStateGroup Name="OrientationStates">
                    <VisualState Name="Portrait">
                        <VisualState.Setters>
                            <Setter Property="Orientation" Value="Horizontal" />
                        </VisualState.Setters>
                    </VisualState>
                    <VisualState Name="Landscape">
                        <VisualState.Setters>
                            <Setter Property="Orientation" Value="Vertical" />
                        </VisualState.Setters>
                    </VisualState>
                </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>

            <StackLayout x:Name="menuStack">
                <VisualStateManager.VisualStateGroups>
                    <VisualStateGroup Name="OrientationStates">
                        <VisualState Name="Portrait">
                            <VisualState.Setters>
                                <Setter Property="Orientation" Value="Horizontal" />
                            </VisualState.Setters>
                        </VisualState>
                        <VisualState Name="Landscape">
                            <VisualState.Setters>
                                <Setter Property="Orientation" Value="Vertical" />
                            </VisualState.Setters>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateManager.VisualStateGroups>

                <StackLayout.Resources>
                    <Style TargetType="Button">
                        <Setter Property="VisualStateManager.VisualStateGroups">
                            <VisualStateGroupList>
                                <VisualStateGroup Name="OrientationStates">
                                    <VisualState Name="Portrait">
                                        <VisualState.Setters>
                                            <Setter Property="HorizontalOptions" Value="CenterAndExpand" />
                                            <Setter Property="Margin" Value="10, 5" />
                                        </VisualState.Setters>
                                    </VisualState>
                                    <VisualState Name="Landscape">
                                        <VisualState.Setters>
                                            <Setter Property="VerticalOptions" Value="CenterAndExpand" />
                                            <Setter Property="HorizontalOptions" Value="Center" />
                                            <Setter Property="Margin" Value="10" />
                                        </VisualState.Setters>
                                    </VisualState>
                                </VisualStateGroup>
                            </VisualStateGroupList>
                        </Setter>
                    </Style>
                </StackLayout.Resources>

                <Button Text="Banana"
                        Command="{Binding SelectedCommand}"
                        CommandParameter="Banana.jpg" />
                <Button Text="Face Palm"
                        Command="{Binding SelectedCommand}"
                        CommandParameter="FacePalm.jpg" />
                <Button Text="Monkey"
                        Command="{Binding SelectedCommand}"
                        CommandParameter="monkey.png" />
                <Button Text="Seated Monkey"
                        Command="{Binding SelectedCommand}"
                        CommandParameter="SeatedMonkey.jpg" />
            </StackLayout>
        </ScrollView>

        <Image x:Name="image"
               VerticalOptions="FillAndExpand"
               HorizontalOptions="FillAndExpand" />
    </StackLayout>
</ContentPage>

名为 menuScroll 的内部 ScrollView 和名为 menuStackStackLayout 会实现按钮菜单。 这些布局的方向与 mainStack 相反。 菜单应在纵向模式下为水平,在横向模式下为垂直。

VSM 标记的第四部分是按钮本身的隐式样式。 此标记设置特定于纵向和横向的 VerticalOptionsHorizontalOptionsMargin 属性。

代码隐藏文件将 menuStackBindingContext 属性设置为实现 Button 命令,并将处理程序附加到页面的 SizeChanged 事件:

public partial class VsmAdaptiveLayoutPage : ContentPage
{
    public VsmAdaptiveLayoutPage ()
    {
        InitializeComponent ();

        SizeChanged += (sender, args) =>
        {
            string visualState = Width > Height ? "Landscape" : "Portrait";
            VisualStateManager.GoToState(mainStack, visualState);
            VisualStateManager.GoToState(menuScroll, visualState);
            VisualStateManager.GoToState(menuStack, visualState);

            foreach (View child in menuStack.Children)
            {
                VisualStateManager.GoToState(child, visualState);
            }
        };

        SelectedCommand = new Command<string>((filename) =>
        {
            image.Source = ImageSource.FromResource("VsmDemos.Images." + filename);
        });

        menuStack.BindingContext = this;
    }

    public ICommand SelectedCommand { private set; get; }
}

SizeChanged 处理程序会调用 VisualStateManager.GoToState 获得两个 StackLayoutScrollView 元素,然后循环访问 menuStack 的子级以调用 VisualStateManager.GoToState 获得 Button 元素。

代码隐藏文件似乎可以通过在 XAML 文件中设置元素的属性来更直接地处理方向更改,但可视状态管理器绝对是一种更具结构化的方法。 所有视觉对象都保存在 XAML 文件中,它们更易于检查、维护和修改。

具有 Xamarin.University 的可视状态管理器

Xamarin.Forms 3.0 可视状态管理器视频