iOS Animations in Xamarin
Will Hutchinson (@tetowill)
april 7th, 2016
We recently launched the app TaxChat, “tax preparation for people who have better things to do.” The iOS app saves you from having to do your taxes by yourself; instead you just answer a few questions, snap a couple of photos and a certified tax professional will take care of your tax return for you. All through a beautiful & intuitive interface. You can read more about it at tax.chat.
Since we built TaxChat using Xamarin, I figure this is a great time to write a post on iOS animations in Xamarin and detail some of the animations seen in the app. If you don’t already know about Xamarin, check out this introduction to Xamarin by our resident Xamarin MVP, Sean Sparkman. Essentially, Xamarin allows you to build native apps for multiple platforms all in C#, which is pretty sweet.
Why animate?
Animations are an important part of any user interface. Subtle
animations can turn a static layout into an engaging experience by
showing movement when an action occurs or informing the user that
something is happening. Not only can animations be helpful, but when
done properly, they create a very polished look and feel. There are
quite a few ways to animate in iOS, some of which are mentioned in
Xamarin’s CoreAnimation intro.
In this post I’ll discuss AnimateNotify
, AnimateKeyframes
and
AddKeyframeWithRelativeStartTime
, since that’s what I used in
TaxChat. I’ll show the animation comps for a few screens, go over
what’s happening and then discuss the code.
Simple position animation
This video comp shows the launch screen and four onboarding screens
visible when you first open the app. I’m going to focus on the second
screen with the yellow background first since it’s the simplest
animation. Here we have six icons, when the screen appears the second
and third row icons slide in from the right to line up horizontally
with the first row icons. When the screen disappears the icons slide
back over to the right. The six icons are inside a container UIView
which has the necessary constraints placed on it, so all we have to
worry about is updating the positioning of those icons inside the
container.
To set the stage, let me say that this screen is fully laid out in the
storyboard with the icons lined up in their final position. On load
we’ll adjust the second row icons to have a 10pt offset and the third
row a 20pt offset. To update the positioning we’ll use the elements’
center CGPoint
.
public override void ViewDidLoad()
{
base.ViewDidLoad();
// set icon offsets
iconThree.Center = new CGPoint(iconThree.Center.X + 10, iconThree.Center.Y);
iconFour.Center = new CGPoint(iconFour.Center.X + 10, iconFour.Center.Y);
iconFive.Center = new CGPoint(iconFive.Center.X + 20, iconFive.Center.Y);
iconSix.Center = new CGPoint(iconSix.Center.X + 20, iconSix.Center.Y);
}
When the view is going to appear we’ll animate those icons back to
their base position. We’ll be using AnimateNotify
with a one second
duration, no time delay, an optional easing and since we don’t need
anything to happen on completion we’ll make that null. The Xamarin
documentation has a
full list of all the UIViewAnimationOptions.
It should also be noted that you can use multiple
UIViewAnimationOptions
by separating them with a pipe operator.
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
// AnimateNotify ( duration in seconds, delay in seconds, options, animations, completion )
UIView.AnimateNotify(1, 0, UIViewAnimationOptions.CurveEaseOut,
()=> {
iconThree.Center = new CGPoint(iconThree.Center.X - 10, iconThree.Center.Y);
iconFour.Center = new CGPoint(iconFour.Center.X - 10, iconFour.Center.Y);
iconFive.Center = new CGPoint(iconFive.Center.X - 20, iconFive.Center.Y);
iconSix.Center = new CGPoint(iconSix.Center.X - 20, iconSix.Center.Y);
}, null);
}
When the view is going to disappear, we’ll animate the icons over to their offset position again.
public override void ViewWillDisappear(bool animated)
{
base.ViewWillDisappear(animated);
UIView.AnimateNotify(1, 0, UIViewAnimationOptions.CurveEaseOut,
()=> {
iconThree.Center = new CGPoint(iconThree.Center.X + 10, iconThree.Center.Y);
iconFour.Center = new CGPoint(iconFour.Center.X + 10, iconFour.Center.Y);
iconFive.Center = new CGPoint(iconFive.Center.X + 20, iconFive.Center.Y);
iconSix.Center = new CGPoint(iconSix.Center.X + 20, iconSix.Center.Y);
}, null);
}
Grouped animations
Alright, that first one was simple enough. Let’s move on to the third
onboarding screen (light gray background) now. Here we have three
images, one of a tax return and two chat bubbles. While fading in the
tax return slides up, the first chat bubble slides right, and the
second chat bubble slides left. There are a couple differences with
this screen and the last. First, the animations are staggered
slightly. This means we can’t use one AnimateNotify
block, so
instead we’ll use AnimateKeyframes
and
AddKeyframeWithRelativeStartTime
. Second, we have constraints set on
each of these images. This means we can’t adjust their positioning
like before, instead we’ll have to update the corresponding
constraints placed on each image.
This screen is also fully laid out in the storyboard with all the images in their final position. When the view loads we’ll set the starting points for our animations.
public override void ViewDidLoad()
{
base.ViewDidLoad();
chatBubbleOne.Alpha = 0f;
chatBubbleTwo.Alpha = 0f;
taxReturnImage.Alpha = 0f;
// shift horizontal center constraint to the left 10
chatBubbleOneCenter.Constant = -15f;
// shift leading constraint to the right 10
chatBubbleTwoLeading.Constant = 80f;
// shift top constraint down 10
taxReturnImageTop.Constant = 70f;
View.SetNeedsLayout(); // specify the layout needs updating
View.LayoutIfNeeded(); // update layout
}
When the view is going to appear we’ll animate things back into place.
We’ll be using AnimateKeyframes
with a one second duration, no time
delay, an optional easing and since we don’t need anything to happen
on completion we’ll leave that empty (null throws an error). The
Xamarin documentation also has a
full list of all the IViewKeyframeAnimationOptions.
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
// AnimateKeyframes ( duration in seconds, delay in seconds, options, animations, completion )
UIView.AnimateKeyframes(1, 0, UIViewKeyframeAnimationOptions.CalculationModeLinear, () => {
// AddKeyframeWithRelativeStartTime ( start time, duration, animations )
// start time and duration are 0-1 as percentage of wrapping AnimateKeyframes duration
UIView.AddKeyframeWithRelativeStartTime (0, 0.4, () => {
taxReturnImageTop.Constant = 60f;
taxReturnImage.Alpha = 1f;
});
UIView.AddKeyframeWithRelativeStartTime (0.2, 0.5, () => {
chatBubbleOneCenter.Constant = -5f;
chatBubbleOne.Alpha = 1f;
});
UIView.AddKeyframeWithRelativeStartTime (0.4, 0.6, () => {
chatBubbleTwoLeading.Constant = 70f;
chatBubbleTwo.Alpha = 1f;
});
View.SetNeedsLayout();
View.LayoutIfNeeded();
}, (finished) => {});
}
Let’s go over the keyframe animations above. AnimateKeyframes
and
AddKeyframeWithRelativeStartTime
work together. AnimateKeyframes
is the container that controls the overall settings of the animation
group. As the name suggests, AddKeyframeWithRelativeStartTime
adds
the ability to adjust the start time and duration of animations inside
the group relative to the duration specified in AnimateKeyframes
.
Above we have our entire group of animations lasting 1 second. The tax
return image animation starts immediately and lasts 0.4 or 40% of the
overall duration, which in this case is 0.4 seconds. The second chat
bubble animation has a start time delay of 0.4 or 40% of the overall
duration and lasts 0.6 or 60% of the overall duration. Notice in that
scenerio both the delay and duration add up to 1.0 or 100%. If the
total combined value of delay and duration in
AddKeyframeWithRelativeStartTime
is greater than 1.0 your animation
will get cut short and jump right to the final specified settings when
the overall duration is reached.
Something else to point out from the above animation block is the call
to View.SetNeedsLayout()
and View.LayoutIfNeeded()
. Both of these
are necessary because we are updating constraint values.
SetNeedsLayout
gets called on a view to specify that the view’s
layout needs updating. LayoutIfNeeded
is then called to update the
layout for any items that need it. If you do not call these you will
not see the animation, just the end result. Also rememeber to make
sure you’re not creating any conflicts when updating constraints.
Chained animations with spring
In this screen we are also relying on information coming back from the back-end API that will signal when to move forward in the onboarding process, so during these animations, in addition to moving things around, we also need to handle any errors or possible delays appropriately. Let’s break this down and see what we’re doing here.
The elements on this screen are being laid out entirely in the code based on screen size, since the app is locked in portrait mode, we don’t have to worry about constraints or screen orientation changes. We have five elements being animated. Three that you can easily identify: the text label and both chevrons; two that are less noticeable, which are cover layers behind each chevron matching the background. The cover layers hide the text label when the chevrons close while allowing the two chevrons to overlap each other.
For animations we have: the chevrons opening, closing, and the final animation where the elements disperse and fade out. We will need to set up each of these animations as a separate block and call the next one on completion if all is well. There are a lot of variables used in the following animation blocks. These are being set earlier in the code so the specific values are not shown here, but they should be self explanatory. Our chevrons start closed, so when the screen appears we’ll call our method to open the chevrons.
void ChevronsOpen()
{
// AnimateNotify ( duration, delay, spring damping, spring velocity, options, animations, completion )
UIView.AnimateNotify(0.5, 0.5, 0.85, 1.0, 0,
() => {
leftChevronView.Center = new CGPoint (chevronOpenX, chevronY);
rightChevronView.Center = new CGPoint (screenWidth - chevronOpenX, chevronY);
leftCover.Frame = new CGRect (0, 0, coverWidth, screenHeight);
rightCover.Frame = new CGRect (screenWidth - coverWidth, 0, coverWidth, screenHeight);
},
(finished) => {
if(passes >= 4) {
// if at least 4 passes and expert is found, go to expert screen
if(preparerId > 0 ) {
ShowExpertScreen();
}
// if we've made 12 passes and haven't connected to a preparer
else if(totalPasses >= 12 && preparerId == 0) {
FailedToFindPreparerError(); // shows alert and returns to previous screen
return;
}
// give it another run
else {
passes = 0;
ChevronsClose();
}
}
else {
ChevronsClose();
}
}
);
}
In the above method we’re using AnimateNotify
with the optional
spring damping and velocity parameters to add a slight bounce to the
chevrons at the end of the animation. The spring damping ratio is a
value between 0 and 1, where the oscillation increases with a smaller
value, so 1 is no oscillation and 0 is max oscillation. The initial
spring velocity is points per second. We want a very slight
oscillation, so we’ll use a 0.85 damping and 1.0 velocity.
In the completion call we’ll check the number of passes we’ve made,
which we’re tracking in the ChevronsClose
method to follow. If we
are at less than four passes we’ll call the method for the closing
animation. If we are at four or more passes then we’ll check to see if
we have a TaxChat expert lined up. If so, we’ll call
ShowExpertScreen
which will run our final animation and move on to
the next screen. If not, we’ll either give it some more time or go
back after 12 passes.
Let’s take a look at our close animation method. Here we’re animating
our chevrons back to their center positions using the same spring
damping and velocity values. On completion we update the label text,
attempt to match the user with a tax preparer, update the pass count
and then call the ChevronsOpen
method.
void ChevronsClose()
{
// AnimateNotify ( duration, delay, spring damping, spring velocity, options, animations, completion )
UIView.AnimateNotify(0.5, 0.5, 0.85, 1.0, 0,
() => {
leftChevronView.Center = leftChevronCenter;
rightChevronView.Center = rightChevronCenter;
leftCover.Frame = leftCoverFrame;
rightCover.Frame = rightCoverFrame;
},
(finished) => {
// swap out text on even passes
if(passes % 2 == 0) {
titleLabel.Text = "We're finding your TaxChat Expert";
}
else {
titleLabel.Text = "Just a moment";
}
// match user with a preparer
AsyncHelper.RunSync<bool> (GetPreparerId);
passes += 1;
totalPasses += 1;
ChevronsOpen();
}
);
}
When we’re ready to move on we call the ShowExpertScreen
method.
Here we’re animating our chevrons and text label out with no spring
effect. Then when the animation is complete we move on to our next
screen.
void ShowExpertScreen()
{
// AnimateNotify ( duration, delay, options, animations, completion )
UIView.AnimateNotify(0.5, 0.5, UIViewAnimationOptions.CurveEaseInOut,
() => {
leftChevronView.Alpha = 0;
leftChevronView.Center = new CGPoint (20 + 11, chevronY);
rightChevronView.Alpha = 0;
rightChevronView.Center = new CGPoint (screenWidth - (20 + 11), chevronY);
titleLabel.Alpha = 0;
titleLabel.Center = new CGPoint (titleLabel.Center.X, titleLabel.Center.Y - 50);
},
(finished) => {
// go to next screen
NavigationController.PushViewController(expertController, true);
}
);
}
To be continued…
So those are a few ways to animate in Xamarin.iOS. There are many more. It may seem complicated, but it’s really not that bad. If you’re thinking about adding animations to your app, things are going to go a lot smoother if you have a designer who can provide animation comps. For the animations in TaxChat we were given video comps, as shown, and Flinto prototypes, which were also helpful. Having a set visual guide for your animations will allow you to plan and implement the best solution.
There are still a couple more animations from TaxChat that I’d like to share with you, but to keep things a bit more digestable I’m going to split those off into a second post that’ll be coming up soon. The next post will discuss animating a CGAffineTransform and using CABasicAnimation. Thanks for reading, I hope you enjoyed this post and I look forward to having you return for part two. Cheers!
Tags: technology csharp xamarin animation