iOS Animations in Xamarin - part 2

« Return to Our Notebook

iOS Animations in Xamarin - part 2

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!

We solve problems with technology. What can we solve for you?

Reach Out

t: 800.646.0188