Xamarin.Forms 可视状态管理器
使用可视状态管理器根据代码中设置的视觉状态对 XAML 元素进行更改。
可视状态管理器 (VSM) 提供了一种结构化方式,用于从代码对用户界面进行视觉更改。 在大多数情况下,应用程序的用户界面在 XAML 中定义,此 XAML 包含描述可视状态管理器如何影响用户界面视觉对象的标记。
VSM 引入了视觉状态的概念。 Xamarin.Forms 视图(如 Button
)可以具有多种不同的视觉外观,具体取决于其基础状态,可以是禁用、按下,或具有输入焦点。 这些是按钮的状态。
视觉状态在“视觉状态组”中收集。 视觉状态组中的所有视觉状态都是互斥的。 视觉状态和视觉状态组均由简单的文本字符串标识。
Xamarin.Forms 可视状态管理器使用以下视觉状态定义一个名为“CommonStates”的视觉状态组:
- "Normal"
- "Disabled"
- "Focused"
- "Selected"
派生自 VisualElement
的所有类都支持此视觉状态组,它是 View
和 Page
的基类。
还可以自行定义视觉状态组和视觉状态,本文将对此进行演示。
注意
熟悉触发器的 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:Name
或 Name
,但不能同时使用两者。
VisualStateGroup
类定义了一个名为 States
的属性,该属性是 VisualState
对象的集合。 States
是VisualStateGroups
的 内容属性,因此可以直接在 VisualStateGroup
标记之间包括 VisualState
标记。 (内容属性在“基本 XAML 语法”一文中进行了讨论。)
下一步是为该组中的每个视觉状态添加一对标签。 还可以使用 x:Name
或 Name
进行标识:
<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) 上运行的启动页面:
当前视觉状态为“已禁用”,因此第二个 Entry
的背景在 iOS 和 Android 屏幕上为粉红色。 禁用 Entry
时,Entry
的 UWP 实现不允许设置背景色。
在第三个 Entry
中输入一些文本时,第二个 Entry
将切换到“正常”状态,背景现在为青绿色:
到达第二个 Entry
时,它会获取输入焦点。 它会切换到“Focused”状态,并扩展到原高度的两倍:
请注意,当 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
都会有青绿色背景:
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" />
在此示例中,名为 label
的 Label
将其 TextColor
属性设置为 Red
。 设置 TargetName
属性时,必须在 Property
中指定属性的完整路径。 因此,若要在 Label
上设置 TextColor
属性,请将 Property
指定为 Label.TextColor
。
注意
Setter
对象引用的任何属性都必须由可绑定属性提供支持。
示例中的“具有资源库 TargetName 的 VSM”页面演示如何从单个视觉状态组在多个元素上设置状态。 XAML 文件由包含 Label
元素、Entry
和 Button
的 StackLayout
组成:
<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”状态处于活动状态,并且可以输入对问题的响应:
按下 Button
时,“Pressed”状态会变为活动状态:
“Pressed”VisualState
会指定当按下 Button
时,其 Scale
属性将从默认值 1 更改为 0.8。 此外,已命名为 entry
的 Entry
会将其 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
和一个 Button
的 StackLayout
:
<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
:
输入有效的电话号码时,当前状态将变为“有效”。 Entry
获得绿黄色背景,第二个 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
方法以初始化状态。 应始终存在当前状态。 但是,代码中没有任何对视觉状态组名称的引用,尽管为了清晰起见,XAML 中将其称为 "ValidationStates"。
请注意,代码隐藏文件只需要考虑定义视觉状态的页面上的对象,并为此对象调用 VisualStateManager.GoToState
。 这是因为这两种视觉状态都针对页面上的多个对象。
你可能想知道:如果代码隐藏文件必须引用定义视觉状态的页面上的对象,为什么代码隐藏文件不能直接访问此对象和其他对象? 它当然可以。 但是,使用 VSM 的优点是,你可以控制视觉元素在 XAML 中对不同状态的反应方式,从而将所有 UI 设计保留在一个位置。 这可以避免通过直接从代码隐藏访问视觉元素来设置视觉外观。
视觉状态触发器
视觉状态支持状态触发器,状态触发器是一组专用的触发器,用于定义应用 VisualState
的条件。
状态触发器添加到 VisualState
的 StateTriggers
集合。 此集合可以包含一个或多个状态触发器。 当此集合中的任何状态触发器处于活动状态时,便会应用 VisualState
。
使用状态触发器来控制视觉对象状态时,Xamarin.Forms 使用以下优先规则来确定哪个触发器(以及相应的 VisualState
)处于活动状态:
- 任何派生自
StateTriggerBase
的触发器。 - 因满足
MinWindowWidth
条件而激活的AdaptiveTrigger
。 - 因满足
MinWindowHeight
条件而激活的AdaptiveTrigger
。
如果多个触发器同时处于活动状态(例如,两个自定义触发器),则标记中声明的第一个触发器优先。
有关状态触发器的详细信息,请参阅状态触发器。
使用可视状态管理器进行自适应布局
在手机上运行的 Xamarin.Forms 应用程序通常可以在纵向或横向纵横比中查看,并且假定许多不同的大小和纵横比,则可以调整桌面上运行的 Xamarin.Forms 程序的大小。 设计良好的应用程序可能会针对这些不同的页面或窗口外形规格以不同的方式显示其内容。
此方法有时称为自适应布局。 由于自适应布局只涉及程序的视觉对象,因此它是可视状态管理器的理想应用。
一个简单的示例是一个应用程序,它显示影响应用程序内容的按钮的小型集合。 在纵向模式下,这些按钮可能显示在页面顶部的水平行中:
在横向模式下,按钮数组可能会移到一侧,并显示在列中:
从上到下,程序在通用 Windows 平台、Android 和 iOS 上运行。
示例中的“VSM 自适应布局”页面定义了一个名为“OrientationStates”的组,其中包含两个名为“竖屏”和“横屏”的可视状态。 (更复杂的方法可能是基于多个不同的页面或窗口宽度进行的。)
VSM 标记会出现在 XAML 文件中的四个位置。 名为 mainStack
的 StackLayout
包含菜单和内容,这是一个 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
和名为 menuStack
的 StackLayout
会实现按钮菜单。 这些布局的方向与 mainStack
相反。 菜单应在纵向模式下为水平,在横向模式下为垂直。
VSM 标记的第四部分是按钮本身的隐式样式。 此标记设置特定于纵向和横向的 VerticalOptions
、HorizontalOptions
和 Margin
属性。
代码隐藏文件将 menuStack
的 BindingContext
属性设置为实现 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
获得两个 StackLayout
和 ScrollView
元素,然后循环访问 menuStack
的子级以调用 VisualStateManager.GoToState
获得 Button
元素。
代码隐藏文件似乎可以通过在 XAML 文件中设置元素的属性来更直接地处理方向更改,但可视状态管理器绝对是一种更具结构化的方法。 所有视觉对象都保存在 XAML 文件中,它们更易于检查、维护和修改。
具有 Xamarin.University 的可视状态管理器
Xamarin.Forms 3.0 可视状态管理器视频