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.