可视状态

.NET Multi-platform App UI (.NET MAUI) 视觉状态管理器提供了一种结构化的方法,用于通过代码对用户界面进行视觉更改。 在大多数情况下,应用的用户界面在 XAML 中定义,而 XAML 可以包含描述视觉状态管理器如何影响用户界面视觉效果的标记。

视觉状态管理器引入了“视觉状态”的概念。 .NET MAUI 视图(如 Button)可以有多种不同的视觉外观,这取决于它的基本状态——是禁用、按下还是有输入焦点。 这些是按钮的状态。 视觉状态在“视觉状态组”中收集。 视觉状态组中的所有视觉状态都是互斥的。 视觉状态和视觉状态组均由简单的文本字符串标识。

.NET MAUI 视觉状态管理器定义了一个名为 CommonStates 的视觉状态管理组,其中包含下列视觉状态:

  • 正常
  • 已禁用
  • Focused
  • 已选择
  • PointerOver

派生自 VisualElementViewPage 的基类)的所有类都支持 NormalDisabledFocusedPointerOver 视觉状态。 此外,还可以定义自己的视觉状态组和视觉状态。

使用视觉状态管理器定义外观,而不是直接从代码隐藏访问视觉元素,其优势在于完全可以在 XAML 中控制视觉元素对不同状态的反应,从而将所有 UI 设计集中在一个位置。

注意

触发器还可以根据视图属性的更改或触发事件对用户界面中的视觉对象进行更改。 但是,使用触发器处理这些更改的各种组合可能会令人困惑。 使用视觉状态管理器时,视觉状态组中的视觉状态始终互斥。 在任何时候,每个组中只有一个状态是当前状态。

常见视觉状态

视觉状态管理器允许在 XAML 文件中包含标记,当视图正常、禁用、具有输入焦点、处于选中状态或鼠标光标悬停在视图上方但未按下时,可以更改视图的视觉外观。 这些称为常见状态

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

  • 禁用 Entry 时,Entry 背景应为粉红色。
  • 正常情况下,Entry 的背景应为绿黄色。
  • Entry 具有输入焦点时,应扩展至正常高度的两倍。
  • 鼠标光标悬停在 Entry 上但未按下时,其背景应为淡蓝色。

可以将视觉状态管理器标记附加到单个视图,也可以将其定义为样式(如果应用于多个视图)。

定义视图上的视觉状态

VisualStateManager 类定义了一个 VisualStateGroups 附加属性,用于将视觉状态附加到视图。 VisualStateGroups 属性类型为 VisualStateGroupList,是 VisualStateGroup 对象的集合。 因此,VisualStateManager.VisualStateGroups 附加属性的子属性是一个 VisualStateGroup 对象。 该对象定义了一个 x:Name 特性,表示组的名称。 另外,VisualStateGroup 类定义了一个可供你备选使用的 Name 属性。 有关附加属性的详细信息,请参阅附加属性

VisualStateGroup 类定义了一个名为 States 的属性,该属性是 VisualState 对象的集合。 StatesVisualStateGroups 类的内容属性,因此可以将 VisualState 对象列为 VisualStateGroup 的子级。 每个 VisualState 对象应使用 x:NameName 标识。

VisualState 类定义了一个名为 Setters 的属性,该属性是 Setter 对象的集合。 这些与 Style 对象中使用的 Setter 对象相同。 Setters 不是 VisualState 的内容属性,因此必须为 Setters 属性包含属性元素标记。 Setter 对象应作为 Setters 的子级插入。 每个 Setter 对象都指示当前状态下的属性值。 Setter 对象引用的任何属性都必须由可绑定属性提供支持。

重要

为了使视觉状态 Setter 对象正常工作,VisualStateGroup 必须包含 Normal 状态的 VisualState 对象。 如果此视觉状态没有任何 Setter 对象,则应将其作为空视觉状态包含 (<VisualState Name="Normal" />)。

以下示例演示了在 Entry 上定义的视觉状态:

<Entry FontSize="18">
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup Name="CommonStates">
            <VisualState Name="Normal">
                <VisualState.Setters>
                    <Setter Property="BackgroundColor" Value="Lime" />
                </VisualState.Setters>
            </VisualState>

            <VisualState Name="Focused">
                <VisualState.Setters>
                    <Setter Property="FontSize" Value="36" />
                </VisualState.Setters>
            </VisualState>

            <VisualState Name="Disabled">
                <VisualState.Setters>
                    <Setter Property="BackgroundColor" Value="Pink" />
                </VisualState.Setters>
            </VisualState>

            <VisualState Name="PointerOver">
                <VisualState.Setters>
                    <Setter Property="BackgroundColor" Value="LightBlue" />
                </VisualState.Setters>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
</Entry>

以下屏幕截图显示了处于其四种已定义视觉状态的 Entry

条目上三个定义的视觉状态的屏幕截图。

Entry 处于 Normal 状态时,其背景为绿黄色。 当 Entry 获得输入焦点时,其字号将加倍。 当 Entry 被禁用时,其背景变为粉红色。 当 Entry 获得输入焦点时,不会保留其绿黄色背景。 当鼠标指针悬停在 Entry 上但未按下时, Entry 背景变为浅蓝色。 当视觉状态管理器在视觉状态之间切换时,上一状态设置的属性为未设置。 因此,视觉状态是互斥的。

如果希望 EntryFocused 状态中具有绿黄色背景,请将另一个 Setter 添加到该视觉状态:

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

在样式中定义视觉状态

通常需要在两个或多个视图中共享相同的视觉状态。 在此方案中,可以在 Style 中定义视觉状态。 这可以通过为 VisualStateManager.VisualStateGroups 属性添加 Setter 对象来实现。 Setter 对象的内容属性是其 Value 属性,因此可以将其指定为 Setter 对象的子级。 该 VisualStateGroups 属性的类型为 VisualStateGroupList,因此 Setter 对象的子级是 VisualStateGroupList,可向其添加包含 VisualState 对象的 VisualStateGroup

以下示例显示了定义常见视觉状态的 Entry 的隐式样式:

<Style TargetType="Entry">
    <Setter Property="FontSize" Value="18" />
    <Setter Property="VisualStateManager.VisualStateGroups">
        <VisualStateGroupList>
            <VisualStateGroup Name="CommonStates">
                <VisualState Name="Normal">
                    <VisualState.Setters>
                        <Setter Property="BackgroundColor" Value="Lime" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState Name="Focused">
                    <VisualState.Setters>
                        <Setter Property="FontSize" Value="36" />
                        <Setter Property="BackgroundColor" Value="Lime" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState Name="Disabled">
                    <VisualState.Setters>
                        <Setter Property="BackgroundColor" Value="Pink" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState Name="PointerOver">
                    <VisualState.Setters>
                        <Setter Property="BackgroundColor" Value="LightBlue" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateGroupList>
    </Setter>
</Style>

当此样式包含在页面级资源字典中时,Style 对象将应用于页面上的所有 Entry 对象。 因此,页面上的所有 Entry 对象将以相同的方式响应其视觉状态。

.NET MAUI 中的视觉状态

下表列出了 .NET MAUI 中定义的视觉状态:

状态 更多信息
Button Pressed 按钮视觉状态
CarouselView DefaultItemCurrentItemPreviousItemNextItem CarouselView 视觉状态
CheckBox IsChecked CheckBox 视觉状态
CollectionView Selected CollectionView 视觉状态
ImageButton Pressed ImageButton 视觉状态
RadioButton CheckedUnchecked RadioButton 视觉状态
Switch OnOff Switch 视觉状态
VisualElement NormalDisabledFocusedPointerOver 常见状态

在多个元素上设置状态

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

Setter 类型具有一个类型为 stringTargetName 属性,该属性表示视觉状态的 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 对象引用的任何属性都必须由可绑定属性提供支持。

以下示例演示如何从单个视觉状态组设置多个对象的状态:

<StackLayout>
    <Label Text="What is the capital of France?" />
    <Entry x:Name="entry"
           Placeholder="Enter answer" />
    <Button Text="Reveal answer">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup Name="CommonStates">
                <VisualState Name="Normal" />
                <VisualState 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>

在此示例中,当未按下 Button 时,Normal 状态为活动状态,并且可以在 Entry 中输入响应。 按下 ButtonPressed 状态变为活动状态,并指定其 Scale 属性将从默认值 1 更改为 0.8。 此外,已命名为 entryEntry 会将其 Text 属性设置为 Paris。 因此,结果是按下 Button 后,会将其重新缩放为略小尺寸,并且 Entry 将显示 Paris:

按钮的按下状态的屏幕截图。

然后,当松开 Button 时,会重新缩放为默认值 1,且 Entry 会显示以前输入的任何文本。

重要

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

定义自定义视觉状态

可以像定义常见状态的视觉状态那样定义自定义视觉状态,只是要使用你选择的名称,然后调用 VisualStateManager.GoToState 方法来激活状态。

以下示例演示了如何使用视觉状态管理器进行输入验证:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             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="18" />
        <Entry x:Name="entry"
               Placeholder="555-555-5555"
               FontSize="18"
               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="18"
                Margin="0, 20"
                VerticalOptions="Center"
                HorizontalOptions="Center" />
    </StackLayout>
</ContentPage>

在此示例中,视觉状态附加到 StackLayout,并且有两个名为 ValidInvalid 的互斥状态。 如果 Entry 不包含有效的电话号码,则当前状态为 Invalid,因此 Entry 具有粉红色背景,第二个 Label 为可见,Button 被禁用。 输入有效的电话号码时,当前状态将变为 ValidEntry 获得绿黄色背景,第二个 Label 消失,Button 现在已启用:

视觉状态验证示例的屏幕截图。

代码隐藏文件负责处理来自 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 方法来初始化状态。 应始终存在当前状态。 然后,代码隐藏文件在定义视觉状态的对象上调用具有状态名称的 VisualStateManager.GoToState

视觉状态触发器

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

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

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

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

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

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