25 April 2016

Behavior for view model driven animated popups in Xamarin Forms

Preface

popupIn my previous post I showed the basics for animation behaviors in Xamarin Forms. Here’s another one I made, that in conjunction with some nifty element binding makes for a pretty fluid popup appearing from the middle. Of course, you can use the native alert box but I’d rather want to see is something as displayed to the right, especially with a nice animation. The advantage of such a custom made popup is that is looks much more consistent across platforms, which is a huge win especially for LOB apps.

You can see the behavior in action below:

... and all there is to it...

The actual code is pretty small now all the heavy lifting has been done by the base classes from the previous post:

using Wortell.XamarinForms.Behaviors.Base;
using Xamarin.Forms;

namespace Wortell.XamarinForms.Behaviors
{
  public class AnimateScaleBehavior : AnimateFoldBehaviorBase
  {
    protected override void Init(bool newValue)
    {
      if (newValue)
      {
        FoldOutPosition = 1;
        FoldInPosition = 0;
        AssociatedObject.Scale = FoldInPosition;
      }
    }

    protected override void ExecuteAnimation(double start, double end, 
                                             uint runningTime)
    {
      var animation = new Animation(
        d => AssociatedObject.Scale = d, start, end, Easing.SinOut);

      animation.Commit(AssociatedObject, "Unfold", length: runningTime,
        finished: (d, b) =>
        {
          if (AssociatedObject.Scale.Equals(FoldInPosition))
          {
            AssociatedObject.IsVisible = false;
          }
        });
    }
  }
}

In stead of animating TranslationX and TranslationY, like in the previous post, now the Scale property is animated from 0 to 1 and back. Attentive readers may have seen something else though - as soon as the animation starts, a grey haze appears over the underlying screen, that only disappears when the animation is done. This is entirely done in XAML - using element binding.

The whole popup sits in MenuControl.xaml – this is a user control

<?xml version="1.0" encoding="utf-8" ?>
<Grid xmlns="http://xamarin.com/schemas/2014/forms"
      //Rest of namespace stuff omitted
      IsVisible="{Binding IsVisible, Source={x:Reference AnimatedGrid}}">
  <ContentView BackgroundColor="{StaticResource SeeThrough}"
               HorizontalOptions="Fill" VerticalOptions="Fill" >

    <ContentView Padding="20,0,20,0" x:Name="AnimatedGrid" IsVisible="False">
      <ContentView.Behaviors>
        <behaviors:AnimateScaleBehavior IsVisible="{Binding IsMenuVisible}" 
           ViewIsInitialized="{Binding ViewIsInitialized}"/>
      </ContentView.Behaviors>
      <ContentView Style="{StaticResource MenuStyle}" HorizontalOptions="Fill"
                    VerticalOptions="CenterAndExpand" Padding="0,0,0,10">
        <Grid>
          <Grid.RowDefinitions>
            <RowDefinition Height="40"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
          </Grid.RowDefinitions>

          <ContentView  Grid.Row="0" Style="{StaticResource PopupHeaderStyle}">
            <Label Style="{StaticResource PopupHeaderTextStyle}" Text="Popup header" 
                VerticalOptions="Center"/>
          </ContentView>

          <StackLayout Orientation="Vertical" Grid.Row="1">

            <StackLayout Style="{StaticResource ContentStyle}"  Orientation="Vertical">
              <Label Style="{StaticResource MenuTextStyle}" Text="Here be text" />  
              <Label Style="{StaticResource MenuTextStyle}" Text="Here be more text"
                  VerticalOptions="Center"/>
            </StackLayout>

            <ContentView Style="{StaticResource Separator}"></ContentView>

            <StackLayout Style="{StaticResource ContentStyle}"  Orientation="Vertical">
              <Label Style="{StaticResource MenuTextStyle}" Text="Here be text" />
              <Label Style="{StaticResource MenuTextStyle}" Text="Here be more text" />
              <Label Style="{StaticResource MenuTextStyle}" 
                  Text="Here be more whatever UI elements you want" />
            </StackLayout>

            <Grid HeightRequest="10"></Grid>

            <Grid Style="{StaticResource ContentStyle}">
              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"></ColumnDefinition>
                <ColumnDefinition Width="10"></ColumnDefinition>
                <ColumnDefinition Width="*"></ColumnDefinition>
              </Grid.ColumnDefinitions>
                 <Button Text="Ok" Command="{Binding CloseMenuCommand}"></Button>
              <Button Text="Cancel" Grid.Column="2" 
                  Command="{Binding CloseMenuCommand}"></Button>
            </Grid>
          </StackLayout>
        </Grid>
      </ContentView>
    </ContentView>

    <ContentView.GestureRecognizers>
      <TapGestureRecognizer Command="{Binding CloseMenuCommand}"></TapGestureRecognizer>
    </ContentView.GestureRecognizers>
  </ContentView>

</Grid>

I have marked the interesting parts in red and bold. On top you see that the visibility of the control itself is bound to the visibility of the grid called “AnimatedGrid”. This grid is inside a ContentView which has a background color defined by the resource “SeeTrough. This you can find in app.xaml defined as being color #B2000000, aka "70% black" in designer lingo (i.e. black that is 30% transparent). AnimatedGrid is the top level element of the actual popup – it’s visibility is controlled by the AnimateScaleBehavior. As soon as the animation starts by IsVisible flipping value, the AnimatedGrid is first made visible, and it’s parent – and the SeeThrough ContentView – popup into view, only to disappear when the animation is completely finished. This is exactly what I wanted to achieve – the 70% haze turns the focus of the user to the little task at hand now, and also blocks tapping access to whatever UI elements are still visible below it.

Note, there no values supplied for FoldInTime and FoldOutTime so the default values of 150 and 250ms are used. But you can easily change that by adding them to the behavior in XAML.

As a final thing, I have added a TapGestureRecognizer that calls the popup closing command to the 70% gray area as well. This dismisses the popup when you tap outside it, which is exactly what one expects in a mobile app. It’s effectively the same thing as hitting Cancel (and in this case, Ok, as that does nothing but closing the popup as well).

Viewmodel

Hardly worth mentioning, but to make the picture complete:

using Xamarin.Forms;

namespace XamarinFormsDemos.ViewModels
{
  public class PopupViewModel : MenuViewModelBase
  {
    public Command CloseMenuCommand { get; private set; }

    public PopupViewModel() : base()
    {
      CloseMenuCommand = new Command(() => IsMenuVisible = false);
    }
  }
}
Command makes boolean false. So menu disappears again. Doh ;)

Usage

<?xml version="1.0" encoding="utf-8" ?>
<demoViewFramework:BaseContentPage xmlns="http://xamarin.com/schemas/2014/forms"
    // stuff omitted
  <Grid>
    <!-- Page content -->
    <Grid.RowDefinitions>
      <RowDefinition Height="25*"></RowDefinition>
      <RowDefinition Height="75*"></RowDefinition>
    </Grid.RowDefinitions>
    <Grid Row="0" Style="{StaticResource HeaderOutsideStyle}">
      <ContentView Style="{StaticResource ContentStyle}">
        <Label Text="Popup menu" Style="{StaticResource HeaderStyle}"  
        VerticalOptions="CenterAndExpand"></Label>
      </ContentView>
    </Grid>

    <ContentView Grid.Row="1" Style="{StaticResource ContentStyle}" 
        HorizontalOptions="Fill">
      <Label Text="Here be page content" 
        Style="{StaticResource ContentTextStyle}"></Label>

      <ContentView  VerticalOptions="Start" Style="{StaticResource ContentStyle}" >
          <Button Text="Open popup menu" Command="{Binding ToggleMenuCommand}" 
                  HorizontalOptions="Fill" VerticalOptions="Start"></Button>
      </ContentView>
    </ContentView>

    <!-- Menu -->
    <controls:PopupControl Grid.Row="0" Grid.RowSpan="2"></controls:PopupControl>

  </Grid>
</demoViewFramework:BaseContentPage;/Grid>

Once again, the control is added last and convers the whole screen, as to appear over the existing UI.

Some final words

When you are developing cross-platform apps, UI testing is crucial. For instance, while I was making this app, I noticed that when you change this

<Grid 
      //Namespaces omitted
      IsVisible="{Binding IsVisible, Source={x:Reference AnimatedGrid}}">
  <ContentView BackgroundColor="{StaticResource SeeThrough}"
               HorizontalOptions="Fill" VerticalOptions="Fill">

in this:

       
<Grid 
      //Namespaces omitted
      >
  <ContentView IsVisible="{Binding IsVisible, Source={x:Reference AnimatedGrid}}"
               BackgroundColor="{StaticResource SeeThrough}"
               HorizontalOptions="Fill" VerticalOptions="Fill">

you get a visually a identical result on all three platforms covered in the test project. On Android and Windows 10 it even works the same. But if you try to run on iOS, you will notice the “Open popup menu” is not clickable. This is because the outer grid covers the whole screen and is always visible - and although it’s empty, it apparently blocks all user input. As usual, Apple does things different ;)

The demo project – with updated code – can be found here

This article appeared earlier – in Dutch – on the Wortell company blog

13 April 2016

Behaviors for animated scroll-down and slide-in menus in Xamarin Forms

Preface

Some time ago I wrote about a proof-of-concept for viewmodel driven animations using behaviors in Xamarin Forms. In the mean time, time has moved on, so has Xamarin Forms, and the idea I had back then made it into a kind of framework solution I now used professionally. And it’s time to show how it’s done now in detail.

This article is about building animated scroll-into-view menu’s in Xamarin Forms, and to make sure we all understand what that means, I made this little video showing the code in action on an Android emulator, a Windows 10 Universal Windows Platform app, and an iPhone simulator:

Framework

The demo app contains a simple framework (project DemoViewFramework) that supports a number of things crucial to an app sporting viewmodel driven behaviors, that is:

  • facilitation of  MVVM (using MVVMLight)
  • registration of which view belongs to what viewmodel
  • navigation from viewmode to viewmodel (in stead of from page to page)
  • exposing essential events in the life cycle of the page to it’s accompanying viewmodel.

I won’t go into very much detail about the framework – it’s a pretty naïve implementation anyway – but there are two key things to take away. The first one is the class BaseContentPage

using Xamarin.Forms;

namespace DemoViewFramework
{
  public class BaseContentPage : ContentPage
  {
    protected override void OnAppearing()
    {
      base.OnAppearing();
      Context?.OnViewAppearing();
    }

    protected override void OnSizeAllocated(double width, double height)
    {
      base.OnSizeAllocated(width, height);
      Context?.OnViewInitialized(true);
    }

    protected override void OnDisappearing()
    {
      base.OnDisappearing();
      Context?.OnViewInitialized(false);
      Context?.OnViewDisappearing();
    }

    private IPageViewModelBase Context => (IPageViewModelBase)BindingContext;
  }
}
This class is intended to be a base class for your pages, and it's sole purpose is to channel the OnAppearing, OnSizeAllocated and OnViewDisappearing events to the object in the Page's BindingContext - that is, the view model. To this intent, every view model should implement the interface IPageViewModelBase so the corresponding methods can be called:
namespace DemoViewFramework
{
  public interface IPageViewModelBase
  {
    void OnViewInitialized(bool value);

    void OnViewAppearing(object state = null );

    void OnViewDisappearing();

    INavigationService NavigationService { get; set; }
  }
}
If you are using your own Navigation framework, you can forget about the NavigationService property. To make implementation easier, there's a base class for view models – which is the second key takeaway. In the demo project is a child class from MVVMLight's ViewModelBase, but of course you are very free to implement your own INotifyPropertyChanged base class as well.
using GalaSoft.MvvmLight;

namespace DemoViewFramework
{
  public class PageViewModelBase : ViewModelBase, IPageViewModelBase
  {
    public virtual void OnViewInitialized(bool value)
    {
      ViewIsInitialized = value;
    }

    private bool _viewIsInitialized;
    public bool ViewIsInitialized
    {
      get { return _viewIsInitialized; }
      set { Set(() => ViewIsInitialized, ref _viewIsInitialized, value); }
    }

    public virtual void OnViewAppearing(object state = null)
    {
      ViewHasAppeared = true;
    }

    public virtual void OnViewDisappearing()
    {
      ViewHasAppeared = false;
    }

    private bool _viewHasAppeared;
    public bool ViewHasAppeared
    {
      get { return _viewHasAppeared; }
      set { Set(() => ViewHasAppeared, ref _viewHasAppeared, value); }
    }

    public INavigationService NavigationService { get; set; }
  }
}
Key thing here is that there are two properties - ViewHasAppeared and ViewIsInitialized - available in every view model and they are set automatically by the framework. Behaviors handling animations need to know when they actually can start doing stuff and they can bind to one of these properties. As I explained in a previous blog post, from Xamarin 2.1 apparently you can only start after OnSizeAllocated, which translates to ViewIsInitialized in the view model, so the initialization code needs to be fired when that property is set to true. The actual activation of the behavior (that is, the animation), needs to come from another property. That may sound complicated, but I assure you it's not that bad. Just read on.

The (base) view model for driving animations

Basically, we need something to kick off the animations. To this extent, we are using this very simple view model

using DemoViewFramework;
using Xamarin.Forms;

namespace XamarinFormsDemos.ViewModels
{
  public class MenuViewModelBase : PageViewModelBase
  {
    private bool _isMenuVisible;

    public MenuViewModelBase()
    {
      ToggleMenuCommand = new Command(() => IsMenuVisible = !IsMenuVisible);
    }

    public bool IsMenuVisible
    {
      get { return _isMenuVisible; }
      set { Set(() => IsMenuVisible, ref _isMenuVisible, value); }
    }

    public Command ToggleMenuCommand { get; private set; }
  }
}

A command that toggles a simple boolean property. I mean, how hard can it be, right?

Leveling the playing field

If you are coming from Windows behaviors, like I do, you are in for a surprise. Xamarin behaviors are a prime example of something that quacks like a duck and walks like a duck, can be a goose after all. Some things are a bit different and – like a goose – can bite you pretty badly. First of all, there is no standard binding context, and there is no AssociatedObject property to easily refer to. This can make things a bit complex when 'translating' behaviors from Windows to Xamarin and back, hence this base class to make sure we have the same - or at least a more similar - base

using System;
using Xamarin.Forms;

namespace Wortell.XamarinForms.Behaviors.Base
{
  public abstract class BindableBehaviorBase<T> : Behavior<T> 
    where T : VisualElement
  {
    protected T AssociatedObject { get; private set; }

    protected override void OnAttachedTo(T bindable)
    {
      AssociatedObject = bindable;
      bindable.BindingContextChanged += Bindable_BindingContextChanged;
      base.OnAttachedTo(bindable);
    }

    protected override void OnDetachingFrom(T bindable)
    {
      bindable.BindingContextChanged -= Bindable_BindingContextChanged;
      base.OnDetachingFrom(bindable);
      AssociatedObject = null;
    }

    private void Bindable_BindingContextChanged(object sender, EventArgs e)
    {
      if (AssociatedObject != null)
      {
        BindingContext = AssociatedObject.BindingContext;
      }
    }
  }
}
It's a bit of an oddball class, but it's purpose is simple - after this has run, you can bind to any of the behavior's dependency properties, and in your code you can refer to a typed AssociatedObject, just like you would in Windows XAML behaviors.

Getting a bit animated

Now earlier in this blog post I mentioned the fact that behaviors doing animations probably need to know when a view is ready initializing, so they can initialize themselves. To get to this point, there is this base class on top of  BindableBehaviorBase, with a similar ‘original name:

using Xamarin.Forms;

namespace Wortell.XamarinForms.Behaviors.Base
{
  public abstract class ViewInitializedBehaviorBase<T> : BindableBehaviorBase<T> 
    where T : VisualElement
  {
    #region ViewIsInitialized Attached Dependency Property      
    public static readonly BindableProperty ViewIsInitializedProperty =
       BindableProperty.Create(nameof(ViewIsInitialized), typeof(bool), 
       typeof(ViewInitializedBehaviorBase<T>),
       default(bool), BindingMode.TwoWay,
       propertyChanged: OnViewIsInitializedChanged);

    public bool ViewIsInitialized
    {
      get { return (bool)GetValue(ViewIsInitializedProperty); }
      set { SetValue(ViewIsInitializedProperty, value); }
    }

    private static void OnViewIsInitializedChanged(BindableObject bindable, 
            object oldValue, object newValue)
    {
      var thisObj = bindable as ViewInitializedBehaviorBase<T>;
      thisObj?.Init((bool)newValue);
    }

    #endregion

    protected abstract void Init(bool viewIsInitialized);
  }
}

When you bind the behavior’s ViewIsInitialized property to the viewmodel’s ViewIsInitialized property, the behavior knows ‘when the view is ready (or not anymore). Whatever, if the property in the view model changes, the behavior calls its (now abstract) method Init.

So what happens is

BaseContentPage –> PageViewModelBase –> (property binding) ViewInitializedBehaviorBase.Init –>
  Concrete Implementation.Init

And finally - menu animations

One more base class to go. I noticed pretty soon that animations handling the folding or scrolling of things almost always consider one property to be animated. So I created a yet another base class

using Xamarin.Forms;

namespace Wortell.XamarinForms.Behaviors.Base
{
  public abstract class AnimateFoldBehaviorBase : 
    ViewInitializedBehaviorBase<View>
  {
    protected double FoldInPosition;
    protected double FoldOutPosition;

    protected VisualElement GetParentView()
    {
      var parent = AssociatedObject as Element;
      VisualElement parentView = null;
      if (parent != null)
      {
        do
        {
          parent = parent.Parent;
          parentView = parent as VisualElement;
        } while (parentView?.Width <= 0 && parent.Parent != null);
      }

      return parentView;
    }

    protected override void OnAttachedTo(View bindable)
    {
      base.OnAttachedTo(bindable);
      bindable.IsVisible = false;
    }

    private void ExecuteAnimation(bool show)
    {
      if (show)
      {
        AssociatedObject.IsVisible = true;
        ExecuteAnimation(FoldInPosition, FoldOutPosition, (uint)FoldOutTime);
      }
      else
      {
        ExecuteAnimation(FoldOutPosition, FoldInPosition, (uint)FoldInTime);
      }
    }

    protected abstract void ExecuteAnimation(double start, double end, 
      uint runningTime);

    public static readonly BindableProperty IsVisibleProperty =
       BindableProperty.Create(nameof(IsVisible), typeof(bool), 
       typeof(AnimateFoldBehaviorBase),
       false, BindingMode.OneWay,
       propertyChanged: OnIsVisibleChanged);

    public bool IsVisible
    {
      get { return (bool)GetValue(IsVisibleProperty); }
      set {SetValue(IsVisibleProperty, value); }
    }

    private static void OnIsVisibleChanged(BindableObject bindable, 
       object oldValue, object newValue)
    {
      var thisObj = bindable as AnimateFoldBehaviorBase;
      thisObj?.ExecuteAnimation((bool)newValue);
    }   

    // FoldInTime Attached Dependency Property      
 
    // FoldOutTime Attached Dependency Property      
  }
}

This class handles a few important things. First of all, it gets the parent view – in a menu’s case the page. In fact, it finds the first object that has a width, thus a size. This is the behavior’s ‘playing field’. Then there is the ExecuteAnimation method, that actually starts the animation – or reverses it. Note also that the behavior actually hides the element that it’s animating – this whole setup assumes you will move something into view, while it’s not in view initially. You might notice that the Init method which is abstract in it's parent class is still not implemented. That happens only in the concrete class - that is actually pretty small, now all the ground work has been laid:

using Wortell.XamarinForms.Behaviors.Base;
using Xamarin.Forms;

namespace Wortell.XamarinForms.Behaviors
{
  public class AnimateSlideDownBehavior : AnimateFoldBehaviorBase
  {
    protected override void Init(bool newValue)
    {
      if (newValue)
      {
        var parentView = GetParentView();
        if (parentView != null)
        {
          FoldInPosition = -parentView.Height;
          AssociatedObject.TranslationY = FoldInPosition;
        }
      }
    }

    protected override void ExecuteAnimation(double start, 
       double end, uint runningTime)
    {
      var animation = new Animation(
        d => AssociatedObject.TranslationY = d, start, end, Easing.SinOut);

      animation.Commit(AssociatedObject, "Unfold", length: runningTime,
        finished: (d, b) =>
        {
          if (AssociatedObject.TranslationY.Equals(FoldInPosition))
          {
            AssociatedObject.IsVisible = false;
          }
        });
    }
  }
}

So on Init, the menu is moved up exactly it’s own height and - assuming it’s on top of the page - it will appear just outside the view. The actual animation code itself is laughably simple – it just animates over TranslationY, and when it find it’s ending at the FoldInPosition, it will make the animated object invisible

All that’s left now is defining the menu in XAML and then adding the behavior to it. It’s important to add the menu after the actual page content so it will be drawn over it. Otherwise it will just slide behind it, and that’s not very useful.

<!-- Menu -->
<ContentView Grid.Row="0" Grid.RowSpan="2">
  <ContentView.Behaviors>
    <behaviors:AnimateSlideDownBehavior
      IsVisible="{Binding IsMenuVisible}"
      ViewIsInitialized="{Binding ViewIsInitialized}"
      FoldOutTime="400" FoldInTime="300"/>
  </ContentView.Behaviors>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="75*"></RowDefinition>
      <RowDefinition Height="25*"></RowDefinition>
    </Grid.RowDefinitions>
    <ContentView Style="{StaticResource MenuStyle}">
      <StackLayout Style="{StaticResource ContentStyle}" 
        Orientation="Vertical" VerticalOptions="Start">
        <Image Source="chevronup.png" HeightRequest="40" 
          VerticalOptions="Start" HorizontalOptions="Start">
          <Image.GestureRecognizers>
            <TapGestureRecognizer Command="{Binding ToggleMenuCommand}"/>
          </Image.GestureRecognizers>
        </Image>
        <Label Text="Menu" Style="{StaticResource HeaderStyle}"
          VerticalOptions="Start"/>
        <Label Text="Here be menu content" 
         Style="{StaticResource MenuTextStyle}"></Label>
      </StackLayout>
    </ContentView>

    <!-- Leftover space -->
    <ContentView Grid.Row="1"  HorizontalOptions="Fill" VerticalOptions="Fill">
      <ContentView.GestureRecognizers>
        <TapGestureRecognizer Command="{Binding ToggleMenuCommand}"/>
      </ContentView.GestureRecognizers>
    </ContentView>
  </Grid>
</ContentView>

Note the binding of ViewIsInitialized to ViewIsInitialized for the initialization, and of IsVisible to IsMenuVisible for the actual execution of the animation. Also notice the FoldOutTime and FoldInTime – the menu will fold out in 400, and fold in in 300ms. These are optional values – if you don’t specify them, the behavior will use default values. Finally, note the fact that the menu actually covers the whole screen, but the lower part is empty. Yet, if you tap that part, the menu will move away as well, as you would expect in an app.

The AnimateSlideInBehavior is nearly the same but animates TranslateX – just have a look at the sample code, it’s yours for the taking

Some concluding remarks

Of course you can also code these animations into code behind or in a control’s code behind, but that way you will need to copy and paste code into your views time and time again, and potentially you will need to fix the same bugs on multiple places. What I usually do is indeed make a control, but use the behavior in that and then re-use the control over multiple pages. Using a behavior decouples the code from a specific control or page, and you can re-use the dynamic behavior everywhere.

One final bit – the Xamarin iOS linker is still sometimes ‘optimizing’ away code, just like .NET Native, something that already bit me last year. So in the Wortell.XamarinForms there is this extremely odd class Initializer:

namespace Wortell.XamarinForms
{
  public static class Initializer
  {
    public static void Init()
    {
    }
  }
}
that indeed does completely nothing, except for being called from the iOS project's AppDelegate
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    global::Xamarin.Forms.Forms.Init();
    global::Wortell.XamarinForms.Initializer.Init();
    LoadApplication(new App());

    return base.FinishedLaunching(app, options);
}
Which still does nothing, but  tricks the compiler into thinking the code is actually used (which is true) so the assembly actually gets packaged with the app.

I hope this blog will inspire you to try your hand at animations with Xamarin Forms as well. It’s actually pretty easy when you get the hang of it. And there is more to come on this blog.

Parts of this text appeared earlier on the Wortell company blog, in Dutch