.NET多平台应用 UI (.NET MAUI) 视觉状态管理器提供了一种结构化方式,用于从代码对用户界面进行视觉更改。 在大多数情况下,应用的用户界面在 XAML 中定义,此 XAML 可以包含描述 Visual State Manager 如何影响用户界面视觉对象的标记。
视觉状态管理器引入了 视觉状态的概念。 .NET MAUI视图(如 Button),根据其基本状态的不同(例如被禁用、被按下或具有输入焦点),可以展现出多种不同的视觉外观。 这些是按钮的状态。 视觉状态被收集在视觉状态组中。 视觉状态组中的所有视觉状态都是互斥的。 视觉状态和视觉状态组均由简单的文本字符串标识。
.NET MAUI Visual State Manager 使用以下视觉状态定义名为 CommonStates 的视觉状态组:
- 正常
- 已禁用
- 聚焦
- 已选择
- PointerOver
Normal、Disabled、Focused 和 PointerOver 视觉状态在所有从 VisualElement 派生的类中都受支持,VisualElement 是 Page 和 的基类。 此外,还可以定义自己的视觉状态组和视觉状态。
使用 Visual State Manager 定义界面的优势在于,你可以在 XAML 中完全控制视觉元素对不同状态的反应,而不需要从后台代码直接访问视觉元素,从而将所有的 UI 设计集中在一个地方。
注释
触发器还可以根据视图属性的更改或触发事件对用户界面中的视觉对象进行更改。 但是,使用触发器处理这些更改的各种组合可能会令人困惑。 使用视觉状态管理器时,视觉状态组中的视觉状态始终互斥。 在任何时候,每个组中只有一个状态是当前状态。
常见视觉状态
Visual State Manager 允许在 XAML 文件中包括标记,如果视图正常、禁用、具有输入焦点、处于选中状态或鼠标光标悬停在视图上方但未按下,则可以更改视图的视觉外观。 这些称为 常见状态。
例如,假设在页面上有一个 Entry 视图,并希望 Entry 的视觉外观以以下方式改变:
- 当Entry被禁用时,Entry的背景应为粉红色。
- 通常Entry应该有亮绿色背景。
- 当 Entry 具有输入焦点时,应该扩展到其正常高度的两倍。
- Entry在鼠标光标悬停在上方但未按下时,其背景应为浅蓝色。
可以将视觉状态管理器标记附加到单个视图,也可以将其定义为样式(如果应用于多个视图)。
定义视图上的视觉状态
该 VisualStateManager 类定义一个 VisualStateGroups 附加属性,该属性用于将视觉状态附加到视图。 该 VisualStateGroups 属性的类型是 VisualStateGroupList,它是 VisualStateGroup 对象的集合。 因此,附加属性的 VisualStateManager.VisualStateGroups 子级是一个 VisualStateGroup 对象。 此对象定义一个 x:Name 属性,该属性指示组的名称。 或者,你可以使用该 VisualStateGroup 类定义的 Name 属性。 有关附加属性的详细信息,请参阅 附加属性。
VisualStateGroup 类定义了一个名为 States 的属性,它是由 VisualState 对象组成的集合。
States 是 VisualStateGroups 类的内容属性,因此您可以将 VisualState 对象包含为 VisualStateGroup 的子级。 每个VisualState对象应使用x:Name或Name进行识别。
VisualState 类定义了一个名为 Setters 的属性,它是由 Setter 对象组成的集合。 这些对象与您在Setter对象中使用的Style对象相同。
Setters 不是 VisualState 的内容属性,因此必须为 Setters 属性包含属性元素标记。
Setter 对象应作为 Setters 的子对象插入。 每个 Setter 对象都指示该状态为当前状态时的属性的值。 对象引用 Setter 的任何属性都必须由可绑定属性提供支持。
重要
为了使视觉状态 Setter 对象正常运行,VisualStateGroup 必须包含 VisualState 状态的 Normal 对象。 如果此视觉状态没有任何 Setter 对象,则应将其作为空视觉状态 (<VisualState Name="Normal" />) 包含。
以下示例显示了在以下对象 Entry上定义的视觉状态:
<Entry FontSize="18">
<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" />
</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>
</VisualStateManager.VisualStateGroups>
</Entry>
以下屏幕截图显示了 Entry 其四个定义的视觉状态:
Entry在Normal状态时,其背景为酸橙色。 当 Entry 获得输入焦点时,其字号会翻倍。
Entry禁用后,其背景变为粉红色。 当获得输入焦点时,Entry 不会保留其背景颜色为石灰绿。 当鼠标指针悬停在 Entry鼠标指针上方但未按下时, Entry 背景变为浅蓝色。 当视觉状态管理器在视觉状态之间进行切换时,由上一状态设置的属性被取消设置。 因此,视觉状态是相互排斥的。
如果希望在 Entry 状态时 Focused 具有石灰背景,请在该视觉状态下添加另一个 Setter。
<VisualState Name="Focused">
<VisualState.Setters>
<Setter Property="FontSize" Value="36" />
<Setter Property="BackgroundColor" Value="Lime" />
</VisualState.Setters>
</VisualState>
在样式中定义视觉状态
通常需要在两个或多个视图中共享相同的视觉状态。 在这种情况下,可以在Style中定义视觉状态。 可以通过为属性添加 Setter 对象 VisualStateManager.VisualStateGroups 来实现此目的。 对象 Setter 的内容属性是其属性 Value,因此可以将其指定为对象 Setter 的子项。 该VisualStateGroups属性的类型是VisualStateGroupList,因此Setter对象的子级是能包含VisualStateGroupList对象并可向其添加VisualStateGroup的VisualState。
以下示例显示了用于定义常见视觉状态的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中定义的视觉状态:
| Class | States | 详细信息 |
|---|---|---|
| Button | Pressed |
按钮视觉状态 |
| CarouselView |
DefaultItem、CurrentItem、PreviousItem、NextItem |
CarouselView 视觉状态 |
| CheckBox | IsChecked |
CheckBox 视觉状态 |
| CollectionView | Selected |
CollectionView 视觉状态 |
| ImageButton | Pressed |
ImageButton 视觉状态 |
| RadioButton |
Checked、Unchecked |
RadioButton 视觉状态 |
| Switch |
On、Off |
切换视觉状态 |
| VisualElement |
Normal、Disabled、Focused、PointerOver |
常见状态 |
在多个元素上设置状态
在前面的示例中,视觉状态附加到单个元素并针对单个元素进行操作。 但是,也可以创建附加到单个元素的视觉状态,但在同一范围内设置其他元素的属性。 这样就避免了对运行状态的每个元素重复视觉状态。
该 Setter 类型具有 TargetName 类型的 string 属性,该属性表示要由 Setter 的视觉状态操作的目标对象。 当TargetName属性被定义时,将Setter设定在Property所定义的对象的TargetName为Value。
<Setter TargetName="label"
Property="Label.TextColor"
Value="Red" />
在此示例中, Label 命名项 label 的属性 TextColor 将设置为 Red. 设置 TargetName 属性时,必须在 Property 指定属性的完整路径。 因此,若要在TextColor上设置Label属性,需将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>
在此示例中,Normal处于活动状态,当Button未按下时,可以将响应输入到Entry中。
Pressed在按下Button时,其状态将变为活动状态,并且Scale属性将从默认值1更改为0.8。 此外,名为 Entry 的 entry 其 Text 属性将被设置为巴黎。 按下 Button 后,它将被缩放到略小的尺寸,而 Entry 则显示巴黎:
然后,当Button被释放时,它会重新缩放为默认值1,而Entry将显示所有先前输入的文本。
重要
在指定Setter属性的TargetName元素中,属性路径不被支持。
定义自定义视觉状态
可以通过定义自定义视觉状态来实现,就像定义常见状态的视觉状态一样,但具有所选名称,然后调用 VisualStateManager.GoToState 方法来激活状态。
以下示例演示如何使用 Visual State Manager 进行输入验证:
<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其中,并且有两个相互排斥的状态命名 Valid 和 Invalid。
Entry如果不包含有效的电话号码,则当前状态为Invalid,因此Entry具有粉红色背景,第二Label个为可见,并且Button处于禁用状态。 输入有效的电话号码时,当前状态将变为 Valid。
Entry 变为浅绿色背景,第二个 Label 消失,Button 现在已启用:
后台代码文件负责处理来自 TextChanged 的 Entry 事件。 处理程序使用正则表达式来确定输入字符串是否有效。
GoToState代码隐藏文件内的方法调用VisualStateManager.GoToState对象上的静态StackLayout方法:
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。
状态触发器已添加到 StateTriggers 的 VisualState 集合中。 此集合可以包含单个状态触发器或多个状态触发器。 当集合中的任何状态触发器处于活动状态时,将应用 A VisualState 。
使用状态触发器控制视觉状态时,.NET MAUI使用以下优先规则来确定哪个触发器(以及相应的VisualState)将处于活动状态:
- 派生自 StateTriggerBase. 的任何触发器。
- 由于AdaptiveTrigger条件被满足,MinWindowWidth已被激活。
- 由于AdaptiveTrigger条件被满足,MinWindowHeight已被激活。
如果多个触发器同时处于活动状态(例如两个自定义触发器),则标记中声明的第一个触发器优先。
有关状态触发器的详细信息,请参阅 状态触发器。
使视觉状态失效并重新应用视觉状态
在运行时修改 VisualState 中的 setter 值时,受影响的控件不会自动更新更改。 InvalidateVisualStates(VisualElement) 方法强制一个 VisualElement 取消应用当前状态的设置器,然后在所有视觉状态组中重新应用这些设置器,这样更新的值才能生效。
以下示例修改视觉状态的 setter 值,然后调用 InvalidateVisualStates 以刷新控件:
// Locate the "Pressed" visual state defined on the button.
var groups = VisualStateManager.GetVisualStateGroups(myButton);
var pressedState = groups
.SelectMany(g => g.States)
.First(s => s.Name == "Pressed");
// Modify a setter value in-place.
pressedState.Setters[0].Value = Colors.Orange;
// Force the control to reapply the current state's setters.
VisualStateManager.InvalidateVisualStates(myButton);
注释
InvalidateVisualStates是调用者驱动的 API — .NET MAUI 不会自动检测视觉状态设置值的改变。 必须在应反映更新的视觉状态的每个元素上调用此方法。