Material Designer for Xamarin Forms

appscreenshot_iphone6_gold_side2 copy

There’s been a lot of talk recently about Material Design. You can find some really nice inspiring designs at materialup and dribbble. I wasn’t entirely happy with my Area Weather Forecast page design, so I wanted to experiment with the design and take some ideas and inspiration from these great designs.

I could try doing this in a design tool like Photoshop but, as a developer, I find this quite tedious and find myself wasting a lot of time wrestling with it and then have to reproduce it in Xaml. Wouldn’t it be nice if there was a real Xaml design editor for Xamarin Forms just like we have in WPF and Windows Phone? I’m sure Xamarin have this on their ‘to do’ list somewhere.

I’m not always a fan of using the designer however. It isn’t always necessary some of the time, especially if you’re just displaying a list of items or something similar. But sometimes it really does help for more complex layouts where you want to be able to experiment and tweak things. There’s nothing worse than having to continuously make changes and then run up the app every time. I find this such a waste of time.

I wondered whether it would actually be possible to use the Windows Phone designer and somehow convert the Xaml to Xamarin Forms Xaml. The Xaml is very similar so surely all that’s needed is some sort of conversion tool. I did a quick search and to my delight I discovered one already exists.

Pete Vickers of GUI Innovations has done just that here. The same thought obviously occurred to him and he’s written this wonderful conversion tool to convert Windows Phone Xaml to Xamarin Forms Xaml. This got me very excited, so I set about coming up with a workflow that would allow me to visually create and modify my designs in Xaml using the Windows Phone Xaml designer and then convert and use this in my own Xamarin Forms App.

Screen Shot 2015-05-18 at 21.20.04

The conversion tool converts things like TextBlock to Label, TextBox to Entry, StackPanel to StackLayout etc, as well as converting properties like Margin to Padding and HorizontalAlignment to HorizontalOptions, with options for how you want to convert the alignments. There is also a settings screen that allows you to extend what the tool can convert, ignore and remove. This I found very useful as I’ll discuss shortly.

Screen Shot 2015-05-18 at 21.20.17

(Just a slight rant here; I really wish there was some kind of Xaml body to govern the standards for Xaml in the same way that the W3C does for web standards. Why oh why do we have all these inconsistencies. First it was WPF v Silverlight, then there was Windows Phone, followed by Windows RT and then Windows Universal Apps, and now we have another version of Xaml for Xamarin Forms. All of which have different names for the same control with differing properties and standards. This I feel really sucks. If Xaml were better managed and standardised like HTML we wouldn’t have to resort to having to convert from one to the other. Rant over.)

The first thing you will need is a Windows Phone project running in Visual Studio. If you have set up your Xamarin Forms solution to include Windows Phone then you can just use the existing Windows Phone project in your solution. What I did was created a folder called ‘Views’ and creating a new Windows Phone page from the new items templates and called it Prototype. You should then see the Designer alongside the Xaml like this.

Screen Shot 2015-05-18 at 21.25.13

Then you can set about designing your page using either the designer or the Xaml directly. I find editing the xaml directly is far easier and quicker. The best approach is to stick to the basic controls which you know will convert well. Sticking to Grid and StackPanel for the layout works well and TextBlock and Image work good for content. It’s best just to create something very simple first and then run it through the converter to see how it looks.

Open the converter and choose your Windows Phone xaml page you’ve just created by clicking the ellipse. The tool will then automatically suggest a name for the converted file in the same location. You can change this if you like but I just kept it the same as I’m just going to copy the converted xaml and paste it in my Xamarin Forms project. Then click ‘Generate’ to convert the Xaml to Xamarin Forms. You might also want to experiment a bit with the settings to see which works out best for you. I ended up adding a bunch of extra stuff to the replace list and deleted most of the stuff from the remove list. For example I didn’t want it to remove DataContext as I wanted to define some bindings in the Xaml so I deleted this from the remove list and added a replace for DataContext to BindingContext to the replace list. I also removed Height and Width from the remove list and added Height,HeightRequest and Width,WidthRequest to the replace list. Here are my modified settings.

  • Rectangle,BoxView;
  • HorizontalTextAlignment,XAlign;
  • Height,HeightRequest;
  • Width,WidthRequest;
  • RowDefinition HeightRequest,RowDefinition Height;
  • ColumnDefinition WidthRequest,ColumnDefinition Width;
  • DataContext,BindingContext;
  • ItemsControl,views:ItemsView;

Notice the last modification that converts an ItemsControl to my custom ItemsView, complete with the namespace prefix of views. I also had to convert Row/Column Definition Height/WidthRequest back to Height/Width as the converter converts them to Height/WidthRequest and in the case of Row/Column Definitions the correct properties are actually Height/Width.

Note: I discovered there appears to be a bug where anything you add to the remove list does not seem to get saved. I tracked down the location of the file where it gets saved to in

‘C:\Program Files (x86)\GUI Innovations Limited\Windows Phone Forms to Xamarin Forms\WP to XF.exe.config’

for the default settings, and custom user settings get saved in user.config, which I is located in

‘users\[username]\AppData\Local\ GUI_Innovations_Limited\…’

The trick here is to add a section for the Remove List to this file, because there won’t be one, which you can copy from ‘WP to XF.exe.config’. Then add in your entries in the format similar to ‘TextBox,Entry’. I’ve mentioned this issue to Pete so hopefully this will be fixed soon. It’s also worth pointing out that this is a beta product at the moment and any feedback from users will greatly help its development.

Once you have got the converted Xaml, copy this to your Xamarin Forms page. What I did was create a new page called Prototype that I set as the MainPage of my Xamarin Forms Application so that I can test out the results without running up the rest of my app in full. This allows me to quickly run up my prototype design before I incorporate it into the pages within my application.

One thing to bear in mind is the variations in font sizes and the way in which widths and heights are interpreted on different platforms. This is particularly evident between Windows Phone and iOS/Android. And since we are designing everything using the Windows Phone designer we definitely need to cater for this. We can do this using Styles, which are supported by both WP and XF Xaml. The trick here is to place anything that might vary within a style.

I didn’t want to clutter my Xaml page with Styles so I placed all these in the App.xaml of the Windows Phone project. I would have preferred to place them in a completely separate xaml file but there doesn’t seem to be a way to merge this with the application styles like you can with WPF (more to investigate here). You then need the equivalent styles in your Xamarin Forms project with the exact same keys, which you should place in the App.xaml file of your XF project.

You can initially use the conversion tool to convert the styles from WP to XF, so that things like Margin become Padding and TargetTypes like StackPanel become StackLayout. However, you will need to add in variations for the various platforms using OnPlatform for anything that will vary. What I did was initially set up Styles for the various Xamarin Forms Named Font Sizes of Default, Micro, Small, Medium and Large. These vary across platforms and you can’t use these directly within the WP Xaml so you will need a Style to represent them if you intend to use them. Here’s an example of the Styles I created for these for both WP and XF.

WP Styles:

<Style x:Key="textStyle" TargetType="TextBlock">
    <Setter Property="FontFamily" Value="Segoe WP Light" />
    <Setter Property="Foreground" Value="White" />
    <Setter Property="FontSize" Value="20" />
    <Setter Property="TextWrapping" Value="Wrap"/>
</Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource textStyle}"/>

<Style x:Key="microTextStyle" TargetType="TextBlock" BasedOn="{StaticResource textStyle}">
    <Setter Property="FontSize" Value="16" />
</Style>
<Style x:Key="smallTextStyle" TargetType="TextBlock" BasedOn="{StaticResource textStyle}">
    <Setter Property="FontSize" Value="19" />
</Style>

<Style x:Key="mediumTextStyle" TargetType="TextBlock" BasedOn="{StaticResource textStyle}">
    <Setter Property="FontSize" Value="22" />
</Style>

<Style x:Key="largeTextStyle" TargetType="TextBlock" BasedOn="{StaticResource textStyle}">
    <Setter Property="FontSize" Value="32" />
</Style>
<Style x:Key="largeTextStyle1" TargetType="TextBlock" BasedOn="{StaticResource textStyle}">
    <Setter Property="FontSize" Value="48" />
</Style>
<Style x:Key="largeTextStyle2" TargetType="TextBlock" BasedOn="{StaticResource textStyle}">
    <Setter Property="FontSize" Value="56" />
</Style>
<Style x:Key="largeTextStyle3" TargetType="TextBlock" BasedOn="{StaticResource textStyle}">
    <Setter Property="FontSize" Value="100" />
</Style>

And here are the equivalent styles for Xamarin Forms with the named style names:

<Style x:Key="textStyle" TargetType="Label">
  <Setter Property="FontFamily" Value="SegoeWP-Light" />
  <Setter Property="TextColor" Value="White" />
  <Setter Property="FontSize" Value="Default" />
</Style>
<Style TargetType="Label" BasedOn="{StaticResource textStyle}" />

<Style x:Key="microTextStyle" TargetType="Label" BasedOn="{StaticResource textStyle}">
  <Setter Property="FontSize" Value="Micro" />
</Style>

<Style x:Key="smallTextStyle" TargetType="Label" BasedOn="{StaticResource textStyle}">
  <Setter Property="FontSize" Value="Small" />
</Style>

<Style x:Key="mediumTextStyle" TargetType="Label" BasedOn="{StaticResource textStyle}">
  <Setter Property="FontSize" Value="Medium" />
</Style>

<Style x:Key="largeTextStyle" TargetType="Label" BasedOn="{StaticResource textStyle}">
  <Setter Property="FontSize" Value="Large" />
</Style>
<Style x:Key="largeTextStyle1" TargetType="Label" BasedOn="{StaticResource textStyle}">
  <Setter Property="FontSize">
    <Setter.Value>
      <OnPlatform x:TypeArguments="x:Double" iOS="36" Android="36" WinPhone="48" />
    </Setter.Value>
  </Setter>
</Style>

<Style x:Key="largeTextStyle2" TargetType="Label" BasedOn="{StaticResource textStyle}">
  <Setter Property="FontSize">
    <Setter.Value>
      <OnPlatform x:TypeArguments="x:Double" iOS="42" Android="42" WinPhone="56" />
    </Setter.Value>
  </Setter>
</Style>

<Style x:Key="largeTextStyle3" TargetType="Label" BasedOn="{StaticResource textStyle}">
  <Setter Property="FontSize">
    <Setter.Value>
      <OnPlatform x:TypeArguments="x:Double" iOS="75" Android="75" WinPhone="100" />
    </Setter.Value>
  </Setter>
</Style>

Notice I have created 3 additional large text styles and defined their platform variations using OnPlatform.

There’s a little bit of work involved to define the styles initially, and you will probably end up maintaining these manually rather than using the conversion tool, but I think its good practice to place anything that varies in a separate Style. Your Xaml then becomes less tied to fixed sizes which make it more flexible. You also need to bear in mind that any Sizes for Heights, Widths or Margins will need to be roughly about 25% less on iOS and Android. If for example you define a Grid with a Height and Width of 100, it might look ok in the WP designer, but it will look much larger when you run it on the other platforms. Again factoring these out into Styles means that you catch these layout variations early and provide OnPlatform variations in a Style. This means your design is more likely to work against all platforms. Here’s a good example of a Style I created for variations in sizes for my page.

WP style

<Style x:Key="periodOuterStyle" TargetType="Grid">
    <Setter Property="Margin" Value="1" />
    <Setter Property="Width" Value="120" />
</Style>

Equivalent XF Style using OnPlatform:

<Style x:Key="periodOuterStyle" TargetType="Grid" BasedOn="{StaticResource gridStyle}">
  <Setter Property="Padding" Value="1" />
  <Setter Property ="WidthRequest">
    <Setter.Value>
      <OnPlatform x:TypeArguments="x:Double" iOS="90" Android="90" WinPhone="120" />
    </Setter.Value>
  </Setter>
</Style>

This means that your xaml shouldn’t actually have any hard coded font sizes or heights, instead referencing styles. It should just define the layout and content. This in many ways is similar to the approach used for Web pages using CSS.

Finally, my Piece De Resistance!

In WPF/WP it is possible to create design data which you can use to get a better visual representation in the designer of the data you will be working with. It also allows you to correctly define all the data bindings which will hook up to the view model of the page. I really wanted my converted Xaml to come compete with all these bindings and to be able to visually see what the result was in the actual designer. Design Data can be defined in your Xaml using the d:DataContext declaration. With this you can either define some design data as an xml file, or you can specify the actual type that the designer will create at design-time and populate with data. I was curious if it would be possible to use my existing Weather Service which my View Model requires as an injected parameter. I guess dependency injection is too much to ask really, but all I needed to do was to create a SampleData class that derives from my ViewModel with a parameterless constructor that creates my Weather Service and passes it to the base constructor like this:

    public class SampleData : AreaForecastReportViewModel
    {
        public SampleData() : base(new MountainWeatherService())
        {}
    }

And then define the d:DataContext on the root like this.

d:DataContext="{d:DesignInstance Type=designData:SampleData, IsDesignTimeCreatable=True }"

My service actually makes a call out to a real web service to return real data.

Is this really going to work in the designer at design-time??

Well initially, no it didn’t, until I discovered I small glitch in Visual Studio. It took me a while to figure this out, but if you unload and reload the Windows Phone project and then reopen the xaml it reloads the designer, creates the design data and bingo I can see the data in all its glory at design-time right in the designer.

Screen Shot 2015-05-18 at 22.06.31

This is real Data Binding at design time right within the designer. I have to say that this blew me away. And as I continue to modify my design and add more bindings to the view model the designer updates itself and shows the data as I go.

My design is now using a special custom font for the weather icons rather than individual icon images. This seems to be the way to go these days with the likes of FontAwesome for example. I managed to source the weather fonts from the same designer who created the icon images I’ve been using. This saves on image load time and allows the icon size and colour to be changed to whatever you like. Check out the Xamarin Forms Working with Fonts documentation if you are interested in taking this approach.

I have different backgrounds for each weather type so it can be challenging to get the balance right between the background images colours and the text and icon fonts. The designer is a great tool to help with this. I can easily switch the background image simply by choosing a different image in the Source property and instantly see the effect. This enabled me to play about with the opacity of overlays I am applying over some areas. This has saved me a lot of time and really helped tweak the design.

My workflow now consists of designing the layout in the WP designer; adding/modifying any styles in the WP and XF versions, converting the Xaml using the conversion tool, copying the output to my XF page and running up the App. Lets take a look at the full conversion of the Xaml from WP to XF.

<phone:PhoneApplicationPage
    x:Class="Silkweb.Mobile.MountainWeather.WinPhone.Views.Prototype"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:designData="clr-namespace:Silkweb.Mobile.MountainWeather.DesignData;assembly=Silkweb.Mobile.MountainWeather"
    xmlns:converters="clr-namespace:Silkweb.Mobile.MountainWeather.WinPhone.Converters"
    SupportedOrientations="Portrait" Orientation="Portrait"
    mc:Ignorable="d"
    shell:SystemTray.IsVisible="True"
    d:DataContext="{d:DesignInstance Type=designData:SampleData, IsDesignTimeCreatable=True }"
    Title="{Binding Title}" Margin="-1,0,1,0">

    <Grid>
        <Grid DataContext="{Binding Items[0]}">
            <Image Stretch="UniformToFill" Source="../Assets/Backgrounds/wsymbol_0011_light_snow_showers.jpeg" HorizontalAlignment="Center" />
            <Grid Background="#33000000" />
        </Grid>

        <Grid Style="{StaticResource pageStyle}">
            <Grid.Resources>
                <ResourceDictionary>
                    <DataTemplate x:Key="periodTemplate">
                        <Grid Style="{StaticResource periodOuterStyle}" VerticalAlignment="Top">
                            <Grid Background="{StaticResource periodColor}">
                                <StackPanel Style="{StaticResource periodStyle}">
                                    <TextBlock Text="{Binding Period}" Style="{StaticResource periodTextStyle}" />
                                    <TextBlock Text="{Binding WeatherCode.Glyph}"  Style="{StaticResource smallIconStyle}" />
                                    <Grid Style="{StaticResource periodDescriptionStyle}">
                                        <TextBlock Text="{Binding WeatherCode.Description}" Style="{StaticResource periodTextStyle}" />
                                    </Grid>
                                    <TextBlock Text="{Binding Probability}" Style="{StaticResource periodTextStyle}" VerticalAlignment="Bottom" />
                                </StackPanel>
                            </Grid>
                        </Grid>
                    </DataTemplate>

                    <DataTemplate x:Key="tabTemplate">
                        <Grid Style="{StaticResource tabOuterStyle}">
                            <Grid Style="{StaticResource tabStyle}">
                                <Grid Margin="10">
                                    <TextBlock Text="{Binding Date, StringFormat='\{0:ddd\}'}" Style="{StaticResource mediumTextStyle}" />
                                    <TextBlock Text="{Binding WeatherCode.Glyph}" VerticalAlignment="Center" Style="{StaticResource smallIconStyle}" />
                                </Grid>
                            </Grid>
                        </Grid>
                    </DataTemplate>
                </ResourceDictionary>
            </Grid.Resources>

            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <Grid Grid.Row="0" Style="{StaticResource detailPanelStyle}" DataContext="{Binding Items[0]}">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="*"/>
                </Grid.RowDefinitions>

                <StackPanel>
                    <TextBlock Text="{Binding Title}" Style="{StaticResource largeTextStyle2}" />
                </StackPanel>

                <Grid Grid.Row="2">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="20" />
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Row="0" Grid.ColumnSpan="2" Text="{Binding Day}" Style="{StaticResource largeTextStyle}" />
                    <TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding WeatherCode.Glyph}" Style="{StaticResource largeIconStyle}"
                               VerticalAlignment="Center" />
                    <StackPanel Grid.Row="1" Grid.Column="1">
                        <StackPanel  Orientation="Horizontal"
                                VerticalAlignment="Top" HorizontalAlignment="Center">
                            <TextBlock Text="{Binding Temperature}" Style="{StaticResource largeTextStyle3}" />
                            <StackPanel Margin="0,15,0,0">
                                <TextBlock Text="℃" Style="{StaticResource largeTextStyle1}" />
                            </StackPanel>
                        </StackPanel>

                        <TextBlock Text="{Binding WeatherCode.Description}" Style="{StaticResource mediumTextStyle2}" HorizontalAlignment="Center" />
                    </StackPanel>
                  </Grid>              

                <StackPanel Grid.Row="2" Margin="0,0,0,-10"
                    Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Bottom">
                    <TextBlock Text="." Style="{StaticResource largeTextStyle3}" />
                    <TextBlock Text="." Style="{StaticResource largeTextStyle3}" Opacity="0.5" />
                    <TextBlock Text="." Style="{StaticResource largeTextStyle3}" Opacity="0.5" />
                    <TextBlock Text="." Style="{StaticResource largeTextStyle3}" Opacity="0.5" />
                </StackPanel>
            </Grid>

            <Grid Grid.Row="1" VerticalAlignment="Bottom">
                <Grid.RowDefinitions>
                    <RowDefinition Height="*"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>

                <ScrollViewer Grid.Row="0" Style="{StaticResource scrollViewStyle}" Margin="0,0,0,30" DataContext="{Binding Items[0]}">
                    <ItemsControl ItemsSource="{Binding WeatherPeriods}" ItemTemplate="{StaticResource periodTemplate}" Style="{StaticResource itemsControlStyle}" />
                </ScrollViewer>

                <ScrollViewer Grid.Row="1" VerticalAlignment="Bottom" Style="{StaticResource tabScrollViewStyle}">
                    <ItemsControl ItemsSource="{Binding Items}" ItemTemplate="{StaticResource tabTemplate}" Style="{StaticResource itemsControlStyle}" />
                </ScrollViewer>
            </Grid>
        </Grid>
    </Grid>
</phone:PhoneApplicationPage>

And here is the Xaml converted to Xamarin Forms:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:views="clr-namespace:Silkweb.Mobile.Core.Views;assembly=Silkweb.Mobile.Core"
             x:Class="Silkweb.Mobile.MountainWeather.Views.Prototype"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:viewModels="clr-namespace:Silkweb.Mobile.MountainWeather.ViewModels;assembly=Silkweb.Mobile.MountainWeather"
             xmlns:designData="clr-namespace:Silkweb.Mobile.MountainWeather.DesignData;assembly=Silkweb.Mobile.MountainWeather"
             xmlns:sys="clr-namespace:System;assembly=System.Runtime"
             xmlns:views1="clr-namespace:Silkweb.Mobile.MountainWeather.Views;assembly=Silkweb.Mobile.MountainWeather"
             mc:Ignorable="d"
             d:DataContext="{d:DesignInstance Type=viewModels:AreaForecastReportViewModel, IsDesignTimeCreatable=False}">

  <Grid>
    <Grid BindingContext="{Binding Items[0]}">
      <Image Aspect="AspectFill" Source="{Binding BackgroundImage}" />
      <Grid BackgroundColor="#33000000" />
    </Grid>

    <Grid  Style="{StaticResource pageStyle}">
      <Grid.Resources>
        <ResourceDictionary>
          <DataTemplate x:Key="periodTemplate">
            <Grid Style="{StaticResource periodOuterStyle}" VerticalOptions="Start">
              <Grid BackgroundColor="{StaticResource periodColor}">
                <StackLayout Style="{StaticResource periodStyle}">
                  <Label Text="{Binding Period}" Style="{StaticResource periodTextStyle}" />
                  <Label Text="{Binding WeatherCode.Glyph}"  Style="{StaticResource smallIconStyle}" />
                  <Grid Style="{StaticResource periodDescriptionStyle}">
                    <Label Text="{Binding WeatherCode.Description}" Style="{StaticResource periodTextStyle}" />
                  </Grid>
                  <Label Text="{Binding Probability}" Style="{StaticResource periodTextStyle}" VerticalOptions="End" />
                </StackLayout>
              </Grid>
            </Grid>
          </DataTemplate>

          <DataTemplate x:Key="tabTemplate">
            <Grid Style="{StaticResource tabOuterStyle}">
              <Grid Style="{StaticResource tabStyle}">
                <Grid >
                  <Label Text="{Binding Date, StringFormat='\{0:ddd\}'}" Style="{StaticResource mediumTextStyle}" />
                  <Label Text="{Binding WeatherCode.Glyph}" VerticalOptions="Center" Style="{StaticResource smallIconStyle}" />
                </Grid>
              </Grid>
            </Grid>
          </DataTemplate>
        </ResourceDictionary>
      </Grid.Resources>

      <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto"/>
      </Grid.RowDefinitions>

      <Grid Grid.Row="0" Padding="5,0,5,0" Style="{StaticResource detailPanelStyle}" BindingContext="{Binding Items[0]}">
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <StackLayout>
          <Label Text="{Binding Title}" Style="{StaticResource largeTextStyle2}" />
        </StackLayout>

        <Grid Grid.Row="2">
          <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="20" />
          </Grid.RowDefinitions>
          <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
          </Grid.ColumnDefinitions>
          <Label Grid.Row="0" Grid.ColumnSpan="2" Text="{Binding Day}" Style="{StaticResource largeTextStyle}" />
          <Label Grid.Row="1" Grid.Column="0" Text="{Binding WeatherCode.Glyph}" Style="{StaticResource largeIconStyle}"
                     VerticalOptions="Center" />
          <StackLayout Grid.Row="1" Grid.Column="1">
            <StackLayout  Orientation="Horizontal"
                    VerticalOptions="Start" HorizontalOptions="Center">
              <Label Text="{Binding Temperature}" Style="{StaticResource largeTextStyle3}" />
              <StackLayout Padding="0,15,0,0">
                <Label Text="℃" Style="{StaticResource largeTextStyle1}" />
              </StackLayout>
            </StackLayout>

            <Label Text="{Binding WeatherCode.Description}" Style="{StaticResource mediumTextStyle2}"
                       HorizontalOptions="Center" />
          </StackLayout>
        </Grid>

        <StackLayout Grid.Row="2" Padding="0,0,0,-10"
            Orientation="Horizontal" HorizontalOptions="Center" VerticalOptions="End">
          <Label Text="." Style="{StaticResource largeTextStyle3}" />
          <Label Text="." Style="{StaticResource largeTextStyle3}" Opacity="0.5" />
          <Label Text="." Style="{StaticResource largeTextStyle3}" Opacity="0.5" />
          <Label Text="." Style="{StaticResource largeTextStyle3}" Opacity="0.5" />
        </StackLayout>

      </Grid>

      <Grid Grid.Row="1" VerticalOptions="End">
        <Grid.RowDefinitions>
          <RowDefinition Height="*"/>
          <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <ScrollView Grid.Row="0" Style="{StaticResource scrollViewStyle}" Padding="0,0,0,30" BindingContext="{Binding Items[0]}">
          <views:ItemsView ItemsSource="{Binding WeatherPeriods}" ItemTemplate="{StaticResource periodTemplate}" Style="{StaticResource itemsControlStyle}" />
        </ScrollView>

        <ScrollView Grid.Row="1" VerticalOptions="End" Style="{StaticResource tabScrollViewStyle}">
          <views:ItemsView ItemsSource="{Binding Items}" ItemTemplate="{StaticResource tabTemplate}" Style="{StaticResource itemsControlStyle}" />
        </ScrollView>
      </Grid>
    </Grid>
  </Grid>
</ContentPage>

As you can see the conversion is seamless with everything converted to the appropriate XF equivalent, with the exception of the background image that I hard coded in the WP version so I could test how things looked with different backgrounds. Also notice the use of the Styles in both versions, which abstracts away all of the platform variations.

Let’s see how this looks on each platform.

appscreenshot_iphone6_gold_side1

Screen Shot 2015-05-20 at 11.26.23_nexus4_angle1

Screen Shot 2015-05-20 at 09.43.30_lumia920blue_portrait

You might notice that I have a swipe carousel in the top part of the screen. This is evident by the familiar dots that indicate the number of screens you can scroll to, just like you get on iOS. I haven’t yet figured out the best solution to this (I will be exploring this in a later post), but it does really matter at this stage. I can still create the design to give this effect, by simply using a series of periods with opacity, and it is clear that there should be 3 more parts that you can swipe in to view. I can then simply switch in the Xaml for each part in the designer to see how it looks. There is more work still required on this screen but it feels to be heading in the right direction. The thing is I now have a visual designer that I can use to modify and tweak my design until I am happy with it.

I’d finally really like to thank Pete Vickers for this brilliant tool. It really has opened up a whole new world for me. I would really like to see this tool adopted more by the Xamarin Forms community and help make it something we can really benefit from. That said, will there ever be a real Xamarin Forms designer from Xamarin, who knows?

Creating an Animated Accordion Control with Xamarin Forms.

My mountain weather app now has a new page, which displays the weather forecast for specific mountain locations. I am again using the Met Office data point service to retrieve a 5 day mountain specific forecast. This provides me with 3 hourly forecast periods each containing 14 forecast elements like Temperature, Wind Speed etc. I am using a similar layout to my area forecast using my TabbedView with a tab for each of the 5 days. I am reusing my ItemsView to display the 3 hourly periods as a horizontal scrolling list. The result looks like this.

Screen Shot 2015-04-29 at 20.01.45

Notice however that I am only displaying some of the elements so it doesn’t look too crowded. What I really want to be able to do is to tap on one of the periods and have it expand a detail section to display all the elements for that period. I also want this to fill the whole width of the screen together with the panel I just tapped. I want the period panel to scroll to the side and for the detail section to slide out. This is very similar to how the BBC weather app works. As you can see below:

IMG_1191  IMG_1192  IMG_1193

Another requirement here is for the tapped panel to either slide to the left or to the right depending on where its original position is. If it happens to be close to the right of the scroll area then I want it to slide to the right and for the detail to slide out to the left. If it were to always slide to the left then it would look a little odd because it would have to scroll most to the screen width. So it is more natural for it to slide to the closest edge.

That’s quite a requirement to tackle. Sounds like I need an Accordion control, which doesn’t come with Xamarin Forms, unsurprisingly. I couldn’t find one out there so I wrote my own.

It seemed like a natural choice to reuse my ItemsView as the base class, which already provides properties for the ItemsSource and ItemTemplate. Let’s have another quick look at what ItemsView looks like. You can find more about this in my previous post here.

using System;
using System.Linq;
using Xamarin.Forms;
using System.Collections;
using System.Collections.Generic;
using System.Windows.Input;
using Silkweb.Mobile.Core.Interfaces;

namespace Silkweb.Mobile.Core.Views
{
    public class ItemsView : Grid
    {
        protected ScrollView ScrollView;
        protected readonly ICommand SelectedCommand;
        protected readonly StackLayout ItemsStackLayout;

        public ItemsView()
        {
            ScrollView = new ScrollView
            {
                Orientation = ScrollOrientation.Horizontal
            };

            ItemsStackLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                Padding = new Thickness(0),
                Spacing = 0,
                HorizontalOptions = LayoutOptions.FillAndExpand
            };

            ScrollView.Content = ItemsStackLayout;
            Children.Add(ScrollView);

            SelectedCommand = new Command<object>(item =>
            {
                var selectable = item as ISelectable;
                if (selectable == null) return;
                
                SetSelected(selectable);
                SelectedItem = selectable.IsSelected ? selectable : null;
            });

            PropertyChanged += (sender, e) =>
            {
                if (e.PropertyName == "Orientation")
                {
                    ItemsStackLayout.Orientation = ScrollView.Orientation == ScrollOrientation.Horizontal ? StackOrientation.Horizontal : StackOrientation.Vertical;
                }

            };
        }

        protected virtual void SetSelected(ISelectable selectable)
        {
            selectable.IsSelected = true;
        }

        public bool ScrollToStartOnSelected { get; set; }

        public event EventHandler SelectedItemChanged;

        public static readonly BindableProperty ItemsSourceProperty =
            BindableProperty.Create<ItemsView, IEnumerable>(p => p.ItemsSource, default(IEnumerable<object>), BindingMode.TwoWay, null, ItemsSourceChanged);

        public IEnumerable ItemsSource
        {
            get { return (IEnumerable)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        public static readonly BindableProperty SelectedItemProperty =
            BindableProperty.Create<ItemsView, object>(p => p.SelectedItem, default(object), BindingMode.TwoWay, null, OnSelectedItemChanged);

        public object SelectedItem
        {
            get { return (object)GetValue(SelectedItemProperty); }
            set { SetValue(SelectedItemProperty, value); }
        }

        public static readonly BindableProperty ItemTemplateProperty =
            BindableProperty.Create<ItemsView, DataTemplate>(p => p.ItemTemplate, default(DataTemplate));

        public DataTemplate ItemTemplate
        {
            get { return (DataTemplate)GetValue(ItemTemplateProperty); }
            set { SetValue(ItemTemplateProperty, value); }
        }

        private static void ItemsSourceChanged(BindableObject bindable, IEnumerable oldValue, IEnumerable newValue)
        {
            var itemsLayout = (ItemsView)bindable;
            itemsLayout.SetItems();
        }

        protected virtual void SetItems()
        {
            ItemsStackLayout.Children.Clear();

            if (ItemsSource == null)
                return;

            foreach (var item in ItemsSource)
                ItemsStackLayout.Children.Add(GetItemView(item));

            SelectedItem = ItemsSource.OfType<ISelectable>().FirstOrDefault(x => x.IsSelected);
        }

        protected virtual View GetItemView(object item)
        {
            var content = ItemTemplate.CreateContent();
            var view = content as View;
            if (view == null) return null;

            view.BindingContext = item;

            var gesture = new TapGestureRecognizer
            {
                Command = SelectedCommand,
                CommandParameter = item
            };

            AddGesture(view, gesture);

            return view;
        }

        protected void AddGesture(View view, TapGestureRecognizer gesture)
        {
            view.GestureRecognizers.Add(gesture);

            var layout = view as Layout<View>;

            if (layout == null)
                return;

            foreach (var child in layout.Children)
                AddGesture(child, gesture);
        }

        private static void OnSelectedItemChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var itemsView = (ItemsView)bindable;
            if (newValue == oldValue)
                return;

            var selectable = newValue as ISelectable;
            itemsView.SetSelectedItem(selectable ?? oldValue as ISelectable);
        }

        protected virtual void SetSelectedItem(ISelectable selectedItem)
        {
            var items = ItemsSource;

            foreach (var item in items.OfType<ISelectable>())
                item.IsSelected = selectedItem != null && item == selectedItem && selectedItem.IsSelected;

            var handler = SelectedItemChanged;
            if (handler != null)
                handler(this, EventArgs.Empty);
        }

    }
}

I have amended ItemView slightly so that it derives from Grid rather than ScrollView directly as before. You will see why I have done this in a moment.

My Accordion control will extend this and add a new BindableProperty for the ItemDetailTemplate, which will provide the template for the detail. I also handle the changed event for this so that I can create the content of the template when it is set. Here’s the code so far:

 
    public class Accordion : ItemsView
    {
       private View _detailView;

        public static readonly BindableProperty ItemDetailTemplateProperty =
            BindableProperty.Create<Accordion, DataTemplate>(p => p.ItemDetailTemplate, default(DataTemplate)
            , BindingMode.TwoWay, null, ItemDetailTemplateChanged);

        public DataTemplate ItemDetailTemplate
        {
            get { return (DataTemplate)GetValue(ItemDetailTemplateProperty); }
            set { SetValue(ItemDetailTemplateProperty, value); }
        }

        private static void ItemDetailTemplateChanged(BindableObject bindable, DataTemplate oldvalue, DataTemplate newvalue)
        {
            var Accordion = bindable as Accordion;
            if (Accordion == null) return;
            Accordion.CreateDetailView();
        }

        private void CreateDetailView()
        {
            var itemDetail = ItemDetailTemplate.CreateContent() as View;

            _detailView = new ScrollView
            {
                Content = itemDetail
            };
        }
    }

Notice I am wrapping the item detail in a ScrollView. This allows me to naturally scroll the item detail into view within its own scroll view. This took me ages to figure out, but as you will see shortly it works very effectively.

Next I need to handle the expanding and collapsing of the item when it is tapped. For this I need to override the SetSelectedItem method, which I have made virtual in the ItemsView. This is called when an item is tapped and its IsSelected property gets set. Note that all the items must implement ISelectable. For this to work correctly I also need to toggle the IsSelected property of the item. I do this by overriding the SetSelected method of ItemsView and toggling the value like this:

        protected override void SetSelected(ISelectable selectable)
        {
            selectable.IsSelected = !selectable.IsSelected;
        }

The code in the SetSelectItem override needs to check whether the item is selected or not and animate the collapse or expanding of the detail and position the tapped item. I found this a real challenge and spent days figuring this out. There are a number of animation extension methods available in Xamarin Forms including Animate. This allows you to supply an animation action that will get invoked as the animation runs and is passed the percentage from 0 to 1 of the animation sequence. What I needed to figure out was how to expand the detail content and scroll the item to its nearest edge both at the same time.

Firstly I also need to know the current scroll position so that I can scroll the item relative to the current scroll position. I discovered that there isn’t a property on ScrollView that tells me this. However, if I handle the Scrolled event the ScrolledEventArgs does have the scroll x and y positions, so I capture this and save it as a private member.

        private ScrolledEventArgs _scrolledEventArgs;

        public Accordion()
        {
            ScrollView.Scrolled += AccordionViewScrolled;
        }

        private void AccordionViewScrolled(object sender, ScrolledEventArgs e)
        {
            _scrolledEventArgs = e;
        } 

Now that I have this information let’s take a look at the code for the SetSelectItem method:

        protected override void SetSelectedItem(ISelectable selectedItem)
        {
            base.SetSelectedItem(selectedItem);

            var element = ItemsStackLayout.Children.FirstOrDefault(x => x.BindingContext == selectedItem);
            if (element == null) return;

            var index = ItemsStackLayout.Children.IndexOf(element);
            var scrollPosition = _scrolledEventArgs != null ? _scrolledEventArgs.ScrollX : 0;

            if (selectedItem.IsSelected)
            {
                var scrollDistance = element.X - scrollPosition; // the distance to scroll
                _lastScrollPosition = scrollPosition;

                if (Device.OS != TargetPlatform.WinPhone)
                {
                    if (ItemsStackLayout.Children.Contains(_detailView))
                        ItemsStackLayout.Children.Remove(_detailView);
                }
                else
                    CreateDetailView();

                _detailView.BindingContext = selectedItem;

                if (scrollDistance < Width / 2)
                    ItemsStackLayout.Children.Insert(index + 1, _detailView);
                else
                    ItemsStackLayout.Children.Insert(index, _detailView);

                var width = Width - element.Width; // width to expand to

                _detailView.Animate("expand",
                    x =>
                    {
                        var change = width * x;
                        _detailView.WidthRequest = change;

                        var position = scrollPosition + (scrollDistance * x);
                        ScrollView.ScrollToAsync(position, 0, false);

                    }, 0, 400, Easing.Linear, (d, b) =>
                    {
                        _overlay.IsVisible = true;
                    });
            }
            else
            {
                var width = _detailView.WidthRequest; // width to collapse
                var scrollDistance = scrollPosition - _lastScrollPosition; // the distance to scroll

                _detailView.Animate("collapse",
                    x =>
                    {
                        var change = width * x;
                        _detailView.WidthRequest = width - change;

                        var position = scrollPosition - (scrollDistance * x);
                        ScrollView.ScrollToAsync(position, 0, false);

                    }, 0, 400, Easing.Linear, (d, b) =>
                    {
                        ItemsStackLayout.Children.Remove(_detailView);
                        _detailView.Parent = null;
                        _overlay.IsVisible = false;
                    });
            }
        }

As you can see there is a lot going on here, so let’s break it down.

Firstly I get the actual visual element bound to the selected item and work out what its index is.

var element = ItemsStackLayout.Children.FirstOrDefault(x => x.BindingContext == selectedItem);
if (element == null) return;

var index = ItemsStackLayout.Children.IndexOf(element);

And I get the current scroll position if there is one (we may not have scrolled at all).

var scrollPosition = _scrolledEventArgs != null ? _scrolledEventArgs.ScrollX : 0;

Then I check if the item is selected. Let’s take a look at the selected code which expands the detail view.

Firstly I calculate the distance I need to scroll from the item position to the current scroll position. I also need to save the current scroll position so that I can scroll back to it later when I collapse the item.

var scrollDistance = element.X - scrollPosition; // the distance to scroll
_lastScrollPosition = scrollPosition;

Next I need to remove the item detail view if it already exists in the item StackLayout. This ensures that I am only reusing the item detail view once. I also discovered a strange issue with Windows Phone reusing the details view which caused it to throw an exception so I have a device specific case for this that recreates the detail view for Windows Phone.

if (Device.OS != TargetPlatform.WinPhone)
{
    if (ItemsStackLayout.Children.Contains(_detailView))
        ItemsStackLayout.Children.Remove(_detailView);
}
else
    CreateDetailView();

I then set the BindingContext of the detail view to the selectedItem and insert the detail view into the item StackLayout. This needs to be inserted after the index of the current element if we have selected an item before the centre point or before it if the selected item is at or beyond the centre point.

 
_detailView.BindingContext = selectedItem;

if (scrollDistance < Width / 2)
    ItemsStackLayout.Children.Insert(index + 1, _detailView);
else
    ItemsStackLayout.Children.Insert(index, _detailView);

Before I animate I need to calculate the width that I need to expand the detail to. This is simply the total width less the width of the selected element.

 
var width = Width - element.Width; // width to expand to

Now for the fun part. The Animate method needs to set the width of the detail view to the above width multiplied by the animation percentage passed to the Animate method. I also need to scroll the selected item by the scroll distance I calculated above by multiplying the scroll distance by the animation percentage and adding it to the current scroll position. I then call the ScrollToAsync method on the ScrollView to scroll it to this position with the animate flag set to false because I am doing the animation.

    _detailView.Animate("expand",
        percent =>
        {
            var change = width * percent;
            _detailView.WidthRequest = change;

            var position = scrollPosition + (scrollDistance * percent);
            ScrollView.ScrollToAsync(position, 0, false);

        }, 0, 400, Easing.Linear, (d, b) =>
        {
            _overlay.IsVisible = true;
        }); 

I set the animation time to 400ms and specify Linear Easing. Notice there is also another action that is the completed action. I will cover this shortly.

Now let’s take a look at the collapse code.

    var width = _detailView.WidthRequest; // width to collapse
    var scrollDistance = scrollPosition - _lastScrollPosition; // the distance to scroll

    _detailView.Animate("collapse",
        percent =>
        {
            var change = width * percent;
            _detailView.WidthRequest = width - change;

            var position = scrollPosition - (scrollDistance * percent);
            ScrollView.ScrollToAsync(position, 0, false);

        }, 0, 400, Easing.Linear, (d, b) =>
        {
            ItemsStackLayout.Children.Remove(_detailView);
            _detailView.Parent = null;
            _overlay.IsVisible = false;
        });

This time the width to collapse is just the width of the detail view. The distance to scroll is the current scroll position less the last scroll position, which we saved when the item was expanded. This is the distance I need to scroll the item back to.

The Animate method calculates the amount to reduce the width by and subtracts this from the width and sets this to the detail view WidthRequest. I then work out the amount I need to scroll using the animation percentage and call the ScrollToAsync method as before. Finally notice that the animation completed action then removes the detail view from the items StackLayout and sets its parent to null to ensure it isn’t related anymore.

Finally, notice the code which sets _overlay.IsVisible to false. When the Item is expanded I don’t want to be able to scroll the items. I want the ScrollView to be fixed until tapping it again collapses the item. I deliberated over this and tried all sorts of crazy things before I discovered a very simple solution. All I needed to do was to position a transparent control over the top of the scroll view, which stops it from interacting with any gestures. This overlay however does need to handle tap gestures so that it can collapse the item. I therefore create a ContentView as a private member with a tap genture, which I add to the Accordion with its visibility initially set to false. This is the reason why the ItemsView is now a Grid so that I can add this overlay. I guess this also allows any kind of adorner to be added. Here’s the code in the constructor.

        private readonly ContentView _overlay;

        public Accordion()
        {
            ...

            _overlay = new ContentView
            {
                IsVisible = false
            };

            AddGesture(_overlay, new TapGestureRecognizer(view =>
            {
                _overlay.IsVisible = false;
                SelectedCommand.Execute(SelectedItem);
            }));

            Children.Add(_overlay);
        }

The tap gesture sets the visibility of the overlay to false and then calls the SelectCommand to toggle the IsSelected of the item. I then set the visibility of the overlay accordingly in the animation completed call-backs of the Animate methods.

Phew! That took quite some explaining. I hope you managed to follow along. Let’s take a look at the Accordion code in full.

using System.Linq;
using Silkweb.Mobile.Core.Interfaces;
using Xamarin.Forms;

namespace Silkweb.Mobile.Core.Views
{
    public class Accordion : ItemsView
    {
        private View _detailView;
        private ScrolledEventArgs _scrolledEventArgs;
        private double _lastScrollPosition;
        private readonly ContentView _overlay;

        public Accordion()
        {
            ScrollView.Scrolled += AccordionViewScrolled;

            _overlay = new ContentView
            {
                IsVisible = false
            };

            AddGesture(_overlay, new TapGestureRecognizer(view =>
            {
                _overlay.IsVisible = false;
                SelectedCommand.Execute(SelectedItem);
            }));

            Children.Add(_overlay);
        }

        private void AccordionViewScrolled(object sender, ScrolledEventArgs e)
        {
            _scrolledEventArgs = e;
        }

        protected override void SetSelected(ISelectable selectable)
        {
            selectable.IsSelected = !selectable.IsSelected;
        }

        public static readonly BindableProperty ItemDetailTemplateProperty =
            BindableProperty.Create<Accordion, DataTemplate>(p => p.ItemDetailTemplate, default(DataTemplate)
            , BindingMode.TwoWay, null, ItemDetailTemplateChanged);

        public DataTemplate ItemDetailTemplate
        {
            get { return (DataTemplate)GetValue(ItemDetailTemplateProperty); }
            set { SetValue(ItemDetailTemplateProperty, value); }
        }

        private static void ItemDetailTemplateChanged(BindableObject bindable, DataTemplate oldvalue, DataTemplate newvalue)
        {
            var accordionView = bindable as Accordion;
            if (accordionView == null) return;
            accordionView.CreateDetailView();
        }

        private void CreateDetailView()
        {
            var itemDetail = ItemDetailTemplate.CreateContent() as View;

            _detailView = new ScrollView
            {
                Content = itemDetail
            };
        }

        protected override void SetSelectedItem(ISelectable selectedItem)
        {
            base.SetSelectedItem(selectedItem);

            var element = ItemsStackLayout.Children.FirstOrDefault(x => x.BindingContext == selectedItem);
            if (element == null) return;

            var index = ItemsStackLayout.Children.IndexOf(element);
            var scrollPosition = _scrolledEventArgs != null ? _scrolledEventArgs.ScrollX : 0;

            if (selectedItem.IsSelected)
            {
                var scrollDistance = element.X - scrollPosition; // the distance to scroll
                _lastScrollPosition = scrollPosition;

                if (Device.OS != TargetPlatform.WinPhone)
                {
                    if (ItemsStackLayout.Children.Contains(_detailView))
                        ItemsStackLayout.Children.Remove(_detailView);
                }
                else
                    CreateDetailView();

                _detailView.BindingContext = selectedItem;

                if (scrollDistance < Width / 2)
                    ItemsStackLayout.Children.Insert(index + 1, _detailView);
                else
                    ItemsStackLayout.Children.Insert(index, _detailView);

                var width = Width - element.Width; // width to expand to

                _overlay.IsVisible = true;

                _detailView.Animate("expand",
                    percent =>
                    {
                        var change = width * percent;
                        _detailView.WidthRequest = change;

                        var position = scrollPosition + (scrollDistance * percent);
                        ScrollView.ScrollToAsync(position, 0, false);

                    }, 0, 400, Easing.Linear, (d, b) =>
                    {
                        _overlay.IsVisible = true;
                    });
            }
            else
            {
                var width = _detailView.WidthRequest; // width to collapse
                var scrollDistance = scrollPosition - _lastScrollPosition; // the distance to scroll

                _detailView.Animate("collapse",
                    percentage =>
                    {
                        var change = width * percentage;
                        _detailView.WidthRequest = width - change;

                        var position = scrollPosition - (scrollDistance * percentage);
                        ScrollView.ScrollToAsync(position, 0, false);

                    }, 0, 400, Easing.Linear, (d, b) =>
                    {
                        ItemsStackLayout.Children.Remove(_detailView);
                        _detailView.Parent = null;
                        _overlay.IsVisible = false;
                    });
            }
        }
    }
}

There’s actually not a huge amount of code really for what I am doing. Now let’s use this in my app. Here’s the Xaml for my mountain site forecast view.

<ContentView ...>

  <ContentView.Resources>
    <ResourceDictionary>
      
      <DataTemplate x:Key="itemItemplate">
        <views:SiteForecastPeriodView WidthRequest="75" IsVisible="{Binding IsVisible}" />
      </DataTemplate>

      <DataTemplate x:Key="detailItemplate">
        <views:SiteForecastPeriodDetailView />
      </DataTemplate>

    </ResourceDictionary>
  </ContentView.Resources>

  <AbsoluteLayout>
    <StackLayout Spacing="0"
                 AbsoluteLayout.LayoutBounds="0.0, 1, 1, 0.95"
                 AbsoluteLayout.LayoutFlags="All">
      <StackLayout Padding="5,0,5,0">
        <Label Text="{Binding Title}" Style="{StaticResource largeHeaderStyle}" />
        <Label Text="{Binding Altitude, StringFormat='{0} meters'}" Style="{StaticResource textStyle}" />
        <Label Text="{Binding Day}" Style="{StaticResource mediumHeaderStyle}" FontAttributes="None" />
      </StackLayout>

      <coreViews:Accordion Padding="0,10,0,0" ItemsSource="{Binding Periods}"
                           ItemTemplate="{StaticResource itemItemplate}"
                           ItemDetailTemplate="{StaticResource detailItemplate}"
                           VerticalOptions="FillAndExpand"/>
    </StackLayout>
  </AbsoluteLayout>
</ContentView> 

As you can see there’s not a lot to it. I define two data templates for both the item and the item detail templates. These simply display the views I have created for each template so that it keeps this view nice and clean. I then declare an Accordion within my view and set the templates accordingly. Notice to that the ItemsSource is bound to a property called Periods on my view model. I’m not going to show you the view model as I don’t think it’s that relevant here.

Now let’s spin this up and see how it looks on all 3 platforms with these videos.



Wow! That looks really great I’m sure you’ll agree. And again this was all done without the use of any Custom Renders and is pure Xamarin Forms.

You may have noticed the use of AbsoluteLayout and the curious use of the LayoutBounds. This will be the subject of my next post.

Animating the TabbedView

It’s been a while since my last post as, amongst other things, I’ve been busy working on my mountain weather app. So I thought it was about time I shared my progress with an update on my TabbedView.

Screen Shot 2015-04-19 at 11.30.14

In part 10 of my “Creating a Xamarin Forms App” series I introduced my TabbedView, which provides a really nice customized tabbed view that allows the tabs to scroll horizontally. I wanted to improve the transitions between the views when changing tabs because I was getting a small delay when tapping on the tab, which was quite evident on Android.

The crux of the problem is the TemplateContentView, taken from Xamarin Forms Labs, which I use to bind to the selected item. As the selected item changes the BindingContext of the TemplateContentView is updated. The problem with this is the control needs to rebind the template to the updated BindingContext and this seems to cause the lag. I also wanted to have a nice fading transition between the views when switching tabs and display a background image for each tab. This clearly wasn’t going to work with just a single TemplateContentView. I needed to rethink the TabbedView.

My first thought was to have two TemplateContentViews and switch between them as the selected item changed. This is quite a common technique used for animation transitions because it allows you to fade one view out whilst fading in the other. I found this problematic however as it didn’t really fix the lag issue and the fading transitions, particularly on Android, where not smooth. After much trail and error I found a better approach using the Grid control.

The Grid control can be much more useful than you might think. Not only can you use it to layout rows and columns, it is a great control for overlaying content. You can place any number of controls in any row/column and create really great effects using opacity. You don’t even need to define any rows or columns; you can use it as a single container to overlay content. This technique is quite commonly used in WPF and works equally well with XF.

You can get a similar effect using the AbsoluteLayout control but I find this tricky to work with because you have to define the positioning of every control, relative or absolute, and the syntax for this is both very verbose and difficult to follow.

The Grid however allows us to maintain a much better flow layout and used together with other flow layouts, like StackLayout, we can produce some really nice overlay effects in a very simple way. Not only that but it provides me with a nice simple solution I was looking for.

Let’s take a look at the Xaml for my original TabbedView.

<Grid 
  xmlns="http://xamarin.com/schemas/2014/forms" 
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
  x:Class="Silkweb.Mobile.Core.Views.TabbedView"
  xmlns:cv="clr-namespace:Silkweb.Mobile.Core.Views;assembly=Silkweb.Mobile.Core">

	<Grid.RowDefinitions>
		<RowDefinition Height="*" />
		<RowDefinition Height="Auto" />
	</Grid.RowDefinitions>

	<cv:TemplateContentView Grid.Row="0" x:Name="content" />
	<cv:ItemsView Grid.Row="1" x:Name="items" />
</Grid>

Here is my updated version replacing the TemplateContentView with a Grid and introducing another Grid for the background images.

<Grid
  xmlns="http://xamarin.com/schemas/2014/forms"
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
  x:Class="Silkweb.Mobile.Core.Views.TabbedGridView"
  xmlns:cv="clr-namespace:Silkweb.Mobile.Core.Views;assembly=Silkweb.Mobile.Core">

  <Grid.RowDefinitions>
    <RowDefinition Height="*" />
    <RowDefinition Height="Auto" />
  </Grid.RowDefinitions>

  <Grid x:Name="backgroundGrid" Grid.Row="0" Grid.RowSpan="2" />
  <Grid x:Name="contentGrid" Grid.Row="0" />
  <cv:ItemsView x:Name="itemsView" Grid.Row="1" />

</Grid>

As you can see they are similar in that both have an outer Grid that defines two rows. The first being for the content and the second containing my ItemsView for the Tabs.

You can find more about ItemsView in my previous post here, but essentially it allows me to provide an ItemsSource and a DataTemplate which I can hook up to a StackLayout.

I have replaced the TemplateContentView with a Grid called contentGrid and added another Grid called backgroundGrid that will contain content for the background. Notice that this Grid has a RowSpan of 2 so that the background spans the entire control. What’s interesting is that both the contentGrid and the itemsView will be overlaid over the top of the background. Provided they have some transparency we will still see the background behind them.

Now let’s take a look at the code for this control.

    public partial class TabbedGridView : Grid
    {
        private readonly IDictionary<ISelectable, View> _views = new Dictionary<ISelectable, View>();

        public TabbedGridView()
        {
            InitializeComponent();
            itemsView.SelectedItemChanged += HandleSelectedItemViewChanged;
        }

        public static readonly BindableProperty ItemsSourceProperty =
            BindableProperty.Create<TabbedGridView, IEnumerable<ISelectable>>(p => p.ItemsSource,
                default(IEnumerable<ISelectable>),
                BindingMode.TwoWay, null, ItemsSourceChanged);

        public IEnumerable<ISelectable> ItemsSource
        {
            get { return (IEnumerable<ISelectable>)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        public static readonly BindableProperty BackgroundTemplateProperty =
            BindableProperty.Create<TabbedGridView, DataTemplate>(p => p.BackgroundTemplate, default(DataTemplate),
                BindingMode.TwoWay);

        public DataTemplate BackgroundTemplate
        {
            get { return (DataTemplate)GetValue(BackgroundTemplateProperty); }
            set { SetValue(BackgroundTemplateProperty, value); }
        }

        public static readonly BindableProperty TabTemplateProperty =
            BindableProperty.Create<TabbedGridView, DataTemplate>(p => p.TabTemplate, default(DataTemplate),
                BindingMode.TwoWay, null, TabTemplateChanged);

        public DataTemplate TabTemplate
        {
            get { return (DataTemplate)GetValue(TabTemplateProperty); }
            set { SetValue(TabTemplateProperty, value); }
        }

        public static readonly BindableProperty ItemTemplateProperty =
            BindableProperty.Create<TabbedGridView, DataTemplate>(p => p.ItemTemplate, default(DataTemplate),
                BindingMode.TwoWay);

        public DataTemplate ItemTemplate
        {
            get { return (DataTemplate)GetValue(ItemTemplateProperty); }
            set { SetValue(ItemTemplateProperty, value); }
        }

        public static readonly BindableProperty ItemTemplateSelectorProperty =
            BindableProperty.Create<TabbedGridView, TemplateSelector>(p => p.ItemTemplateSelector,
                default(TemplateSelector), BindingMode.Default);

        public TemplateSelector ItemTemplateSelector
        {
            get { return (TemplateSelector)GetValue(ItemTemplateSelectorProperty); }
            set { SetValue(ItemTemplateSelectorProperty, value); }
        }

        public static readonly BindableProperty SelectedItemProperty =
            BindableProperty.Create<TabbedGridView, ISelectable>(p => p.SelectedItem, default(ISelectable),
                BindingMode.TwoWay, null, SelectedItemChanged);

        public ISelectable SelectedItem
        {
            get { return (ISelectable)GetValue(SelectedItemProperty); }
            set { SetValue(SelectedItemProperty, value); }
        }

        private static void ItemsSourceChanged(BindableObject bindable, IEnumerable<ISelectable> oldValue,
            IEnumerable<ISelectable> newValue)
        {
            if (Equals(newValue, oldValue)) return;
            var behavior = (TabbedGridView)bindable;
            behavior.SetItemsSource(newValue);
        }

        private static void SelectedItemChanged(BindableObject bindable, ISelectable oldValue, ISelectable newValue)
        {
            if (Equals(newValue, oldValue)) return;
            var behavior = (TabbedGridView)bindable;
            behavior.AddItemView(newValue);
        }

        private static void TabTemplateChanged(BindableObject bindable, DataTemplate oldValue, DataTemplate newValue)
        {
            var view = (TabbedGridView)bindable;
            view.itemsView.ItemTemplate = newValue;
        }

        private void SetItemsSource(IEnumerable<ISelectable> itemsSource)
        {
            if (itemsSource == null) return;
            var items = itemsSource as IList<ISelectable> ?? itemsSource.ToList();

            itemsView.ItemsSource = items;

            foreach (var selectable in items)
            {
                if (BackgroundTemplate != null)
                {
                    var backgroundView = GetBackgroundView(selectable);
                    backgroundGrid.Children.Add(backgroundView);
                }

                if (Device.OS != TargetPlatform.WinPhone)
                    AddItemView(selectable);
            }
        }

        private void AddItemView(ISelectable item)
        {
            View view;
            if (!_views.TryGetValue(item, out view))
            {
                view = ItemTemplate != null
                    ? ItemTemplate.CreateContent() as View
                    : ItemTemplateSelector != null
                        ? ItemTemplateSelector.ViewFor(item)
                        : null;

                if (view == null) return;

                view.BindingContext = item;

                AddFadeBehavior(view);

                contentGrid.Children.Add(view);
                _views.Add(item, view);
            }
        }

        private View GetBackgroundView(ISelectable item)
        {
            var view = BackgroundTemplate.CreateContent() as View;
            if (view == null) return null;

            view.BindingContext = item;
            AddFadeBehavior(view);
            return view;
        }

        private void AddFadeBehavior(View view)
        {
            var behavior = new FadeBehavior();
            view.Behaviors.Add(behavior);
            behavior.SetBinding(FadeBehavior.IsSelectedProperty, "IsSelected");
        }

        private void HandleSelectedItemViewChanged(object sender, EventArgs e)
        {
            SelectedItem = itemsView.SelectedItem as ISelectable;
        }
    }

Most of the code simply defines the following BindableProperties:

ItemsSource – The list of Selectable View Models we want to bind to.
BackgroundTemplate – The DataTemplate for the background.
TabTemplate – The DataTemplate for each Tab
ItemTemplate – The DataTemplate for the item content
ItemTemplateSelector – Optional Template Selector for the item content
SelectedItem – The currently selected item.

It’s also worth noting that the view models for each item must implement the ISelectable interface for this control, which is defined as:

    public interface ISelectable
    {
        bool IsSelected { get; set; }

        ICommand SelectCommand { get; set; }
    }

I have provided value changed handers for ItemsSource, SelectedItem and TabTemplate. The most significant of these is ItemsSourceChanged, which calls SetItemsSource. This wires up the ItemsSource of the itemsView for the Tabs and then iterates through the items. For each item a background is created from the BackgroundTemplate and is added to the backgroundGrid, then the content is created and added to the contentGrid using the AddItemView method. This creates the content either from the ItemTemplate or the ItemTemplateSelector, sets the BindingContext and adds a Fade Behavior before adding the control to the contentGrid. Also notice I am caching these views in a dictionary keyed by the item, more on this in a moment. But what is this Fade Behavior?

I have created a FadeBehavior using Behavior that allows me to apply a Fade animation whenever the IsSelected property changes. Let’s take a look at this.

    public class FadeBehavior : BindableBehavior<VisualElement>
    {
        public FadeBehavior()
        {
            FadeInAnimationLength = 250;
            FadeOutAnimationLength = 350;
        }
        public static readonly BindableProperty IsSelectedProperty =
            BindableProperty.Create<FadeBehavior, bool>(p => p.IsSelected, false, BindingMode.Default, null, IsSelectedChanged);

        private static void IsSelectedChanged(BindableObject bindable, bool oldvalue, bool newvalue)
        {
            FadeBehavior behavior = bindable as FadeBehavior;
            if (behavior == null || behavior.AssociatedObject == null) return;
            behavior.Animate();
        }

        private void Animate()
        {
            if (IsSelected)
                AssociatedObject.IsVisible = true;

            AssociatedObject.FadeTo(
                IsSelected ? 1 : 0, 
                IsSelected ? FadeInAnimationLength : FadeOutAnimationLength, 
                Easing.Linear)
                .ContinueWith(x =>
                {
                    if (!IsSelected)
                        AssociatedObject.IsVisible = false;
                }, TaskScheduler.FromCurrentSynchronizationContext());
        }

        public bool IsSelected
        {
            get { return (bool)GetValue(IsSelectedProperty); }
            set { SetValue(IsSelectedProperty, value); }
        }

        public uint FadeInAnimationLength { get; set; }

        public uint FadeOutAnimationLength { get; set; }

        protected override void OnAttachedTo(VisualElement visualElement)
        {
            base.OnAttachedTo(visualElement);
            visualElement.Opacity = 0;
            visualElement.IsVisible = false;
        }
    }

This defines an IsSelected BindableProperty with a change handler that applies a FadeTo animation when this property changes. FadeTo is part of a number of animation extension methods available with XF. Notice also that I am initialising the attached view with an opacity of 0 and setting it’s IsVisible property to false in the OnAttachedTo override. In the change handler IsVisible is set to true when IsSelected is true. The FadeTo animation is then applied. This either animates the Opacity to 1 or 0 depending on the newValue of IsSelected. Effectively fading in if selected or fading out if unselected. I have also provide properties to set the FadeIn and FadeOut Animation Length which I set defaults for. Interestingly FadeTo, along with the other animation extension methods, returns a Task. This means that we can provide a continuation when the animation is complete. In this case I want to set the IsVisible property to false when IsSelected equals false. But why bother setting the IsVisible property at all if we are already setting Opacity? The reason for this is two fold. It means that the view won’t get rendered in the visual tree but it will still get bound to it’s BindingContext. This provides a much smoother animation transition. And secondly it means that overlaid controls won’t obscure any gestures for the currently selected view. I discovered this to be true on both Android and Windows Phone where tap gestures didn’t work property even though an overlay had zero opacity. So better to ensure that any views which are not selected are not visible once the animation sequence has completed.

Back in the TabbedGridView I apply theFadeBahavior as follows:

    private void AddFadeBehavior(View view)
    {
        var behavior = new FadeBehavior();
        view.Behaviors.Add(behavior);
        behavior.SetBinding(FadeBehavior.IsSelectedProperty, "IsSelected");
    }

Notice here that I am binding the IsSelected property of the behavior to the IsSelected property of the view it is attached to. I apply the FadeBehavior to both the item content and also to the background content before they are added to the Grids. This means that initially these controls will have IsVisible set to false and have opacity set to zero, and both will Invoke the FadeTo animation when IsSelected changes.

Notice that in SetItemsSource I have applied a platform tweak for WinPhone. This does not add the item content for each item when the Item Source is set. I was getting some strange behavior doing this with Windows Phone. It caused no content to be displayed at all even when the FadeBehavior made the selected item visible. As an alternative the SelectedItemChanged handler also calls AddItemView which checks to see if the view has already been added by checking the views dictionary. First time the item is selected it will get created and added to the content grid. I could just do this for the other platforms but I get a smoother transition if I preload the views. I may take a further look at this later but for now this WinPhone platform tweak works ok and highlights how we can include these platform optimizations in our code.

Now all I need to do is replace my previous TabbedView with my new TabbedGridView. Here is the Xaml for my AreaForecastReportView in my Mountain Weather App.

<?xml version="1.0" encoding="utf-8"?>
<ContentPage
	xmlns="http://xamarin.com/schemas/2014/forms"
	xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
	x:Class="Silkweb.Mobile.MountainWeather.Views.AreaForecastReportView"
	xmlns:cv="clr-namespace:Silkweb.Mobile.Core.Views;assembly=Silkweb.Mobile.Core"
	xmlns:ex="clr-namespace:Silkweb.Mobile.Core.Extensions;assembly=Silkweb.Mobile.Core"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:viewModels="clr-namespace:Silkweb.Mobile.MountainWeather.ViewModels;assembly=Silkweb.Mobile.MountainWeather"
  mc:Ignorable="d"
  d:DataContext="{d:DesignInstance Type=viewModels:AreaForecastReportViewModel}"
	Title="{Binding Title}" BackgroundColor="{StaticResource backgroundColor}">
  <ContentPage.Resources>
    <ResourceDictionary>
      
      <Style x:Key="tabGridStyle" TargetType="Grid">
        <Setter Property="BackgroundColor" Value="Transparent" />
        <Style.Triggers>
          <DataTrigger Binding="{Binding IsSelected}" TargetType="Grid" Value="False">
            <Setter Property="BackgroundColor" Value="{StaticResource highlightColor}" />
          </DataTrigger>
        </Style.Triggers>
      </Style>

      <DataTemplate x:Key="tabTemplate">
        <Grid WidthRequest="100" HeightRequest="100" Padding="1"
              d:DataContext="{d:DesignInstance Type=viewModels:WeatherDayViewModel}">
          <Grid Style="{StaticResource tabGridStyle}">
            <StackLayout Padding="5" Spacing="0">
              <Label Text="{Binding Date, StringFormat='{0:ddd}'}" Font="Mirco" VerticalOptions="Start" HorizontalOptions="Start"
                     TextColor="{ex:ApplicationResource navigationBarTextColor}" />
              <Label Text="{Binding Date, Converter={StaticResource dateTimeConverter}, ConverterParameter='d{0} MMM'}" Font="Mirco"
                     VerticalOptions="Start" HorizontalOptions="Start" TextColor="{ex:ApplicationResource navigationBarTextColor}" />
              <Image Source="{Binding Icon}" VerticalOptions="Start" HorizontalOptions="Center" WidthRequest="48" HeightRequest="48" />
            </StackLayout>
          </Grid>
        </Grid>
      </DataTemplate>

      <DataTemplate x:Key="itemTemplate">
        <cv:ViewLocatorControl />
      </DataTemplate>

      <DataTemplate x:Key="backgroundImageTemplate">
        <Grid d:DataContext="{d:DesignInstance Type=viewModels:WeatherDayViewModel}">
          <Image Source="{Binding BackgroundImage}" Aspect="AspectFill" />
          <BoxView BackgroundColor="{Binding Tint, Converter={StaticResource stringToColorConverter}}" />
        </Grid>
      </DataTemplate>

    </ResourceDictionary>
  </ContentPage.Resources>

  <ContentPage.ToolbarItems>
    <ToolbarItem Name="Hazards" Command="{Binding ShowHazardsCommand}" />
  </ContentPage.ToolbarItems>

  <Grid>
    <cv:TabbedGridView x:Name="tabbedGridView" ItemsSource="{Binding Items}"
                       TabTemplate="{StaticResource tabTemplate}" ItemTemplate="{StaticResource itemTemplate}"
                       BackgroundTemplate="{StaticResource backgroundImageTemplate}" />

    <ActivityIndicator IsRunning="{Binding IsBusy}" HorizontalOptions="Center" VerticalOptions="Center" />
    <cv:GradientBoxView StartColor="{StaticResource highlightColor}" EndColor="Transparent" HeightRequest="75" VerticalOptions="Start" />
  </Grid>
</ContentPage>

I have defined 3 Data Template’s for the TabTemplate, ItemTemplate and BackgroundTemplate in the resources. The content defines the TabbedGridView and sets the Templates accordingly. I also bind the ItemsSource to an Items property in my view model.

Note the use of d:DataContext which gives me design time intellisense support for my bindings. More on this in my previous post here.

The TabTemplate is the same as the previous version. The new Background Template defines a grid containing an Image bound to the BackgroundImage property on my ViewModel and a BoxView with a BackgroundColor with a Tint colour which is bound to a Tint property on my view model. This allows me to apply differing Tints to the background. This is a popular technique that darkens the image so that white text overlaid over the background is easier to read.

The ItemTemplate is of particular interest here. Let’s take a closer look at this.

      <DataTemplate x:Key="itemTemplate">
        <cv:ViewLocatorControl />
      </DataTemplate>

This contains just one control called ViewLocatorControl. This clever little control is responsible for resolving the correct view that has been registered with my ViewFactory from the bound view model. If you are not familiar with my view factory then it’s worth taking a look at my previous post on View Model First Navigation.

Let’s have a quick look at the code for the ViewLocatorControl.

    public class ViewLocatorControl : ContentView
    {
        private readonly IViewFactory _viewFactory;

        public ViewLocatorControl()
        {
            _viewFactory = ViewFactory.Instance;
            BindingContextChanged += OnBindingContextChanged;
        }

        private void OnBindingContextChanged(object sender, EventArgs eventArgs)
        {
            View view = _viewFactory.Resolve(BindingContext as IViewModel) as View;
            Content = view;

        }
    }

As you can see all this does is use the ViewFactory to resolve the view when the BindingContext changes and sets the content to the resolved view.

This required a slight modification to the ViewFactory to register and resolve a VisualElement instead of Page. This allow me to register and resolve both Views and Pages, which is much more flexible. In this case there are two different views registered with two different view models. This are registered like this.

    viewFactory.Register<AreaForecastViewModel, AreaForecastView>();
    viewFactory.Register<OutlookViewModel, OutlookView>();

This is really a nice alternative to defining a Template Selector using the TemplateSelector class from Xamarin Forms Labs which I was using previously.

The resolved view will then be overlaid over the background image to produce a really nice effect. The views also add some additional opacity effects over the content to produce a further effect.

Now lets see it in action with the following videos showing the animation transition effects as I switch tabs on all 3 platforms.

I think the result looks really nice and shows what is possible using Grid overlays and just a few simple controls.

I really like the results and hope you do to. This post shows some really nice things that are possible with Xamarin Forms without resorting to Custom Renderers.