背景
我的客户希望能够在Xamarin.Forms的Secondary Toolbar中达到如下效果:
一开始的时候我认为这可能是Xamarin本身的一个限制,因为我一直认为安卓原生态的应用一定是可以直接实现的。
但是当我深入研究之后,我发现之所以Xamarin.Forms不支持这样做其实是因为安卓原生态也并不支持。
在原生态安卓或者Xamarin.Android下Secondary Toolbar是怎么工作的?
其实在native的安卓开发中,并没有Secondary Toolbar
这个术语,这仅仅存在于Xamarin.Forms中。
在原生态的安卓中,我们称所有的ToolbarItem为MenuItem
。以下是Xamarin.Android下封装了安卓原生态的MenuItem的原型:
1 | public interface IMenuItem : IJavaObject, IDisposable |
我们是通过SetShowAsAction
来确定这个MenuItem是否作为primary的一个ToolbarItem显示在Toolbar中。
这个设置一共有3个值:
always
: 确保这个MenuItem会被显示在Toolbar中。ifRoom
: 先判断Toolbar中是否还有足够的空间放置这个MenuItem,如果有,则放置,如果没有,则会被分配到overflow area,也就是我们的Secondary Toolbar中。never
: 不管有没有空间,都将这个MenuItem放在overflow area.
下图表示上面的逻辑:
下图则表示了图标(icon),标题(title)以及overflow area是如何被使用的:
- 如果MenuItem是显示在Toolbar中的,那么在Toolbar中只会有图标,标题是作为该图标的一个提示文字。
- 如果MenuItem在overflow area中,那么只有标题会作为文字被显示在这个区域。
在原生态安卓下如何实现可以显示图标的Secondary Toolbar呢?
关于这点我并没有做过任何的测试,但是基于我的研究,有一些其他解决方法,比如我们使用SubMenu来达到效果。原理是,如果一个Menu有一个关联的SubMenu,那么当点击Menu的时候,会显示这个Menu的SubMenu,这个时候的SubMenu是支持icon和文字的。之后我们使用一个三个点的图片作为一个primary的menu的显示,所以这样的实现中,我们并没有overflow area,也就是没有用到Secondary Toolbar的概念。
如何在Xamarin.Forms中实现同样的效果?
为什么不能使用custom renderer?
当时我解决这个问题的第一个思路便是重写这个控件的render的方法,来让它渲染的时候是可以带上icon的。 但是最后发现这条路是行不通的,原因是Xamarin并没有暴露给我们这个所谓的ToolbarItem
的控件让我们可以重写它的渲染方式。
参考链接: Renderer Base Classes and Native Controls
解决方法
我基本上会采取和原生态安卓一样的方法,放弃使用Secondary Toolbar,而是使用一个看似一样的三个点的图标来作为primary的ToolbarItem来达到Seondary Toolbar的显示效果。之后,我会给这个图标绑定一个命令(Command)来控制一个ListView
的IsVisible
的属性。这个ListView就是一个横向的StackLayout
,其中的元素就是一张图和一个文字并排。最后,我会使用RelativeLayout
来将这个ListView放在距离屏幕右侧一定距离的地方。
这个解决方案的好处
通常情况下,使用Xamarin的用户都希望跨平台能够看到的效果一模一样。但是,Xamarin.Android和Xamarin.iOS的Secondary Toolbar的显示效果完全不同。在iOS中,Secondary Toolbar就是在Primary Toolbar下面再加一行Toolbar而已。
所以这个解决方案的一个好处就是由于我们没有使用Secondary Toolbar,所以它和Android看上去的效果理论上会是一样的
实现
我仍然尝试使用MVVM的结构来在Xamarin.Forms中实现相应的效果。
第一步就是定义ToolbarItem的模型,这一步非常直观,因为我们的ToolbarItem就是一张图片加一行文字作并排。在Xamarin.Forms中,我们是使用图片的路径来指定一个图片的source,所以在这个模型中我们只需要定义图片的路径字符串就可以。
我们建立一个文件夹并且取名为Models,然后在里面建立一个类文件,取名为
ToolbarItemMode.cs
.代码如下:
1
2
3
4
5
6public class ToolbarItemModel
{
public string ImagePath { get; set; }
public string MenuText { get; set; }
}现在我们使用
ContentView
来建立一个自定义的控件。ContentView和ContentPage的不同就是,它不是一个页面,所以它可以定义一些元素,然后被嵌入在其他页面中,通常是为了达到一个复用的效果。我们来建立一个文件夹,取名为 CustomControls, 然后从VS的template中建立一个ContentView,取名为 CustomToolbarItem. 这样就会有两个关联的文件被建立出来,分别是页面文件
CustomToolbarItem.xaml
以及后台代码文件CustomToolbarItem.xaml.cs
.首先我们先在后台代码文件中定义
BindablePropery
,里面是用来绑定MenuText和ImagePath的代码,这样我们页面上的元素就可以和后台的数据类型做一个绑定了。其中的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56// This class is genereated from template actually
[ ]
public partial class CustomToolbarItem : ContentView
{
// Bindable Property definition for MenuText
public static readonly BindableProperty MenuTextProperty = BindableProperty.Create(
propertyName: "MenuText",
returnType: typeof(string),
declaringType: typeof(CustomToolbarItem),
defaultValue: "",
defaultBindingMode: BindingMode.OneWay,
propertyChanged: TextPropertyChanged);
// This property name should be the same as the MenuTextProperty's propertyName field
public string MenuText
{
get { return base.GetValue(MenuTextProperty).ToString(); }
set { base.SetValue(MenuTextProperty, value); }
}
// This is the TextPropertyChanged event handler for the MenuTextProperty
private static void TextPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (CustomToolbarItem)bindable;
control.menuText.Text = newValue.ToString();
}
// Below is for IconImage, it's the same concept with MenuText
// They all have 3 steps: BindableProperty, public property, PropertyChanged event handler
public static readonly BindableProperty IconImageProperty = BindableProperty.Create(
propertyName: "IconImage",
returnType: typeof(string),
declaringType: typeof(CustomToolbarItem),
defaultValue: "",
defaultBindingMode: BindingMode.OneWay,
propertyChanged: ImageSourcePropertyChanged);
public string IconImage
{
get { return base.GetValue(IconImageProperty).ToString(); }
set { base.SetValue(IconImageProperty, value); }
}
private static void ImageSourcePropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (CustomToolbarItem)bindable;
// Notice here we are updating the ImageSource of the control's image property
control.iconImage.Source = ImageSource.FromFile(newValue.ToString());
}
// Below is the constructor of the ContentView, nothing special
public CustomToolbarItem ()
{
InitializeComponent();
}
}然后我们回到页面文件来定义页面元素。下面就是一个比较正统的
StackLayout
来排列一张图片和一个文字元素。将下面的代码放入ContentView的Content中:
1
2
3
4
5
6<ContentView.Content>
<StackLayout Orientation="Horizontal" Spacing="10" Padding="5,5,5,5">
<Image x:Name="iconImage" HeightRequest="30" HorizontalOptions="Start" VerticalOptions="Center" />
<Label x:Name="menuText" FontSize="Medium" VerticalOptions="Center" HorizontalOptions="Start" />
</StackLayout>
</ContentView.Content>现在自定义控件的部分其实我们已经做完了。
剩下的是将这个自定义控件使用到我们的页面中去。 在MainPage.xaml这个页面中,我们将添加
ContentPage.ToolbarItems
以及RelativeLayout
,代码如下:记得添加
xmlns:cxcontrols="clr-namespace:AllenCustomSecondaryToolbar.CustomControls"
到MainPage的Content的header中,不然无法使用该控件。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<ContentPage.ToolbarItems>
<!-- Other primary toolbaritems -->
<ToolbarItem Text="More" Icon="menu3dots.png" Priority="1" Order="Primary" x:Name="cmdToolbarItem" />
</ContentPage.ToolbarItems>
<RelativeLayout>
<ListView x:Name="SecondaryToolbarListView" VerticalOptions="Start" HorizontalOptions="Start" WidthRequest="150" IsVisible="False"
RelativeLayout.XConstraint="{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=1, Constant=-160}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<cxcontrols:CustomToolbarItem IconImage="{Binding ImagePath}" MenuText="{Binding MenuText}" BackgroundColor="Default" />
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Label Text="This is a really really really really really really long label" />
</RelativeLayout>注意:在上面的
ListView
中设置的WidthRequest
是基于我在自定义的ToolbarItem控件中的字体大小和高度来制定的。如果你在自定义控件中修改了你想要的字体大小和高度,那这边也需要做一个相应的改动。同样的,上面代码中,ListView中应用的关于
RelativeLayout
中的Constant
的值设定是根据这个ListView的WidthRequest
来制定的。比如上面我们的宽度需求是150个单元,那么我就制定这个Constant为-160来达到使这个ListView距离手机屏幕右边距10个单元的效果。接下来,我们去MainPage的后台代码文件,并且添加如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public MainPage()
{
InitializeComponenet();
var items = new List<ToolbarItemModel>
{
new ToolbarItemModel {ImagePath = "xxxx.png", MenuText = "First Menu Item"},
new ToolbarItemModel {ImagePath = "xxxxx.png", MenuText = "Second Menu Item"}
}
SecondaryToolbarListView.ItemsSource = items;
cmdToolbarItem.Command = new Command(SecondaryToolbarCmd);
}
void SecondaryToolbarCmd()
{
SecondaryToolbarListView.IsVisible = !SecondaryToolbarListView.IsVisible;
}注意:记得将图片文件添加到安卓和iOS的项目中去,这些文件并不是添加在Xamarin.Forms的跨平台项目中的。
在安卓下,添加到
\Resources\drawable
.
在iOS下, 添加到\Resources
.
现在这个demo基本写完了,运行效果如下:
未完的工作
从上面的截图可以看出,我们需要给这个控件添加一些符合主题的背景色等等,这样才能使这个Secondary Toolbar可以遮掉这个Label中的文字,否则的话这些内容会重叠在一起。
评论