单选按钮
内置于旧汽车仪表板中的无线电通常具有一排六个(左右)按钮,可以为各种无线电台“编程”。 按下其中一个按钮会导致无线电跳转到该预选电台,并且还会弹出前一个选择按钮。
那些旧的汽车收音机现在是古董,但我们的电脑屏幕上的互斥选项仍然由我们称为单选按钮的视觉对象表示。
单选按钮有点类似于切换或复选框。 但是单选按钮总是在两个或更多组中找到。 选择或选中该组中的任何按钮会导致其他按钮取消选中。
单选按钮背后的逻辑很复杂,因为应用程序可能在同一页面上有几组单选按钮,这些组应该独立运行。 按下一个组中的按钮只会影响该组中的其他按钮,而不会影响任何其他组中的按钮。
传统上,单选按钮与共同父项分组。在Xamarin.Forms术语中,作为一个StackLayout的子节点的单选按钮被认为是在同一组中,而作为另一个StackLayout的子节点的单选按钮位于另一个独立组中。
但是,有一种更通用的方法来区分单选按钮组,即通过为每个组提供唯一的名称,这实际上意味着该组中的每个单选按钮引用相同的名称。
这些名称的问题在于它们会增加一些额外的开销,特别是当您只需要一组单选按钮时。因此,应该允许一组未通过名称识别的单选按钮。这称为默认组。
这是Xamarin.FormsBook.Toolkit库中基于这些原则的RadioBehavior类。您将此行为附加到要转换为单选按钮的每个视图。与ToggleBehavior类一样,RadioBehavior在它附加的可视元素上设置TapGestureRecognizer。它没有定义像ToggleBehavior这样的IsToggled属性,但它确实定义了一个非常相似的IsChecked属性,并指示单选按钮是选中还是未选中。 RadioBehavior类还定义了string类型的GroupName属性以标识该组;空值或空字符串表示默认组。
RadioBehavior类需要按组存储所有实例化的单选按钮,因此它定义了两个静态集合,其中一个是默认组中所有对象的简单List ,另一个是具有对应键的Dictionary到引用该命名组中所有对象的List 集合的组名:
namespace Xamarin.FormsBook.Toolkit
{
public class RadioBehavior : Behavior<View>
{
TapGestureRecognizer tapRecognizer;
static List<RadioBehavior> defaultGroup = new List<RadioBehavior>();
static Dictionary<string, List<RadioBehavior>> radioGroups =
new Dictionary<string, List<RadioBehavior>>();
public RadioBehavior()
{
defaultGroup.Add(this);
}
public static readonly BindableProperty IsCheckedProperty =
BindableProperty.Create("IsChecked",
typeof(bool),
typeof(RadioBehavior),
false,
propertyChanged: OnIsCheckedChanged);
public bool IsChecked
{
set { SetValue(IsCheckedProperty, value); }
get { return (bool)GetValue(IsCheckedProperty); }
}
static void OnIsCheckedChanged(BindableObject bindable, object oldValue,
object newValue)
{
RadioBehavior behavior = (RadioBehavior)bindable;
if ((bool)newValue)
{
string groupName = behavior.GroupName;
List<RadioBehavior> behaviors = null;
if (String.IsNullOrEmpty(groupName))
{
behaviors = defaultGroup;
}
else
{
behaviors = radioGroups[groupName];
}
foreach (RadioBehavior otherBehavior in behaviors)
{
if (otherBehavior != behavior)
{
otherBehavior.IsChecked = false;
}
}
}
}
public static readonly BindableProperty GroupNameProperty =
BindableProperty.Create("GroupName",
typeof(string),
typeof(RadioBehavior),
null,
propertyChanged: OnGroupNameChanged);
public string GroupName
{
set { SetValue(GroupNameProperty, value); }
get { return (string)GetValue(GroupNameProperty); }
}
static void OnGroupNameChanged(BindableObject bindable, object oldValue,
object newValue)
{
RadioBehavior behavior = (RadioBehavior)bindable;
string oldGroupName = (string)oldValue;
string newGroupName = (string)newValue;
if (String.IsNullOrEmpty(oldGroupName))
{
// Remove the Behavior from the default group.
defaultGroup.Remove(behavior);
}
else
{
// Remove the RadioBehavior from the radioGroups collection.
List<RadioBehavior> behaviors = radioGroups[oldGroupName];
behaviors.Remove(behavior);
// Get rid of the collection if it's empty.
if (behaviors.Count == 0)
{
radioGroups.Remove(oldGroupName);
}
}
if (String.IsNullOrEmpty(newGroupName))
{
// Add the new Behavior to the default group.
defaultGroup.Add(behavior);
}
else
{
List<RadioBehavior> behaviors = null;
if (radioGroups.ContainsKey(newGroupName))
{
// Get the named group.
behaviors = radioGroups[newGroupName];
}
else
{
// If that group doesn't exist, create it.
behaviors = new List<RadioBehavior>();
radioGroups.Add(newGroupName, behaviors);
}
// Add the Behavior to the group.
behaviors.Add(behavior);
}
}
protected override void OnAttachedTo(View view)
{
base.OnAttachedTo(view);
tapRecognizer = new TapGestureRecognizer();
tapRecognizer.Tapped += OnTapRecognizerTapped;
view.GestureRecognizers.Add(tapRecognizer);
}
protected override void OnDetachingFrom(View view)
{
base.OnDetachingFrom(view);
view.GestureRecognizers.Remove(tapRecognizer);
tapRecognizer.Tapped -= OnTapRecognizerTapped;
}
void OnTapRecognizerTapped(object sender, EventArgs args)
{
IsChecked = true;
}
}
}
列表底部的TapGestureRecognizer处理程序非常简单:当轻触可视对象时,附加到该可视对象的RadioBehavior对象将其IsChecked属性设置为true。如果IsChecked属性以前为false,则该更改将导致对OnIsCheckedChanged方法的调用,该方法将同一组中所有RadioBehavior对象的IsChecked属性设置为false。
这是一个简单的演示,用于选择T恤的尺寸。三个单选按钮是简单的Label元素,文本属性为“Small”,“Medium”和“Large”,这就是程序名为RadioLabels的原因。每个Label在其Behaviors集合中都有一个RadioBehavior。每个RadioBehavior都有一个x:Name用于数据绑定,但所有RadioBehavior对象的默认GroupName属性设置为null。每个Label在其Triggers集合中都有一个DataTrigger,它绑定到相应的RadioBehavior,以便在IsChecked属性为true时将Label的TextColor变为绿色。
请注意,中间RadioBehavior属性的IsChecked属性初始化为true,以便在程序启动时选择该对象:
<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"
xmlns:local="clr-namespace:RadioLabels"
x:Class="RadioLabels.RadioLabelsPage"
Padding="0, 50, 0, 0">
<StackLayout>
<Grid>
<Grid.Resources>
<ResourceDictionary>
<Style TargetType="Label">
<Setter Property="FontSize" Value="Medium" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
</ResourceDictionary>
</Grid.Resources>
<Label Text="Small"
TextColor="Gray"
Grid.Column="0">
<Label.Behaviors>
<toolkit:RadioBehavior x:Name="smallRadio" />
</Label.Behaviors>
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={x:Reference smallRadio},
Path=IsChecked}"
Value="True">
<Setter Property="TextColor" Value="Green" />
</DataTrigger>
</Label.Triggers>
</Label>
<Label Text="Medium"
TextColor="Gray"
Grid.Column="1">
<Label.Behaviors>
<toolkit:RadioBehavior x:Name="mediumRadio"
IsChecked="True" />
</Label.Behaviors>
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={x:Reference mediumRadio},
Path=IsChecked}"
Value="True">
<Setter Property="TextColor" Value="Green" />
</DataTrigger>
</Label.Triggers>
</Label>
<Label Text="Large"
TextColor="Gray"
Grid.Column="2">
<Label.Behaviors>
<toolkit:RadioBehavior x:Name="largeRadio" />
</Label.Behaviors>
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={x:Reference largeRadio},
Path=IsChecked}"
Value="True">
<Setter Property="TextColor" Value="Green" />
</DataTrigger>
</Label.Triggers>
</Label>
</Grid>
<Grid VerticalOptions="CenterAndExpand"
HorizontalOptions="Center">
<Image Source="{local:ImageResource RadioLabels.Images.tee200.png}"
IsVisible="{Binding Source={x:Reference smallRadio},
Path=IsChecked}" />
<Image Source="{local:ImageResource RadioLabels.Images.tee250.png}"
IsVisible="{Binding Source={x:Reference mediumRadio},
Path=IsChecked}" />
<Image Source="{local:ImageResource RadioLabels.Images.tee300.png}"
IsVisible="{Binding Source={x:Reference largeRadio},
Path=IsChecked}" />
</Grid>
</StackLayout>
</ContentPage>
单选按钮固有的另一个复杂因素涉及使用所选项目。 在某些情况下,您希望组中的每个单选按钮都由特定的枚举成员表示。 (在此示例中,此类枚举可能有三个成员,名为Small,Medium和Large。)将一组单选按钮合并到枚举值中显然需要更多代码。
RadioLabels程序避免了这些问题,只是简单地将三个RadioBehavior对象的IsChecked属性绑定到三个Image元素的IsVisible属性,这三个Image元素共享XAML文件底部的单个单元格Grid。 它们根据选择显示不同大小的位图。
这些位图的相对大小在这些屏幕截图中并不那么明显,因为每个平台都会以不同的大小显示位图:
当选择该项时,附加到每个Label的DataTrigger将TextColor从其样式颜色Gray更改为Green。
如果要在选择该项时更改每个Label的多个属性,可以向DataTrigger添加更多Setter对象。 但更好的方法是在Style中合并Setter对象,然后在DataTrigger中引用Style。
这在RadioStyle程序中得到了证明。 页面的“资源”字典定义了一个样式,其中键为“baseStyle”,用于定义未经检查的Label的外观,以及一个样式,其键为“selectedStyle”,基于“baseStyle”,但定义了已选中Label的外观。 Resources集合以Label的隐式样式结束,与“baseStyle”相同:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:RadioStyle"
xmlns:toolkit=
"clr-namespace:Xamarin.FormsBook.Toolkit;assembly=Xamarin.FormsBook.Toolkit"
x:Class="RadioStyle.RadioStylePage"
Padding="0, 50, 0, 0">
<ContentPage.Resources>
<ResourceDictionary>
<Style x:Key="baseStyle" TargetType="Label">
<Setter Property="TextColor" Value="Gray" />
<Setter Property="FontSize" Value="Small" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
<Setter Property="VerticalTextAlignment" Value="Center" />
</Style>
<Style x:Key="selectedStyle" TargetType="Label"
BasedOn="{StaticResource baseStyle}">
<Setter Property="TextColor" Value="Green" />
<Setter Property="FontSize" Value="Medium" />
<Setter Property="FontAttributes" Value="Bold,Italic" />
</Style>
<!-- Implicit style -->
<Style TargetType="Label" BasedOn="{StaticResource baseStyle}" />
</ResourceDictionary>
</ContentPage.Resources>
<StackLayout>
<Grid>
<Label Text="Small"
Grid.Column="0">
<Label.Behaviors>
<toolkit:RadioBehavior x:Name="smallRadio" />
</Label.Behaviors>
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={x:Reference smallRadio},
Path=IsChecked}"
Value="True">
<Setter Property="Style" Value="{StaticResource selectedStyle}" />
</DataTrigger>
</Label.Triggers>
</Label>
<Label Text="Medium"
Grid.Column="1">
<Label.Behaviors>
<toolkit:RadioBehavior x:Name="mediumRadio"
IsChecked="True" />
</Label.Behaviors>
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={x:Reference mediumRadio},
Path=IsChecked}"
Value="True">
<Setter Property="Style" Value="{StaticResource selectedStyle}" />
</DataTrigger>
</Label.Triggers>
</Label>
<Label Text="Large"
Grid.Column="2">
<Label.Behaviors>
<toolkit:RadioBehavior x:Name="largeRadio" />
</Label.Behaviors>
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={x:Reference largeRadio},
Path=IsChecked}"
Value="True">
<Setter Property="Style" Value="{StaticResource selectedStyle}" />
</DataTrigger>
</Label.Triggers>
</Label>
</Grid>
<Grid VerticalOptions="CenterAndExpand"
HorizontalOptions="Center">
<Image Source="{local:ImageResource RadioStyle.Images.tee200.png}"
IsVisible="{Binding Source={x:Reference smallRadio},
Path=IsChecked}" />
<Image Source="{local:ImageResource RadioStyle.Images.tee250.png}"
IsVisible="{Binding Source={x:Reference mediumRadio},
Path=IsChecked}" />
<Image Source="{local:ImageResource RadioStyle.Images.tee300.png}"
IsVisible="{Binding Source={x:Reference largeRadio},
Path=IsChecked}" />
</Grid>
</StackLayout>
</ContentPage>
在本章之前,Setter对象只能在样式定义中找到,因此在DataTrigger中看到Setter对象设置Label的Style属性可能看起来有点奇怪。 但截图显示它工作正常。 现在,除了不同的颜色外,所选项目还使用粗体和斜体的较大字体:
您也可以创建新类型的视觉效果,以识别一组单选按钮中的所选项目。 RadioImages计划包含四个位图,表明不同的运输方式。 引用这些位图的Image元素每个都是附加了RadioBehavior的ContentView的子元素,以及一个更改ContentView颜色的DataTrigger:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:RadioImages"
xmlns:toolkit=
"clr-namespace:Xamarin.FormsBook.Toolkit;assembly=Xamarin.FormsBook.Toolkit"
x:Class="RadioImages.RadioImagesPage">
<ContentPage.Resources>
<ResourceDictionary>
<Style TargetType="ContentView">
<Setter Property="WidthRequest" Value="75" />
<Setter Property="HeightRequest" Value="75" />
<Setter Property="Padding" Value="10" />
</Style>
<Color x:Key="selectedColor">#80C0FF</Color>
</ResourceDictionary>
</ContentPage.Resources>
<StackLayout HorizontalOptions="Start"
VerticalOptions="Center"
Padding="20, 0"
Spacing="0">
<ContentView>
<ContentView.Behaviors>
<toolkit:RadioBehavior x:Name="pedestrianRadio" />
</ContentView.Behaviors>
<ContentView.Triggers>
<DataTrigger TargetType="ContentView"
Binding="{Binding Source={x:Reference pedestrianRadio},
Path=IsChecked}"
Value="True">
<Setter Property="BackgroundColor" Value="{StaticResource selectedColor}" />
</DataTrigger>
</ContentView.Triggers>
<Image Source="{local:ImageResource RadioImages.Images.pedestrian.png}" />
</ContentView>
<ContentView>
<ContentView.Behaviors>
<toolkit:RadioBehavior x:Name="carRadio" />
</ContentView.Behaviors>
<ContentView.Triggers>
<DataTrigger TargetType="ContentView"
Binding="{Binding Source={x:Reference carRadio},
Path=IsChecked}"
Value="True">
<Setter Property="BackgroundColor" Value="{StaticResource selectedColor}" />
</DataTrigger>
</ContentView.Triggers>
<Image Source="{local:ImageResource RadioImages.Images.car.png}" />
</ContentView>
<ContentView>
<ContentView.Behaviors>
<toolkit:RadioBehavior x:Name="trainRadio" />
</ContentView.Behaviors>
<ContentView.Triggers>
<DataTrigger TargetType="ContentView"
Binding="{Binding Source={x:Reference trainRadio},
Path=IsChecked}"
Value="True">
<Setter Property="BackgroundColor" Value="{StaticResource selectedColor}" />
</DataTrigger>
</ContentView.Triggers>
<Image Source="{local:ImageResource RadioImages.Images.train.png}" />
</ContentView>
<ContentView>
<ContentView.Behaviors>
<toolkit:RadioBehavior x:Name="busRadio" />
</ContentView.Behaviors>
<ContentView.Triggers>
<DataTrigger TargetType="ContentView"
Binding="{Binding Source={x:Reference busRadio},
Path=IsChecked}"
Value="True">
<Setter Property="BackgroundColor" Value="{StaticResource selectedColor}" />
</DataTrigger>
</ContentView.Triggers>
<Image Source="{local:ImageResource RadioImages.Images.bus.png}" />
</ContentView>
</StackLayout>
</ContentPage>
有时,您需要通过将其中一个RadioBehavior对象的IsChecked属性设置为true来设置初始选定项目,有时不会。 该程序在程序启动时将所有程序都取消选中,但是一旦用户选择了其中一个项目,就无法全部取消选中它们。
此方案中的关键因素是ContentView被赋予了重要的Padding值,因此在选择该项时它似乎包围了Image元素:
当然,即使只有四个项目,重复标记看起来有点不祥。您可以从ContentView派生一个类来合并RadioBehavior和DataTrigger交互,但是您需要在此派生类上定义一个属性来指定与该按钮关联的特定位图,并且很可能是另一个属性或事件来指示何时选择该项目。通常,通过使用Style或其他资源定义公共属性,可以更轻松地将每个单选按钮的标记保持在最低限度。
如果您想创建更传统的单选按钮视觉效果,那也是可能的。 Unicode字符 u25CB和 u25C9类似于传统的未经检查和检查的单选按钮圆圈和点。
TraditionalRadios程序有六个单选按钮,但它们分为两组,每组三个按钮,因此需要为两个组中的至少一个设置GroupName属性。该
程序选择将所有单选按钮的GroupName设置为“platformGroup”或“languageGroup”。每个RadioBehavior都附加到一个水平StackLayout,其中包含一个Label,其中DataTrigger在“&#x25CB;”和“&#x25C9;”字符串之间切换,另一个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="TraditionalRadios.TraditionalRadiosPage">
<ContentPage.Resources>
<ResourceDictionary>
<x:String x:Key="uncheckedRadio">○</x:String>
<x:String x:Key="checkedRadio">◉</x:String>
</ResourceDictionary>
</ContentPage.Resources>
<Grid VerticalOptions="Center" Padding="5, 0">
<!-- Left column -->
<StackLayout Grid.Column="0" Spacing="24">
<!-- Header -->
<StackLayout HorizontalOptions="Start" Spacing="0">
<Label Text="Choose Platform" />
<BoxView Color="Accent" HeightRequest="1" />
</StackLayout>
<!-- Stack of radio buttons -->
<StackLayout Spacing="12">
<StackLayout Orientation="Horizontal">
<StackLayout.Behaviors>
<toolkit:RadioBehavior x:Name="iosRadio"
GroupName="platformGroup" />
</StackLayout.Behaviors>
<Label Text="{StaticResource uncheckedRadio}">
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={x:Reference iosRadio},
Path=IsChecked}"
Value="True">
<Setter Property="Text" Value="{StaticResource checkedRadio}" />
</DataTrigger>
</Label.Triggers>
</Label>
<Label Text="iOS" />
</StackLayout>
<StackLayout Orientation="Horizontal">
<StackLayout.Behaviors>
<toolkit:RadioBehavior x:Name="androidRadio"
GroupName="platformGroup" />
</StackLayout.Behaviors>
<Label Text="{StaticResource uncheckedRadio}">
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={x:Reference androidRadio},
Path=IsChecked}"
Value="True">
<Setter Property="Text" Value="{StaticResource checkedRadio}" />
</DataTrigger>
</Label.Triggers>
</Label>
<Label Text="Android" />
</StackLayout>
<StackLayout Orientation="Horizontal">
<StackLayout.Behaviors>
<toolkit:RadioBehavior x:Name="winPhoneRadio"
GroupName="platformGroup" />
</StackLayout.Behaviors>
<Label Text="{StaticResource uncheckedRadio}">
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={x:Reference winPhoneRadio},
Path=IsChecked}"
Value="True">
<Setter Property="Text" Value="{StaticResource checkedRadio}" />
</DataTrigger>
</Label.Triggers>
</Label>
<Label Text="Windows Phone" />
</StackLayout>
</StackLayout>
</StackLayout>
<!-- Left column -->
<StackLayout Grid.Column="1" Spacing="24">
<!-- Header -->
<StackLayout HorizontalOptions="Start" Spacing="0">
<Label Text="Choose Language" />
<BoxView Color="Accent" HeightRequest="1" />
Chapter 23 Triggers and behaviors 907
</StackLayout>
<!-- Stack of radio buttons -->
<StackLayout Spacing="12">
<StackLayout Orientation="Horizontal">
<StackLayout.Behaviors>
<toolkit:RadioBehavior x:Name="objectiveCRadio"
GroupName="languageGroup" />
</StackLayout.Behaviors>
<Label Text="{StaticResource uncheckedRadio}">
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={x:Reference objectiveCRadio},
Path=IsChecked}"
Value="True">
<Setter Property="Text" Value="{StaticResource checkedRadio}" />
</DataTrigger>
</Label.Triggers>
</Label>
<Label Text="Objective-C" />
</StackLayout>
<StackLayout Orientation="Horizontal">
<StackLayout.Behaviors>
<toolkit:RadioBehavior x:Name="javaRadio"
GroupName="languageGroup" />
</StackLayout.Behaviors>
<Label Text="{StaticResource uncheckedRadio}">
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={x:Reference javaRadio},
Path=IsChecked}"
Value="True">
<Setter Property="Text" Value="{StaticResource checkedRadio}" />
</DataTrigger>
</Label.Triggers>
</Label>
<Label Text="Java" />
</StackLayout>
<StackLayout Orientation="Horizontal">
<StackLayout.Behaviors>
<toolkit:RadioBehavior x:Name="cSharpRadio"
GroupName="languageGroup" />
</StackLayout.Behaviors>
<Label Text="{StaticResource uncheckedRadio}">
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={x:Reference cSharpRadio},
Path=IsChecked}"
Value="True">
<Setter Property="Text" Value="{StaticResource checkedRadio}" />
</DataTrigger>
</Label.Triggers>
</Label>
<Label Text="C♯" />
</StackLayout>
</StackLayout>
</StackLayout>
</Grid>
</ContentPage>
在现代用户界面的环境中,这些单选按钮看起来非常古怪和老式,但同时又非常真实: