如何在Xamarin.Forms的Secondary ToolbarItem中可以显示图标

背景

我的客户希望能够在Xamarin.Forms的Secondary Toolbar中达到如下效果:

一开始的时候我认为这可能是Xamarin本身的一个限制,因为我一直认为安卓原生态的应用一定是可以直接实现的。

但是当我深入研究之后,我发现之所以Xamarin.Forms不支持这样做其实是因为安卓原生态也并不支持。

在原生态安卓或者Xamarin.Android下Secondary Toolbar是怎么工作的?

其实在native的安卓开发中,并没有Secondary Toolbar这个术语,这仅仅存在于Xamarin.Forms中。

在原生态的安卓中,我们称所有的ToolbarItem为MenuItem。以下是Xamarin.Android下封装了安卓原生态的MenuItem的原型:

1
2
3
4
5
6
7
8
9
public interface IMenuItem : IJavaObject, IDisposable
{
// ...
IMenuItem SetTitle(int title); // Text
IMenuItem SetIcon(Drawable icon); // Image
void SetShowAsAction(ShowAsAction actionEnum); // Positioning
ISubMenu SubMenu { get; } // Hierarchy
IMenuItem SetChecked(bool value); // Checked State
}

我们是通过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)来控制一个ListViewIsVisible的属性。这个ListView就是一个横向的StackLayout,其中的元素就是一张图和一个文字并排。最后,我会使用RelativeLayout来将这个ListView放在距离屏幕右侧一定距离的地方。

这个解决方案的好处

通常情况下,使用Xamarin的用户都希望跨平台能够看到的效果一模一样。但是,Xamarin.Android和Xamarin.iOS的Secondary Toolbar的显示效果完全不同。在iOS中,Secondary Toolbar就是在Primary Toolbar下面再加一行Toolbar而已。

所以这个解决方案的一个好处就是由于我们没有使用Secondary Toolbar,所以它和Android看上去的效果理论上会是一样的

实现

我仍然尝试使用MVVM的结构来在Xamarin.Forms中实现相应的效果。

  1. 第一步就是定义ToolbarItem的模型,这一步非常直观,因为我们的ToolbarItem就是一张图片加一行文字作并排。在Xamarin.Forms中,我们是使用图片的路径来指定一个图片的source,所以在这个模型中我们只需要定义图片的路径字符串就可以。

    我们建立一个文件夹并且取名为Models,然后在里面建立一个类文件,取名为ToolbarItemMode.cs.

    代码如下:

    1
    2
    3
    4
    5
    6
    public class ToolbarItemModel
    {
    public string ImagePath { get; set; }

    public string MenuText { get; set; }
    }
  2. 现在我们使用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
    [XamlCompilation(XamlCompilationOptions.Compile)]
    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>

    现在自定义控件的部分其实我们已经做完了。

  1. 剩下的是将这个自定义控件使用到我们的页面中去。 在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
    19
    public 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中的文字,否则的话这些内容会重叠在一起。

Xamarin.Forms使用Azure Mobile Apps SDK的offline sync功能时的痛点介绍 使用.NET SDK来运行PowerShell的命令
You need to set install_url to use ShareThis. Please set it in _config.yml.

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×