Using EventTriggers to animate a login page in Xamarin.Forms
A common scenario in apps is to want users to sign in before they get access to the features and functions of the app. In order to sign in, users must first register, may want to change their password, and at some stage some of them will forget their password and need to request a new one. This post covers one approach to the UI to handle this process. I won’t be going into the authentication process itself here (in fact there are better ways of handling authentication which take care of the UI for you) – rather I’ll be using this as an example to present some techniques for UI manipulation.
Typically then, you’ll want your users to be presented with a login screen when they first run the app. From this screen you’ll want to make functions available to register as a new user, to change their password, and to request a password reset. Only once a user has successfully authenticated will you let them into your app proper. Once they’re in the app, you don’t want them to be able to navigate back to the login page. In fact, the only way they should ever see the login page again should be if they logout or if their session expires. Of course you may want to force them to log in every time, depending on your app scenario, but in any event you don’t want the login screen(s) to interfere with the navigation stack once a user has successfully authenticated.
This is a common scenario which has been addressed before, and one way to achieve this in a Xamarin.Forms based app is to use the MessagingCenter to send messages either requesting the login screen or indicating successful authentication, with the messages being subscribed to by App.cs. This means that App.cs can, on receipt of the message, change the MainPage property of the Xamarin.Forms app, taking care of the navigation that we’re after. (For some more information on the user of MessagingCenter take a look at my earlier blog post GHOST_URL/post/2015/02/16/update-on-viewmodel-to-viewmodel-navigation-in-xamarinforms-using-messagingcenter.aspx)
This post builds on this approach and shows you how to add some nice Xamarin.Forms animation, whilst staying firmly within the Mvvm paradigm.
First, we’ll define a LoginView.xaml page. The approach we’ll take to the four ‘screens’ we’re after (login, register, change password and forgot password) will be to have a StackLayout for each one, all within a Grid on the same page. We’ll control the visibility of each StackLayout programmatically in order to display the ‘screen’ the user has requested. This will let us use some nice animations to hide and view each StackLayout. Our LoginView then, should start off looking something like this:
<?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:triggers="clr-namespace:LoginAnimation.Triggers;assembly=LoginAnimation"
x:Class="LoginAnimation.Views.LoginView">
<ContentPage.Content>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Opacity="1"
Source="image_bg.png"
Aspect="AspectFill"/>
<Grid>
<Grid.Padding>
<OnPlatform x:TypeArguments="Thickness">
<OnPlatform.iOS>
0, 20, 0, 0
</OnPlatform.iOS>
<OnPlatform.Android>
0, 0, 0, 0
</OnPlatform.Android>
<OnPlatform.WinPhone>
0, 0, 0, 0
</OnPlatform.WinPhone>
</OnPlatform>
</Grid.Padding>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackLayout Grid.Row="0">
<Label Text="Login"
TextColor="White"
HorizontalTextAlignment="Center" />
</StackLayout>
<StackLayout Grid.Row="1">
<ActivityIndicator x:Name="activityIndicator"
IsRunning="{Binding IsBusy}"
IsVisible="{Binding IsBusy}"
Color="Blue"
/>
<Label x:Name="labelBusyMessage"
IsVisible="{Binding IsBusy}"
Text="{Binding BusyMessage}" >
<Label.Font>
<OnPlatform x:TypeArguments="Font">
<OnPlatform.iOS>Small</OnPlatform.iOS>
</OnPlatform>
</Label.Font>
</Label>
<Label x:Name="labelErrorMessage"
IsVisible="{Binding IsError}"
Text="{Binding ErrorMessage}"
TextColor="Red">
<Label.Font>
<OnPlatform x:TypeArguments="Font">
<OnPlatform.iOS>Small</OnPlatform.iOS>
</OnPlatform>
</Label.Font>
</Label>
</StackLayout>
<StackLayout x:Name="slLogin"
Grid.Row="1" Grid.Column="0"
IsVisible="True"
Padding="20,20,20,20"
>
<Entry Placeholder="Email"
Text="{Binding UserId}"
Style="{DynamicResource EntryStyle}" />
<Entry Placeholder="Password"
Text="{Binding Password}"
IsPassword="true"
Style="{DynamicResource EntryStyle}" />
<Button Text="Login" Command="{Binding LoginCommand}" />
<Button Text="Register" >
</Button>
<Button Text="Forgot Password">
</Button>
<Button Text="Change Password">
</Button>
</StackLayout>
<StackLayout x:Name="slRegister"
Grid.Row="1" Grid.Column="0"
IsVisible="False"
Padding="20,20,20,20"
>
<Entry Placeholder="Email"
Text="{Binding UserId}"
Style="{DynamicResource EntryStyle}" />
<Entry Placeholder="First name"
Text="{Binding FirstName}"
Style="{DynamicResource EntryStyle}" />
<Entry Placeholder="Last name"
Text="{Binding LastName}"
Style="{DynamicResource EntryStyle}" />
<Button Text="Register"
Command="{Binding RegisterCommand}" />
<Button Text="Cancel">
</Button>
</StackLayout>
<StackLayout x:Name="slForgotPassword"
Grid.Row="1" Grid.Column="0"
IsVisible="False"
Padding="20,20,20,20"
>
<Entry Placeholder="Email"
Text="{Binding UserId}"
Style="{DynamicResource EntryStyle}" />
<Button Text="Send email"
Command="{Binding SendEmailCommand}" />
<Button Text="Cancel">
</Button>
</StackLayout>
<StackLayout x:Name="slChangePassword"
Grid.Row="1" Grid.Column="0"
IsVisible="False"
Padding="20,20,20,20"
>
<Entry Placeholder="Old password"
Text="{Binding OldPassword}"
IsPassword="True"
Style="{DynamicResource EntryStyle}" />
<Entry Placeholder="New password"
Text="{Binding NewPassword}"
IsPassword="True"
Style="{DynamicResource EntryStyle}" />
<Entry Placeholder="Confirm password"
Text="{Binding ConfirmPassword}"
IsPassword="True"
Style="{DynamicResource EntryStyle}" />
<Button Text="OK"
Command="{Binding ChangePasswordCommand}" />
<Button Text="Cancel">
</Button>
</StackLayout>
</Grid>
</Grid>
</ContentPage.Content>
</ContentPage>
Note that this is making use of a style defined in App.xaml as follows:
<?xml version="1.0" encoding="utf-8" ?>
<Application xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="LoginAnimation.App">
<Application.Resources>
<ResourceDictionary>
<Style x:Key="EntryStyle" TargetType="Entry">
<Setter Property="BackgroundColor" Value="#55555555" />
<Setter Property="TextColor" Value="White"/>
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>
You can see here I’ve got four name StackLayouts – slLogin, slRegister, slForgotPassword and slChangePassword.
One approach we could take is to bind the visibility of each StackLayout to a different Boolean property in a LoginViewModel, meaning that we could set these properties programmatically and control which StackLayout is displayed on screen. We could then have commands in our ViewModel which change these properties based on user input. In some cases, this approach can be fine. However, there are a couple of reasons why it’s not suitable for us here. Firstly, we don’t just want the StackLayouts to instantaneously appear and disappiear. Instead we want a nice, slick animation to switch between them. Secondly, bear in mind that we’re changing our UI in response to user input (tapping a button). In terms of architectural purity this is all UI, and should all be handled by the UI layer. It shouldn’t impact the ViewModel at all. How can we achieve this, specifically without loading code into our code behind classes?
EventTriggers
Xamarin.Forms lets us write EventTriggers, which are another great way to isolate and reuse discrete bits of code. First, we’ll write a TriggerAction, like this:
public class SwitchLoginViewEventTrigger : TriggerAction<Button>
{
protected override void Invoke(Button sender)
{
throw new NotImplementedException();
}
}
…and attach it to the Clicked event of one of our buttons like this:
<Button Text="Register" >
<Button.Triggers>
<EventTrigger Event="Clicked">
<triggers:SwitchLoginViewEventTrigger />
</EventTrigger>
</Button.Triggers>
</Button>
For good measure, we’ll also add a property to specify the direction of animation:
public enum eDirection
{
Left,
Right
}
public eDirection Direction { get; set; }
…and use it like this:
<Button Text="Register" >
<Button.Triggers>
<EventTrigger Event="Clicked">
<triggers:SwitchLoginViewEventTrigger
Source="slLogin"
Target="slRegister"
Direction="Right" />
</EventTrigger>
</Button.Triggers>
</Button>
*Note that although we’re working with StackLayouts here, I’ve used Xamarin.Forms.View as the type for any corresponding variables and parameters in the code, to ensure that it can run unchanged against other Xamarin.Forms controls
*
Below is the code that we need in the trigger action to perform the animation, based on these properties:
protected override async void Invoke(Button sender)
{
View hideMe = null;
View showMe = null;
if (Target != null)
{
// Find showMe based on property set in xaml
showMe = ((View)sender.Parent.Parent).FindByName<View>(Target);
if (showMe != null)
{
// Find hideMe based on property set in xaml
if (Source != null)
{
hideMe =
((View)sender.Parent.Parent)
.FindByName<View>(Source);
if (hideMe != null)
{
await PerformAnimation(hideMe, showMe);
}
}
}
}
}
You can see that this is finding the StackLayouts identified by the Source and Target properties, and then passing them into the PerformAnimation method. We can now turn our attention to the code which will actually perform the animation that we’re after. This can take a bit of playing with to get right, especially when it comes to timings. Any animations should look good, but once a user has seen it once it should not slow them down. If they have to wait a second just for an animation to finish then you’ll start to lose engagement, no matter how good the animation is. This may mean that you need to make the animation faster than you expect, but when you’re looking at the end result don’t only judge it on how slick it looks. Be sure to also judge it on whether or not it slows you down when you use the app.
The code I ended up with is as follows:
private async Task PerformAnimation(View pHideMe, View pShowMe)
{
int hideStart = 0;
int hideStop = (Direction == eDirection.Left ? -90 : 90);
int showStart = (Direction == eDirection.Left ? 90 : 270);
int showStop = (Direction == eDirection.Left ? 0 : 360);
// Prep - put the stacklayouts (views) to their
// animation start positions
await pHideMe.RotateYTo(hideStart, 0);
await pShowMe.RotateYTo(showStart, 0);
await pShowMe.ScaleTo(0.2, 0);
pShowMe.IsVisible = true; // This is rotated at 90 or 270
// degrees, so it can't be seen yet
// Animate
// Kick off the fade, and then start rotating immediately
pHideMe.FadeTo(0.5, 100, Easing.SinOut);
pHideMe.ScaleTo(0.2, 100, Easing.Linear);
await pHideMe.RotateYTo(hideStop, 150, Easing.Linear);
// Kick off the fade, and then start rotating immediately
pShowMe.FadeTo(1, 100, Easing.SinIn);
pShowMe.ScaleTo(1, 150, Easing.Linear);
await pShowMe.RotateYTo(showStop, 150, Easing.Linear);
// Tidy up
pHideMe.IsVisible = false; // This can now be hidden as it's
// already out of sight
}
The first three lines after the declarations make sure that the two StackLayouts we’re working with are correctly set up for the animation. First we ensure that the one we’re about to hide is in full view (if it’s notm then we’ve probably got other problems) by rotating it around its Y axis to zero degrees. We’re doing this instantaneously by specifying a duration of zero milliseconds. Next, we’re rotating the StackLayout we’re about to show to either 90 degrees or 270 degrees, depending on the specified animation direction. In any event, it’s rotate to a right angle to the screen, meaning it’s out of view. We’re going to rotate the visible one on its Y axis to this right angle (i.e. until it’s out of view), and then rotate the one we want to view from the right angle in the same way, creating the illusion that the screen has flipped right round to show us what was on the back. We’re also going fade out the one we’re hiding, as it rotates, and fade in the one we’re viewing, again, as it rotates. The next line takes care of the fading. We’re also scaling the image a little so that it shrinks slightly while fading out, and the grows back to full size when fading in. The ScaleTo commands take care of this. Note that the fading and the scaling calls are not awaited. Your IDE will likely warn you that this is the case and suggest you add ‘await’ to the call. We don’t want this, as we want to kick off the fading and scaling, and have this proceed while we’re rotating. The fading is set to take 100 milliseconds, and the scaling 150 ms. This way we can await the rotate over 150ms and be confident that by the time that has finished, the fading and scaling have also finished. We then do the opposite to the other StackLayout – call FadeTo and ScaleTo without awaiting, and await RotateYTo. The last step is to set the first StackLayout’s IsVisible property to false. Although this is redundant in terms of the animations (as it’s been rotated to exactly 90 degrees on its Y axis, meaning it can’t be seen), we will be using its IsVisible property for any subsequent animations to determine which StackLayout is currently in view.
The full code is available here: https://github.com/AlecDTucker/LoginAnimations
Happy animating.