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"
<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"
<Label Grid.Row="0" Text="I'm going to animate"
HorizontalOptions="Center" VerticalOptions="Center" />
</Grid>
</ContentPage>
</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}" />
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="200" x:Name="topRow" />
<RowDefinition Height="*" />
<RowDefinition Height="100" />
</Grid.RowDefinitions>
<RowDefinition Height="200" x: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();
});
}
}
}
{
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;
}
}
{
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!
No comments:
Post a Comment