Monday, February 16, 2015

Animate the Height of RowDefinition

When I first was faced with the requirement to create a kind of accordion-like area that open and closes by the click of a button, I thought to my self that that would be easy...

The solution was pretty easy, but accepting that the most simple solution didn't work was not.

The requirement was based on the image below. When you tap the surface titled "Tap me!" the red surface was going to animate it's height and disappear. I know I'm not going to earn any design awards from this! :)

Grids to the rescue!

I defined a simple three-row grid that gives the layout displayed in the image above.

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="AnimatedGrid.MainPage">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="200" />
            <RowDefinition Height="*" />
            <RowDefinition Height="100" />
        </Grid.RowDefinitions>

        <BoxView BackgroundColor="Red" Grid.Row="0" />
        <BoxView BackgroundColor="Blue" Grid.Row="1">
            <BoxView.GestureRecognizers>
                <TapGestureRecognizer Command="{Binding Animate}" />
            </BoxView.GestureRecognizers>
        </BoxView>
        <BoxView BackgroundColor="Yellow" Grid.Row="2" />

        <Button Grid.Row="1" Text="Tap me!" TextColor="White" 
            HorizontalOptions="Center" 
            VerticalOptions="Center" Command="{Binding Animate}" />
        <Label Grid.Row="0" Text="I'm going to animate" 
            HorizontalOptions="Center" VerticalOptions="Center" />

    </Grid>
</ContentPage>

Attempt one - binding to the height property

I then created a view model with a TopRowHeight property that I bound to the first RowDefinition.

 <RowDefinition Height="{Binding TopRowHeight}" />

This did not work at all. Even though the documentation states that the HeightProperty is bindable it simply does not call the getter. I tried returning an int, float, double and GridLength and any binding mode possible.

The solution - xaml

So what I ended up doing was to give the RowDefinition a name instead. For this example, it's important to keep the Height="200" since it decides what height to open up to again.

        <Grid.RowDefinitions>
            <RowDefinition 
Height="200x:Name="topRow" />
            <RowDefinition Height="*" />
            <RowDefinition Height="100" />
        </Grid.RowDefinitions>

The solution - ViewModel

And then a ViewModel that looks like the snippet below. Two things to note first before you bash my code.
  • The INotifyPropertyChanged-implementation is omitted. Use a base class or implement it in any real world example.
  • We take an Action in the constructor, this might not be so good in the world of IoC, but it's there to make for a simpler example.

    public class MainPageViewModel
    {
        private Action _rowAnimation;

        public MainPageViewModel(Action rowAnimation)
        {
            _rowAnimation = rowAnimation;
        }

        public ICommand Animate
        {
            get
            {
                return new Command((o) =>
                {
                    _rowAnimation.Invoke();
                });
            }
        }
    }

The solution - The View

And then the actual implementation of the View (the page). What we do here is to create the MainPageViewModel and pass a reference of an Action that takes care of the animation of the grid row.

public partial class MainPage : ContentPage
    {    
        private Animation _animation;
        private double _initialHeight;

        public MainPage ()
        {
            InitializeComponent ();

            // Since we can't use binding to animate the row height directly we
            // need to give the ViewModel a method to call when it wants to 
            // animate the row height.
            var model = new MainPageViewModel(AnimateRow);
            BindingContext = model;

            // Store the inital value so we know what to what height to restore to
            _initialHeight = topRow.Height.Value;
        }

        private void AnimateRow()
        {
            if(topRow.Height.Value < _initialHeight)
            {
                // Move back to original height
                _animation = new Animation(
                    (d) => topRow.Height = new GridLength(Clamp(d, 0, double.MaxValue)),
                    topRow.Height.Value, _initialHeight, Easing.SpringIn, () => _animation = null);
            }
            else
            {
                // Hide the row
                _animation = new Animation(
                    (d) => topRow.Height = new GridLength(Clamp(d, 0, double.MaxValue)),
                    _initialHeight, 0, Easing.SpringIn, () => _animation = null);
            }

            _animation.Commit(this, "the animation");
        }

        // Make sure we don't go below zero
        private double Clamp(double value, double minValue, double maxValue)
        {
            if (value < minValue)
            {
                return minValue;
            }

            if (value > maxValue)
            {
                return maxValue;
            }

            return value;
        }
    }

Resources

You can download the example from here!

3 comments:

  1. Exactly what I was looking for thank you.

    ReplyDelete
    Replies
    1. Glad it helped you out! Let me know if there is anything I should write about!

      Delete
  2. Wow... this has saved me so much time! Thanks so much Johan! :)

    ReplyDelete