淡化和定向
在本书中,您已经看到了几个颜色选择程序,可以通过使用三个Slider元素以交互方式形成颜色。 本章的最后一个示例是另一个颜色选择程序,但是这个程序为您提供了选项:它包含三个标记为“RGB Hex”,“RGB Float”和“HSL”的单选按钮(实际上是简单的Label元素)。 这些允许您以三种不同的方式选择颜色:
- 红色,绿色和蓝色十六进制值,范围从00到FF。
- 红色,绿色和蓝色浮点值,范围从0到1。
- 作为色调,饱和度和亮度浮点值,范围从0到1。
在这三个选项之间切换可能起初看起来很复杂。您可能会想到需要使用代码重新定义Slider元素的范围并重新格式化显示的文本以显示值。但是,您实际上可以在XAML中定义整个用户界面。
第一个技巧是XAML文件实际上包含九个Slider元素,并附带Label元素来显示值。每组三个Slider和Label元素占用StackLayout,其IsVisible属性绑定到连接到三个单选按钮的RadioBehavior对象之一。三个StackLayout元素占用单格网格,非常类似于RadioLabels和RadioStyle程序中的T恤图片。
但是让我们更具挑战性:当您选择其中一个单选按钮时,您可能希望一组三个Slider和Label元素被另一个替换。让我们改为拥有前者
设置淡出,新设置淡入。
如何才能做到这一点?
让我们构建标记。如果您只想将一个StackLayout替换为另一个,请将StackLayout的IsVisible属性绑定到相应RadioBehavior的IsChecked属性:
<StackLayout IsVisible="{Binding Source={x:Reference hexRadio},
Path=IsChecked}">
<!-- Trio of Slider and Label elements -->
</StackLayout>
要改为淡出旧的淡入淡出,首先需要将StackLayout的IsVisible属性初始化为False并附加一个引用RadioBehavior的IsChecked属性的DataTrigger:
<StackLayout IsVisible="False">
<StackLayout.Triggers>
<DataTrigger TargetType="StackLayout"
Binding="{Binding Source={x:Reference hexRadio},
Path=IsChecked}"
Value="True">
</DataTrigger>
</StackLayout.Triggers>
<!-- Trio of Slider and Label elements -->
</StackLayout>
然后,您需要向EnterActions和ExitActions集合添加一个Action衍生物,而不是将一个或两个Setter添加到DataTrigger:
<StackLayout IsVisible="False">
<StackLayout.Triggers>
<DataTrigger TargetType="StackLayout"
Binding="{Binding Source={x:Reference hexRadio},
Path=IsChecked}"
Value="True">
<DataTrigger.EnterActions>
<toolkit:FadeEnableAction Enable="True" />
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<toolkit:FadeEnableAction Enable="False" />
</DataTrigger.ExitActions>
</DataTrigger>
</StackLayout.Triggers>
<!-- Trio of Slider and Label elements -->
</StackLayout>
正如您所记得的,当条件变为真时(在这种情况下,当相应的RadioBehavior的IsChecked属性为True时)调用EnterActions,并且当条件变为false时调用ExitActions。
这个假设的FadeEnableAction类有一个名为Enable的布尔属性。当Enable属性为True时,我们希望FadeEnableAction使用FadeTo扩展方法将Opacity属性设置为从0(不可见)到1(完全可见)的动画。当Enable为False时,我们希望FadeTo将不透明度从1设置为0.请记住,当一个StackLayout(及其子元素)淡出时,另一个同时淡入。
但是,除非FadeEnableAction在Enable设置为True时将IsVisible设置为true,否则StackLayout将根本不可见。同样,当Enable设置为False时,FadeEnableAction必须通过将IsVisible设置为false来结束。
在两组Slider和Label元素之间转换期间,您可能不希望两个集都响应用户输入。因此,FadeEnableAction还必须操纵StackLayout的IsEnabled属性,该属性启用或禁用其所有子节点。由于两个动画将同时进行 - 一个StackLayout淡出而另一个淡入 - 在动画中途改变IsEnabled属性是有意义的。
这是Xamarin.FormsBook.Toolkit中的FadeEnableAction类,它满足所有这些条件:
namespace Xamarin.FormsBook.Toolkit
{
public class FadeEnableAction : TriggerAction<VisualElement>
{
public FadeEnableAction()
{
Length = 500;
}
public bool Enable { set; get; }
public int Length { set; get; }
async protected override void Invoke(VisualElement view)
{
if (Enable)
{
// Transition to visible and enabled.
view.IsVisible = true;
view.Opacity = 0;
await view.FadeTo(0.5, (uint)Length / 2);
view.IsEnabled = true;
await view.FadeTo(1, (uint)Length / 2);
}
else
{
// Transition to invisible and disabled.
view.Opacity = 1;
await view.FadeTo(0.5, (uint)Length / 2);
view.IsEnabled = false;
await view.FadeTo(0, (uint)Length / 2);
view.IsVisible = false;
}
}
}
}
让我们给自己另一个挑战。在第17章“掌握网格”中的“响应方向更改”部分中,您了解了如何使用网格在纵向和横向模式之间更改布局。基本上,页面上的所有布局大致分为两半,并成为网格的两个子节点。在纵向模式下,这两个孩子进入两行网格,在横向模式下,他们分为两列。
这样的事情可以通过行为来处理吗?容纳对方向的广义响应将很困难,但是一种简单的方法可能是假设在纵向模式下,第二行应该自动调整,而第一行使用剩余的可用空间。在横向模式下,屏幕简单地分成两半。这就是第17章中GridRgbSliders程序的工作方式,以及第20章中的MandelbrotXF程序。
以下GridOrientationBehavior只能附加到Grid。 Grid不能定义任何行定义或列定义 - 行为负责处理 - 并且它必须只包含两个子节点。该行为监视Grid的SizeChanged事件。当该大小更改时,Behavior将设置Grid的行和列定义以及Grid的两个子项的行和列设置:
namespace Xamarin.FormsBook.Toolkit
{
// Assumes Grid with two children without any
// row or column definitions set.
public class GridOrientationBehavior : Behavior<Grid>
{
protected override void OnAttachedTo(Grid grid)
{
base.OnAttachedTo(grid);
// Add row and column definitions.
grid.RowDefinitions.Add(new RowDefinition());
grid.RowDefinitions.Add(new RowDefinition());
grid.ColumnDefinitions.Add(new ColumnDefinition());
grid.ColumnDefinitions.Add(new ColumnDefinition());
grid.SizeChanged += OnGridSizeChanged;
}
protected override void OnDetachingFrom(Grid grid)
{
base.OnDetachingFrom(grid);
grid.SizeChanged -= OnGridSizeChanged;
}
private void OnGridSizeChanged(object sender, EventArgs args)
{
Grid grid = (Grid)sender;
if (grid.Width <= 0 || grid.Height <= 0)
return;
// Portrait mode
if (grid.Height > grid.Width)
{
// Set row definitions.
grid.RowDefinitions[0].Height = new GridLength(1, GridUnitType.Star);
grid.RowDefinitions[1].Height = GridLength.Auto;
// Set column definitions.
grid.ColumnDefinitions[0].Width = new GridLength(1, GridUnitType.Star);
grid.ColumnDefinitions[1].Width = new GridLength(0);
//Position first child.
Grid.SetRow(grid.Children[0], 0);
Grid.SetColumn(grid.Children[0], 0);
// Position second child.
Grid.SetRow(grid.Children[1], 1);
Grid.SetColumn(grid.Children[1], 0);
}
// Landscape mode
else
{
// Set row definitions.
grid.RowDefinitions[0].Height = new GridLength(1, GridUnitType.Star);
grid.RowDefinitions[1].Height = new GridLength(0);
// Set column definitions.
grid.ColumnDefinitions[0].Width = new GridLength(1, GridUnitType.Star);
grid.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
//Position first child.
Grid.SetRow(grid.Children[0], 0);
Grid.SetColumn(grid.Children[0], 0);
// Position second child.
Grid.SetRow(grid.Children[1], 0);
Grid.SetColumn(grid.Children[1], 1);
}
}
}
}
现在让我们把它们放在一个程序调用MultiColorSliders中。 该程序的主干是第18章“MVVM”中介绍的ColorViewModel,可以在Xamarin.FormsBook.Toolkit库中找到。 ColorViewModel的一个实例被设置为包含页面所有内容的Grid的BindingContext。 三组Slider和Label元素都包含与ViewModel的Red,Green,Blue,Hue,Saturation和Luminosity属性的绑定。 对于十六进制选项,第17章中介绍的DoubleToIntConverter将红色,绿色和蓝色属性的double值转换为乘以255的整数,以便每个Label显示。
这是XAML文件。 它相当长,因为它包含三组三个Slider和Label元素,但有几条注释有助于指导您完成各个部分:
<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="MultiColorSliders.MultiColorSlidersPage">
<ContentPage.Padding>
<OnPlatform x:TypeArguments="Thickness"
iOS="0, 20, 0, 0" />
</ContentPage.Padding>
<ContentPage.Resources>
<ResourceDictionary>
<toolkit:ColorViewModel x:Key="colorViewModel" />
<toolkit:DoubleToIntConverter x:Key="doubleToInt" />
<Style x:Key="baseStyle" TargetType="Label">
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style x:Key="unselectedStyle" TargetType="Label"
BasedOn="{StaticResource baseStyle}">
<Setter Property="TextColor" Value="Gray" />
</Style>
<Style x:Key="selectedStyle" TargetType="Label"
BasedOn="{StaticResource baseStyle}">
<Setter Property="TextColor" Value="Accent" />
<Setter Property="Scale" Value="1.5" />
</Style>
<!-- Implicit style for labels underneath sliders -->
<Style TargetType="Label" BasedOn="{StaticResource baseStyle}" />
</ResourceDictionary>
</ContentPage.Resources>
<Grid>
<Grid.BindingContext>
<toolkit:ColorViewModel Alpha="1" />
</Grid.BindingContext>
<!-- The GridOrientationBehavior takes care of the row and
column definitions, and the row and column settings
of the two Grid children. -->
<Grid.Behaviors>
<toolkit:GridOrientationBehavior />
</Grid.Behaviors>
<!-- First child of Grid is on top or at left. -->
<BoxView Color="{Binding Color}" />
<!-- Second child of Grid is on bottom or at right. -->
<StackLayout Padding="10">
<!-- Three-column Grid for radio labels -->
<Grid>
<Label Text="RGB Hex" Grid.Column="0"
Style="{StaticResource unselectedStyle}">
<Label.Behaviors>
<toolkit:RadioBehavior x:Name="hexRadio"
IsChecked="true" />
</Label.Behaviors>
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={x:Reference hexRadio},
Path=IsChecked}"
Value="True">
<Setter Property="Style" Value="{StaticResource selectedStyle}" />
</DataTrigger>
</Label.Triggers>
</Label>
<Label Text="RGB Float" Grid.Column="1"
Style="{StaticResource unselectedStyle}">
<Label.Behaviors>
<toolkit:RadioBehavior x:Name="floatRadio" />
</Label.Behaviors>
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={x:Reference floatRadio},
Path=IsChecked}"
Value="True">
<Setter Property="Style" Value="{StaticResource selectedStyle}" />
</DataTrigger>
</Label.Triggers>
</Label>
<Label Text="HSL" Grid.Column="2"
Style="{StaticResource unselectedStyle}">
<Label.Behaviors>
<toolkit:RadioBehavior x:Name="hslRadio" />
</Label.Behaviors>
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={x:Reference hslRadio},
Path=IsChecked}"
Value="True">
<Setter Property="Style" Value="{StaticResource selectedStyle}" />
</DataTrigger>
</Label.Triggers>
</Label>
</Grid>
<!-- Single-cell Grid for three sets of sliders and labels -->
<Grid>
<!-- StackLayout for RGB Hex sliders and labels -->
<StackLayout>
<StackLayout.Triggers>
<DataTrigger TargetType="StackLayout"
Binding="{Binding Source={x:Reference hexRadio},
Path=IsChecked}"
Value="True">
<DataTrigger.EnterActions>
<toolkit:FadeEnableAction Enable="True" />
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<toolkit:FadeEnableAction Enable="False" />
</DataTrigger.ExitActions>
</DataTrigger>
</StackLayout.Triggers>
<Slider Value="{Binding Red, Mode=TwoWay}" />
<Label Text="{Binding Red, StringFormat='Red = {0:X2}',
Converter={StaticResource doubleToInt},
ConverterParameter=255}" />
<Slider Value="{Binding Green, Mode=TwoWay}" />
<Label Text="{Binding Green, StringFormat='Green = {0:X2}',
Converter={StaticResource doubleToInt},
ConverterParameter=255}" />
<Slider Value="{Binding Blue, Mode=TwoWay}" />
<Label Text="{Binding Blue, StringFormat='Blue = {0:X2}',
Converter={StaticResource doubleToInt},
ConverterParameter=255}" />
</StackLayout>
<!-- StackLayout for RGB float sliders and labels -->
<StackLayout IsVisible="False">
<StackLayout.Triggers>
<DataTrigger TargetType="StackLayout"
Binding="{Binding Source={x:Reference floatRadio},
Path=IsChecked}"
Value="True">
<DataTrigger.EnterActions>
<toolkit:FadeEnableAction Enable="True" />
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<toolkit:FadeEnableAction Enable="False" />
</DataTrigger.ExitActions>
</DataTrigger>
</StackLayout.Triggers>
<Slider Value="{Binding Red, Mode=TwoWay}" />
<Label Text="{Binding Red, StringFormat='Red = {0:F2}'}" />
<Slider Value="{Binding Green, Mode=TwoWay}" />
<Label Text="{Binding Green, StringFormat='Green = {0:F2}'}" />
<Slider Value="{Binding Blue, Mode=TwoWay}" />
<Label Text="{Binding Blue, StringFormat='Blue = {0:F2}'}" />
</StackLayout>
<!-- StackLayout for HSL sliders and labels -->
<StackLayout IsVisible="False">
<StackLayout.Triggers>
<DataTrigger TargetType="StackLayout"
Binding="{Binding Source={x:Reference hslRadio},
Path=IsChecked}"
Value="True">
<DataTrigger.EnterActions>
<toolkit:FadeEnableAction Enable="True" />
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<toolkit:FadeEnableAction Enable="False" />
</DataTrigger.ExitActions>
</DataTrigger>
</StackLayout.Triggers>
<!-- Trio of Slider and Label elements -->
<Slider Value="{Binding Hue, Mode=TwoWay}" />
<Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}" />
<Slider Value="{Binding Saturation, Mode=TwoWay}" />
<Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}" />
<Slider Value="{Binding Luminosity, Mode=TwoWay}" />
<Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}" />
</StackLayout>
</Grid>
</StackLayout>
</Grid>
</ContentPage>
您可能还记得第18章中介绍的ColorViewModel类舍入了进出ViewModel的颜色组件。 MultiColorSliders恰好是显示未包含值的问题的程序。这是问题所在:
对于Android,Xamarin.Forms使用SeekBar实现Slider,而Android SeekBar只有整数Progress值,范围从0到整数Max属性。若要转换为Slider的浮点Value属性,Xamarin.Forms将SeekBar的Max属性设置为1000,然后根据Slider的Minimum和Maximum属性执行计算。这意味着当Minimum和Maximum的默认值分别为0和1时,Value属性仅以0.001为增量增加,并且始终可以用三个小数位表示。
但是,ColorViewModel使用Color结构在RGB和HSL表示之间进行转换,在此特定程序中,表示RGB和HSL值的所有属性都绑定到Slider元素。即使Slider元素设置的红色,绿色和蓝色属性的值四舍五入到最接近的0.001,结果的Hue,Saturation和Luminosity值也会有三个以上的小数位。如果ViewModel没有舍入这些值,那就是一个问题。当Slider元素的Value属性从这些值设置时,Slider会有效地将它们舍入到三个小数位,然后通过创建一个新的Color触发一个ColorViewModel响应的PropertyChanged事件,从而产生新的红色,绿色和蓝色属性,随之而来的是无限循环。
正如您在第18章中看到的那样,解决方案是向ColorViewModel添加舍入。这避免了无限循环。
这是以纵向模式运行的程序。每个平台都会显示一个不同的选项,但您必须自己运行该程序以查看淡入淡出的动画:
把这本书(或你的电脑屏幕或者你的脑袋)翻过来,你会看到该程序如何响应横向模式:
也许MultiColorSliders程序最好的部分是代码隐藏文件,它只包含对InitializeComponent的调用:
namespace MultiColorSliders
{
public partial class MultiColorSlidersPage : ContentPage
{
public MultiColorSlidersPage()
{
InitializeComponent();
}
}
}
当然,MultiColorSliders中有相当多的代码支持,包括两个Behavior 派生词,一个Action 派生词,一个IValueConverter实现,以及一个用作ViewModel的INotifyPropertyChanged实现。
但是,所有这些代码都在可重用组件中被隔离,这使得该程序成为MVVM设计理念的模型。