响应方向变化
应用程序页面的布局通常与特定的外形和宽高比紧密相关。有时,应用程序将要求仅在纵向或横向模式下使用它。但是,当手机改变方向时,应用程序通常会尝试在屏幕上移动。
网格可以帮助应用程序适应方向更改。可以在XAML中定义网格,对纵向和横向模式都有一定的限制,然后一些代码可以在页面的SizeChanged处理程序中进行适当的调整。
如果您可以将应用程序的整个布局划分为两个大区域,当手机以纵向模式定向或水平定向为横向模式时,此作业最简单。将这些区域中的每一个放在网格的单独单元格中。当手机处于纵向模式时,网格有两行,当它处于横向模式时,它有两列。在下图中,第一个区域始终位于顶部或左侧。第二个区域可以是纵向模式的第二行,也可以是横向模式的第二列:
为了使事情变得相当简单,您需要在XAML中定义具有两行和两列的网格,但在纵向模式下,第二列的宽度为零,而在横向模式下,第二行的高度为零。
GridRgbSliders程序演示了这种技术。它类似于第15章“交互式界面”中的RgbSliders程序,除了布局使用Grid和StackLayout的组合,而Label元素通过使用带有值的数据绑定来显示Slider元素的当前值转换器和值转换器参数。 (稍后会详细介绍。)基于三个Slider元素设置BoxView的Color属性仍然需要代码,因为Color结构的R,G和B属性不受可绑定属性的支持,并且这些属性不能单独更改无论如何,因为他们没有公共集访问器。 (但是,在下一章中,在MVVM上,您将看到一种在代码隐藏文件中消除此逻辑的方法。)
正如您在下面的清单中所看到的,名为mainGrid的Grid确实有两行和两列。但是,它已初始化为纵向模式,因此第二列的宽度为零。 Grid的顶行包含BoxView,使用“*”(星号)设置尽可能大,而底行包含StackLayout和所有交互式控件。这是自动高度:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit=
"clr-namespace:Xamarin.FormsBook.Toolkit;assembly=Xamarin.FormsBook.Toolkit"
x:Class="GridRgbSliders.GridRgbSlidersPage"
SizeChanged="OnPageSizeChanged">
<ContentPage.Padding>
<OnPlatform x:TypeArguments="Thickness"
iOS="0, 20, 0, 0" />
</ContentPage.Padding>
<ContentPage.Resources>
<ResourceDictionary>
<toolkit:DoubleToIntConverter x:Key="doubleToInt" />
<Style TargetType="Label">
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
</ResourceDictionary>
</ContentPage.Resources>
<Grid x:Name="mainGrid">
<!-- Initialized for portrait mode. -->
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="0" />
</Grid.ColumnDefinitions>
<BoxView x:Name="boxView"
Grid.Row="0" Grid.Column="0" />
<StackLayout x:Name="controlPanelStack"
Grid.Row="1" Grid.Column="0"
Padding="10, 5">
<StackLayout VerticalOptions="CenterAndExpand">
<Slider x:Name="redSlider"
ValueChanged="OnSliderValueChanged" />
<Label Text="{Binding Source={x:Reference redSlider},
Path=Value,
Converter={StaticResource doubleToInt},
ConverterParameter=255,
StringFormat='Red = {0:X2}'}" />
</StackLayout>
<StackLayout VerticalOptions="CenterAndExpand">
<Slider x:Name="greenSlider"
ValueChanged="OnSliderValueChanged" />
<Label Text="{Binding Source={x:Reference greenSlider},
Path=Value,
Converter={StaticResource doubleToInt},
ConverterParameter=255,
StringFormat='Green = {0:X2}'}" />
</StackLayout>
<StackLayout VerticalOptions="CenterAndExpand">
<Slider x:Name="blueSlider"
ValueChanged="OnSliderValueChanged" />
<Label Text="{Binding Source={x:Reference blueSlider},
Path=Value,
Converter={StaticResource doubleToInt},
ConverterParameter=255,
StringFormat='Blue = {0:X2}'}" />
</StackLayout>
</StackLayout>
</Grid>
</ContentPage>
这是纵向视图:
XAML文件中的布局以两种方式为横向模式准备。首先,Grid已经有了第二列。这意味着要切换到横向模式,代码隐藏文件需要将第二行的高度更改为零,将第二列的宽度更改为非零值。
其次,包含所有Slider和Label元素的StackLayout可以从代码访问,因为它有一个名称,特别是controlPanelStack。然后,代码隐藏文件可以对此StackLayout进行Grid.SetRow和Grid.SetColumn调用,以将其从第1行和第0列移动到第0行和第1列。
在纵向模式下,BoxView的高度为“”(星号),StackLayout的高度为“自动”。这是否意味着StackLayout的宽度在横向模式下应该是Auto?这不是明智之举,因为它会缩小Slider元素的宽度。横向模式的一个更好的解决方案是给BoxView和StackLayout宽度为“”(星号),将屏幕分成两半。
这是代码隐藏文件,显示负责在纵向和横向模式之间切换的页面上的SizeChanged处理程序,以及设置BoxView颜色的Slider元素的ValueChanged处理程序:
public partial class GridRgbSlidersPage : ContentPage
{
public GridRgbSlidersPage()
{
// Ensure link to Toolkit library.
new Xamarin.FormsBook.Toolkit.DoubleToIntConverter();
InitializeComponent();
}
void OnPageSizeChanged(object sender, EventArgs args)
{
// Portrait mode.
if (Width < Height)
{
mainGrid.RowDefinitions[1].Height = GridLength.Auto;
mainGrid.ColumnDefinitions[1].Width = new GridLength(0, GridUnitType.Absolute);
Grid.SetRow(controlPanelStack, 1);
Grid.SetColumn(controlPanelStack, 0);
}
// Landscape mode.
else
{
mainGrid.RowDefinitions[1].Height = new GridLength(0, GridUnitType.Absolute);
mainGrid.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
Grid.SetRow(controlPanelStack, 0);
Grid.SetColumn(controlPanelStack, 1);
}
}
void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
{
boxView.Color = new Color(redSlider.Value, greenSlider.Value, blueSlider.Value);
}
}
这是横向展示的横向布局:
请注意,特别是在iOS和Android显示器上,每对Slider和Label元素如何组合在一起。这是第三种方式,即XAML文件准备好适应横向模式。每对Slider和Label元素都嵌套在一个嵌套的StackLayout中。这将给出CenterAndExpand的VerticalOptions设置以执行此间距。
稍微考虑安排BoxView和控制面板:在纵向模式下,操纵Slider元素的手指不会遮挡BoxView中的结果,而在横向模式下,惯用右手的用户的手指不会模糊BoxView也是。 (当然,左撇子用户可能会坚持使用程序选项来交换位置!)
屏幕截图显示了以十六进制显示的Slider值。这是通过数据绑定完成的,这通常是个问题。 Slider的Value属性是double类型,如果您尝试使用“X2”格式化十六进制的double,则会引发异常。类型转换器(例如,名为DoubleToIntConverter)必须将源double转换为int以进行字符串格式化。但是,Slider元素的设置范围为0到1,而格式为十六进制的整数值的范围必须介于0到255之间。
解决方案是使用Binding的ConverterParameter属性。设置为此属性的任何内容都作为第三个参数传递给值转换器中的Convert和ConvertBack方法。这是Xamarin.FormsBook.Toolkit库中的DoubleToIntConverter类:
namespace Xamarin.FormsBook.Toolkit
{
public class DoubleToIntConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
string strParam = parameter as string;
double multiplier = 1;
if (!String.IsNullOrEmpty(strParam))
{
Double.TryParse(strParam, out multiplier);
}
return (int)Math.Round((double)value * multiplier);
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
string strParam = parameter as string;
double divider = 1;
if (!String.IsNullOrEmpty(strParam))
{
Double.TryParse(strParam, out divider);
}
return (int)value / divider;
}
}
}
Convert和ConvertBack方法假定参数参数是一个字符串,如果是,则尝试将其转换为double。 然后将该值乘以转换的double值,然后将产品转换为int。
值转换器,转换器参数和字符串格式的组合将从Slider到0到1的值转换为0到255范围内的整数,然后将这些值格式化为两个十六进制数字:
<Label Text="{Binding Source={x:Reference redSlider},
Path=Value,
Converter={StaticResource doubleToInt},
ConverterParameter=255,
StringFormat='Red = {0:X2}'}" />
当然,如果您在代码中定义Binding,则可能将ConverterParameter属性设置为255的数值而不是字符串“255”,并且DoubleToIntConverter中的逻辑将失败。 简单的数值转换器通常比完全防弹更简单。
如果没有代码隐藏文件中的Slider事件处理程序,可以完全实现像GridRgbSliders这样的程序吗? 代码肯定仍然是必需的,但其中一些将被移离用户界面逻辑。 这是下一章探讨的Model-View-ViewModel架构的主要目标。