第二十三章:触发器和行为(十二)

简介: 淡化和定向在本书中,您已经看到了几个颜色选择程序,可以通过使用三个Slider元素以交互方式形成颜色。 本章的最后一个示例是另一个颜色选择程序,但是这个程序为您提供了选项:它包含三个标记为“RGB Hex”,“RGB Float”和“HSL”的单选按钮(实际上是简单的Label元素)。

淡化和定向
在本书中,您已经看到了几个颜色选择程序,可以通过使用三个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添加舍入。这避免了无限循环。
这是以纵向模式运行的程序。每个平台都会显示一个不同的选项,但您必须自己运行该程序以查看淡入淡出的动画:
2019_04_12_104159
把这本书(或你的电脑屏幕或者你的脑袋)翻过来,你会看到该程序如何响应横向模式:
2019_04_12_104245
也许MultiColorSliders程序最好的部分是代码隐藏文件,它只包含对InitializeComponent的调用:

namespace MultiColorSliders
{
    public partial class MultiColorSlidersPage : ContentPage
    {
        public MultiColorSlidersPage()
        {
            InitializeComponent();
        }
    }
}

当然,MultiColorSliders中有相当多的代码支持,包括两个Behavior 派生词,一个Action 派生词,一个IValueConverter实现,以及一个用作ViewModel的INotifyPropertyChanged实现。
但是,所有这些代码都在可重用组件中被隔离,这使得该程序成为MVVM设计理念的模型。

目录
相关文章
|
JavaScript Android开发
第二十三章:触发器和行为(七)
行为 触发器和行为通常是串联讨论的,因为它们具有一些应用重叠。 有时候你会感到困惑是否使用触发器或行为,因为似乎要么这样做工作。你可以用触发器做任何事情,你也可以做一个行为。 但是,行为总是涉及一些代码,这是一个派生自Behavior 的类。
952 0
|
JavaScript Android开发
第二十三章:触发器和行为(十一)
单选按钮内置于旧汽车仪表板中的无线电通常具有一排六个(左右)按钮,可以为各种无线电台“编程”。 按下其中一个按钮会导致无线电跳转到该预选电台,并且还会弹出前一个选择按钮。那些旧的汽车收音机现在是古董,但我们的电脑屏幕上的互斥选项仍然由我们称为单选按钮的视觉对象表示。
913 0
|
JavaScript Android开发
第二十三章:触发器和行为(十)
响应水龙头切换视图的各种表现形式演示了一种响应XAML文件中的点击的方法。 如果将tap事件集成到VisualElement类中,您可以使用EventTrigger更直接且更轻松地获取它们。 但是您无法将EventTrigger附加到TapGestureRecognizer。
546 0
|
JavaScript Android开发 Windows
第二十三章:触发器和行为(九)
切换和复选框在第15章“交互式界面”和第16章“数据绑定”中,您了解了如何构造传统的CheckBox视图。 但是,自定义视图的另一种方法是将视图的交互逻辑合并到行为中,然后完全在XAML中实现视觉效果。
731 0
|
Android开发
第二十三章:触发器和行为(八)
具有属性的行为Behavior 类派生自Behavior类,该类派生自BindableObject。这表明您的Behavior 派生可以定义自己的可绑定属性。之前你看过一些Action 衍生产品,比如ScaleAction和ShiverAction,它们定义了一些属性以赋予它们更大的灵活性。
651 0
|
JavaScript Android开发
第二十三章:触发器和行为(六)
MultiTrigger中的组合条件Trigger和DataTrigger都有效地监视属性以确定它是否等于特定值。 这称为触发器的条件,如果条件为真,则调用Setter对象的集合。作为程序员,您可能会开始怀疑是否可以在触发器中具有多个条件。
657 0
|
JavaScript Android开发 iOS开发
第二十三章:触发器和行为(五)
数据触发器到目前为止,您只看到在特定对象的上下文中运行的触发器。 触发器通过更改同一对象的另一个属性或通过调用影响该对象的Action来响应对象属性的更改。 EventTrigger同样响应一个对象触发的事件,以在同一个对象上调用Action。
943 0
|
JavaScript Android开发 Windows
第二十三章:触发器和行为(四)
更多事件触发器前一章关于动画的章节展示了一个按钮,它在点击时旋转或缩放。 虽然大多数动画示例都是为了制作有趣的演示而采取极端措施,但是按钮用一点动画来响应点击并不是不合理的。 这是EventTrigger的完美工作。
782 0
|
JavaScript Android开发
第二十三章:触发器和行为(三)
触发动作和动画虽然某些触发器可以完全在XAML中实现,但其他触发器需要一些代码支持。 如您所知,Xamarin.Forms没有直接支持在XAML中实现动画,因此如果您想使用触发器为元素设置动画,则需要一些代码。
1028 0
|
JavaScript Android开发
第二十三章:触发器和行为(二)
触发器 在最普遍(和最模糊)的意义上,触发器是导致响应的条件。 更具体地说,触发器通过设置另一个属性或运行一些代码来响应属性更改或触发事件。 几乎总是,设置的属性或运行的代码涉及用户界面,并在XAML中表示。
983 0
下一篇
DataWorks