iOS Animations in Xamarin - part 2
Will Hutchinson (@tetowill)
july 6th, 2016
We’re back with the second part of our post on iOS Animations in
Xamarin. In this post I’m detailing some of the animations seen in
TaxChat,
an iOS App we recently launched. In
the first part
we discussed AnimateNotify
, AnimateKeyframes
and
AddKeyframeWithRelativeStartTime
. In this continuation we will look
at animating rotation and scale using
CGAffineTransform,
then animating a
CAGradientLayer
using
CABasicAnimation.
Transform animation
In this video comp we’re going to take a look at the payment
processing screen, which is similar to the last screen we reviewed in
the previous post, in the fact that it’s chaining together multiple
animation blocks and relying on information from the server to move
forward. This screen is laid out in the storyboard like the first
screen we discussed in post one. All the elements are inside of a
container UIView
that has the necessary constraints placed on it, so
we don’t have to worry about updating any constraints. The one thing
in this animation that’s different is there are a couple of transforms
being used, so that is what we’ll focus on.
The transforms we’re animating are the rotation of the circular progress bar and the scale of the check mark. There’s a fair amount of animation code for this screen and since most of it is similar to what has already been discussed, I’m going to just point out the transform animation sections. For the circular progress bar we’re using five elements: one is the gray background ring and then four gold quarter section rings. The four quarter section gold rings all fade in on the top right corner, then each rotates around to its appropriate position to create the full gold ring.
When the screen appears we’ll call AnimateProcessing
and start the
animation. We’re using AnimateKeyframes
and
AddKeyframeWithRelativeStartTime
again to stagger our animations. In
the first keyframe block we’re fading in our gray background ring and
in the second block we fade in all our gold quarter rings. To make the
rotations we need to create a CGAffineTransform
. Below you’ll see
that in the third keyframe block we’re using
CGAffineTransform.MakeRotation(ConvertToRadians(90))
. The rotation
transforms use radians, so we’ll be converting degrees to radians.
ConvertToRadians
is a method to do this conversion. You can find it
below the AnimateProcessing
method. So in the third keyframe block
we’re rotating the bottom right, bottom left and top left quarter
rings 90 degrees to the bottom right position. In the fourth block
we’re using
CGAffineTransform.Rotate(TopLeftImage.Transform, ConvertToRadians(90))
to rotate the bottom left and top left quarter rings 90 degrees
further to the bottom left position. CGAffineTransform.Rotate
takes
a transform and an angle as parameters then adds the given angle to
the transform’s rotation, essentially making the rotation here 180
degrees. Then in the fifth block we rotate the top left quarter around
to its final position, completing the gold ring. In the end it looks
as though the gold is simply filling in the gray ring. I’m sure there
are other ways to accomplish this, but for what the designers wanted
this worked out well for me.
void AnimateProcessing()
{
// AnimateKeyframes ( duration, delay, options, animations, completion )
UIView.AnimateKeyframes (2.1, 0, UIViewKeyframeAnimationOptions.CalculationModeLinear, () =>
{
// AddKeyframeWithRelativeStartTime ( start time, duration, animations )
// start time and duration 0-1 as percentage of wrapping AnimateKeyframes duration
UIView.AddKeyframeWithRelativeStartTime (0, 0.05, () =>
{
ProcessingImage.Alpha = 1f; // fade in gray background ring
});
UIView.AddKeyframeWithRelativeStartTime (0.15, 0.25, () =>
{
// fade in gold quarter rings
TopRightImage.Alpha = 1f;
BottomRightImage.Alpha = 1f;
BottomLeftImage.Alpha = 1f;
TopLeftImage.Alpha = 1f;
});
UIView.AddKeyframeWithRelativeStartTime (0.15, 0.35, () =>
{
CGAffineTransform transform = CGAffineTransform.MakeRotation (ConvertToRadians(90));
BottomRightImage.Transform = transform;
BottomLeftImage.Transform = transform;
TopLeftImage.Transform = transform;
});
UIView.AddKeyframeWithRelativeStartTime (0.35, 0.35, () =>
{
CGAffineTransform transform = CGAffineTransform.Rotate (TopLeftImage.Transform, ConvertToRadians(90));
BottomLeftImage.Transform = transform;
TopLeftImage.Transform = transform;
});
UIView.AddKeyframeWithRelativeStartTime (0.6, 0.35, () =>
{
CGAffineTransform transform = CGAffineTransform.Rotate (TopLeftImage.Transform, ConvertToRadians(90));
TopLeftImage.Transform = transform;
});
},
(finished) =>
{
if (creditCardChargeResult.Complete)
{
if(creditCardChargeResult.Success)
{
ProcessingImage.Image = UIImage.FromFile("Return/-g-icon-payment-progress-full.png");
TopRightImage.Alpha = 0f;
BottomRightImage.Alpha = 0f;
BottomLeftImage.Alpha = 0f;
TopLeftImage.Alpha = 0f;
AnimateProcessingOut();
}
else
{
// show error and go back
ShowDialog("Error","Could not charge Credit Card.\n Please verify information and try again");
NavigationController.PopViewController (true);
}
}
else
{
RestartProcessing();
}
});
}
nfloat ConvertToRadians(nfloat angle)
{
return (nfloat)((Math.PI / 180f) * angle);
}
When the above animation is complete, if we do not yet have a credit
card charge result we simply rerun the animation by calling
RestartProcessing
. In this method we first fade out the quarter
rings. When that animation completes, we reset the rotation transforms
back to their original values using CGAffineTransform.MakeIdentity()
then call AnimateProcessing
again.
void RestartProcessing()
{
// AnimateNotify ( duration, delay, options, animations, completion )
UIView.AnimateNotify (0.2, 0, UIViewAnimationOptions.CurveLinear, () =>
{
TopRightImage.Alpha = 0f;
BottomRightImage.Alpha = 0f;
BottomLeftImage.Alpha = 0f;
TopLeftImage.Alpha = 0f;
},
(finished) =>
{
BottomRightImage.Transform = CGAffineTransform.MakeIdentity();
BottomLeftImage.Transform = CGAffineTransform.MakeIdentity();
TopLeftImage.Transform = CGAffineTransform.MakeIdentity();
AnimateProcessing();
});
}
Our last transform animation is the scaling of the check mark shown
when the charge has been approved. In AnimateProcessing
above, if
our credit card charge is successful, we update our gray background
ring to use a full gold ring image then fade out all four of our
quarter rings and call AnimateProcessingOut
. This method sets us up
for the check mark to scale in with the bounce at the end and send us
off to our next screen. Here we are using
CGAffineTransform.MakeScale
which takes an x and y value that
specifies a percentage of the current size. We’ll animate the scale of
our now full gold ring down to 75%. On completion of that animation,
we’ll then swap our gold ring image for our check mark and call
AnimateCongrats
.
void AnimateProcessingOut()
{
// AnimateNotify ( duration, delay, options, animations, completion )
UIView.AnimateNotify (0.25, 0, UIViewAnimationOptions.CurveLinear, () =>
{
TitleLabel.Alpha = 0f;
CGAffineTransform transform = CGAffineTransform.MakeScale(0.75f, 0.75f);
ProcessingImage.Transform = transform;
},
(finished) =>
{
TitleLabel.Text = "Congrats, you're done!";
ProcessingImage.Image = UIImage.FromFile("Return/-g-icon-payment-congrats.png");
AnimateCongrats();
});
}
In AnimateCongrats
we’ll use AnimateNotify
with our spring damping
and velocity options. Here we want more of a bounce than with the
chevron animations, so we’re using 0.6 for damping and 50 for the
velocity. Then we just set our scale back to 1, and we’re good to go.
void AnimateCongrats()
{
// AnimateNotify ( duration, delay, spring damping, spring velocity, options, animations, completion )
UIView.AnimateNotify (0.25, 0, 0.6, 50.0, 0, () =>
{
CGAffineTransform transform = CGAffineTransform.MakeScale(1f, 1f);
ProcessingImage.Transform = transform;
}, null);
// AddKeyframeWithRelativeStartTime ( start time, duration, animations )
// start time and duration 0-1 as percentage of wrapping AnimateKeyframes duration
UIView.AnimateKeyframes (2, 0, UIViewKeyframeAnimationOptions.CalculationModeLinear, () =>
{
UIView.AddKeyframeWithRelativeStartTime (0, 0.125, () => {
TitleLabel.Alpha = 1;
});
},
(finished) =>
{
ShowExpertScreen();
});
}
Another thing to mention about the above is that I’m using a separate
AnimateKeyframes
block for the label. This is because we want the
spring animation for the check mark but not the label, so those
animations need to be separate. The reason behind using
AnimateKeyframes
with a nested AddKeyframeWithRelativeStartTime
block for just one simple animation is all about timing. We want to
hold for a second or two after the final animation is finished before
we go to the next screen. In AnimateKeyframes
we’re specifying an
overall duration of 2 seconds. In our
AddKeyframeWithRelativeStartTime
block we’re specifying a relative
duration of 0.125 which is 0.25 seconds. That leaves us 1.75 seconds
after the animation is finished before our completion gets called.
Animating with CABasicAnimation
Creating a CAGradientLayer
The animation in this comp is relatively simple. We have a label that
pops in over a gold/orange background, then it slides up into place
and the background fades out revealing the rest of the screen. The one
thing about this animation that makes it a little more interesting is
that the background is a gradient. We’re making this gradient in code,
so it’s a CAGradientLayer
instead of a UIImage
, which means it has
to be animated a bit differently. Let’s take a look at how we’re
creating the gradient.
gradient = new CAGradientLayer(); // class level CAGradientLayer gradient
gradient.Frame = UIScreen.MainScreen.Bounds;
gradient.Colors = new CGColor[]{ UIColor.Yellow.CGColor, UIColor.Orange.CGColor };
gradient.StartPoint = new CGPoint(0, 0);
gradient.EndPoint = new CGPoint(1, 1);
First we create our new CAGradientLayer
and set its Frame
to the
size of the screen. After that, we’re setting its Colors
with a
CGColor
array. The colors we specify in the array will be used in
the order given to create the gradient. Though it’s hard to see in the
comp, the gradient is running diagonal from the top left to the bottom
right corner. By Default, gradients run vertical from top to bottom.
To adjust the direction we need to specify a StartPoint
and
EndPoint
. These are both set as a CGPoint
with x and y values
between 0 and 1. Looking at our gradient.Frame
, CGPoint(0, 0)
is
the top left corner and CGPoint(1, 1)
is the bottom right corner. So
if we switch the EndPoint
to CGPoint(1, 0)
, which would be the top
right corner, then the gradient will run horizontal from left to
right. Changing our EndPoint
to CGPoint(1, 0.5)
will run the
gradient on a line from the top left corner to the middle of the right
side.
Another thing to mention about creating a gradient is the
Locations
property. Even though we’re not using it here, it’s nice to know.
Locations
lets us specify the gradient stop of each color in
Colors
with an NSNumber array. The value set for each gradient stop
must be between 0 and 1 and should be monotonically increasing. If
Locations
are not specified, the stops are spread uniformly across
the range.
// default stops for two colors
gradient.Locations = new NSNumber[]{ 0, 1 };
// default stops for three colors
gradient.Locations = new NSNumber[]{ 0, 0.5, 1};
Screen layout
Now that we have our gradient, let’s take a look at the layout. We’re
building this entire screen in code! In ViewDidLoad
we add our
UIScrollView
to the View
, then create and add our content views to
the UIScrollView
.
scrollView = new UIScrollView(UIScreen.MainScreen.Bounds); // class level UIScrollView scrollView
View.AddSubview(scrollView);
// create content views here
// add content views
scrollView.AddSubviews(topView, mainReturnTableView, bottomView);
After that we add the gradient and label on top. Since the gradient is
a CAGradientLayer
and not a UIView
, we must add it as such using
Layer.InsertSublayer()
. InsertSublayer()
takes two parameters: the
layer we want to add (gradient
), and the position we want that layer
added (scrollView.Subviews.Length
). Using 0 for the position will
place the layer at the bottom, below the previously added views. To
position the layer above the other views we’ll use Subviews.Length
.
Then we’ll add the label after the gradient.
scrollView.Layer.InsertSublayer(gradient, scrollView.Subviews.Length);
// create titleLabel here with other settings
titleLabel.Transform = CGAffineTransform.MakeScale(0.75f, 0.75f);
titleLabel.Alpha = 0;
scrollView.AddSubview(titleLabel);
Animating the label
With the screen set up we can move on to the animation. When we create
the titleLabel
we set its Alpha
to 0 and scale to 0.75 which is
its starting point in the animation. In ViewDidAppear
we call the
method that handles the animation. I’m going to break what this method
is doing down into smaller parts. Right now when the view becomes
visible we will only see the gradient filling the screen. The first
thing we need to do is animate the label in. To do this we’ll use
Animate
to fade it in and AnimateNotify
with the spring options,
discussed previously in part 1 of this post, for the scale.
// animate label in
UIView.Animate(0.1, ()=> { titleLabel.Alpha = 1; });
// AnimateNotify ( duration, delay, spring damping, spring velocity, options, animations, completion )
UIView.AnimateNotify(0.25, 0.25, 0.6f, 2.0f, UIViewAnimationOptions.CurveLinear, () =>
{
titleLabel.Transform = CGAffineTransform.MakeScale(1, 1);
titleLabel.Alpha = 1;
},
(finished) =>
{
// animate label up
});
When that finishes, we animate the label up to its permanent position.
// animate label up
UIView.AnimateNotify(0.5, 0.5, UIViewAnimationOptions.CurveEaseInOut, () =>
{
titleLabel.Frame = new CGRect(itemInset, 100, itemWidth, 60);
},
(completed) =>
{
// start gradient animation
});
Animating the CAGradientLayer
Here’s where things change. Once the label is in place, we want to
fade out our gradient so everything is visible. CAGradientLayer
happens to use Opacity
instead of Alpha
, but that’s not the big
difference. If we try to animate the Opacity
using one of the
UIView
animation methods it won’t work; it’ll skip right to our end
result. To animate a
CALayer,
which is the base class of CAGradientLayer
, we need to use
CABasicAnimation
.
To animate the Opacity
of our gradient, we need to first set up our
animation as follows:
// create opacity animation for gradient layer
CABasicAnimation gradientAnimation = CABasicAnimation.FromKeyPath ("opacity");
gradientAnimation.TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.EaseOut);
gradientAnimation.From = NSNumber.FromFloat(1f);
gradientAnimation.To = NSNumber.FromFloat(0f);
gradientAnimation.Duration = 0.25;
gradientAnimation.FillMode = CAFillMode.Forwards;
gradientAnimation.RemovedOnCompletion = false;
// on gradient animation stop, get scrollView content height and hide gradient
gradientAnimation.AnimationStopped += (sender, e) => {
GetContentHeight();
gradient.Hidden = true;
};
Above, we are creating our gradientAnimation
using
CABasicAnimation.FromKeyPath()
. FromKeyPath()
takes a string
specifying the property you are animating. Here’s a list of
CALayer Animatable Properties
from Apple. We then set the
CAMediaTimingFunction
to EaseOut
with CAMediaTimingFunction.FromName()
. Next we tell the
animation its starting value (From
an Opacity
of 1), ending value
(To
an Opacity
of 0) and Duration
(0.25 seconds). Then we set
the
CAFillMode
to Forwards
, so that the animation holds on its final value. Last
but not least, we set RemovedOnCompletion
to false
so the
animation is not removed on completion, because if it does get
removed, our Opacity
will go back to its start value before the
animation occurred. We can also set an AnimationStopped
event
listener. When our animation does stop we’ll get the content height of
scrollView
and hide the gradient since it’s no longer needed.
OK, now that we have the opacity animation created, we just have to
add it to our gradient when we’re ready. So let’s go back to the
completion block after the label animates up into place and start the
opacity animation for the gradient (gradientAnimation
). To start the
animation we’ll add it to the gradient using AddAnimation()
, which
takes an animation and a string. The string is used as an identifier
for the animation. You can use null
if you don’t need the
identifier.
// start gradient animation
gradient.AddAnimation(gradientAnimation, null);
Congrats, you’re done! I hope you enjoyed this post and that it’s given you a better understanding of animating for iOS in Xamarin. If you’d like to see these in action, feel free to download TaxChat and take a peek. You can run through the first few screens discussed in part one without having to create an account. Cheers!
Tags: technology csharp xamarin animation