Custom Rotating Animations in RubyMotion

UIView’s recommended block animation class methods make it easy to get stuff flying around your app. animateWithDuration is the workhorse that hides all the nitty gritty details and allows you to move on about your business. I’ve been working on a little RubyMotion app that requires a bit more customization than animateWithDuration provides.

Specifically, I needed:

  1. To spin a view a specific amount of rotations
  2. The ability to customize the timing of the animation
  3. Apply that custom timing to the entire duration of the animation

animateWithDuration doesn’t allow you to specify a custom time function outside of its predefined ones. It also makes rotation animations a bit tricky. It calculates the shortest distance to animate, so a 360 degree rotation wouldn’t actually show any movement. I could do 90 degree increments and recursively call itself in the completion callback, but then the timing function would only apply to each 90 degree animation and not the whole thing.

So I employed CABasicAnimation, a subclass of Core Animation, to get the job done for me. Let’s take a look:

Setup

You’ll need to make sure you include the QuartzCore framework in your application for this work properly.

I’ve got a Github repo fired up with everything together if you’d like to follow along: TxTSpinner

I’m going to setup an image view to use as an example and add a tap listener to fire to our animation method.

logo_image = UIImage.imageNamed('txt-logo')

@logo_view = UIImageView.new
@logo_view.userInteractionEnabled = true
@logo_view.image = logo_image
@logo_view.size = [logo_image.size.width, logo_image.size.height]
@logo_view.center = self.view.center
@logo_view.when_tapped { logo_view_was_tapped }

self.view.addSubview @logo_view

We add it to our view and we’re ready to roll…

Let’s Get Moving

So now that we’ve got an ImageView to play with, let’s build our animation to rotate the logo twice and add it to our view.

def logo_view_was_tapped
  rotateAnimation = CABasicAnimation.animationWithKeyPath("transform.rotation")
  rotateAnimation.toValue = Math::PI / 180 * 720
  rotateAnimation.duration = 2.0

  @logo_view.layer.addAnimation(rotateAnimation, forKey: :logo_spinning)
end

First, we initialize our CABasicAnimation with the property that we want to animate. Which, in our instance, is the transform rotation property.

The toValue assignment defines the final value we want the animation to rotate to. Rotation values need to be in radians (Formula: PI / 180 * degrees). The above will rotate it two full rotations.

Then we set the duration in seconds, add the animation to the view layer, and we’re good to go.

If you rake that, you should see something similar to below:

Linear Spin Timing

Success! Our logo spins around twice, but the linear timing makes it feel very flat. Let’s give it a little more zest with a custom timing function.

Make it Sexy

We can define a cubic Bezier curve using CAMediaTimingFunction’s functionWithControlPoints:::: class method. I found this little tool that generates the control points for the curve you define.

RubyMotion has an issue calling Objective-C methods with nameless parameters. They’re working on it, but in the meantime we can use Ruby’s send method to get the job done.

Let’s update our method to look like this:

def logo_view_was_tapped
  timing = CAMediaTimingFunction.send('functionWithControlPoints::::', 0.28, -0.3, 0.12, 1.0)

  rotateAnimation = CABasicAnimation.animationWithKeyPath("transform.rotation")
  rotateAnimation.toValue = Math::PI / 180 * 720
  rotateAnimation.duration = 2.0
  rotateAnimation.timingFunction = timing

  @logo_view.layer.addAnimation(rotateAnimation, forKey: :logo_spinning)
end

We initialize our timing function up top and set the animation’s timingFunction property to that instance. If you rake that, you’ll see the logo do a little windup, max its speed and slow down.

Bezier Spin Timing

That’s looking pretty good! The custom timing gives the animation a little more life.

Wrapup

This is a relatively simple example. Core Animation has tons of things that you can customize to get it just the way you like. It’s a little more work than animateWithDuration, but the customization opens up the opportunity to give your animations more life and provide a rich user experience.

Note: I’m using the BubbleWrap gem. Besides the handy helpers and goodies they provide, they’ve got something going on that is allowing the send method to work properly when calling functionWithControlPoints. If you don’t want to use the BubbleWrap gem, you’ll need to use the objc_send method mentioned in the HipByte ticket.

comments powered by Disqus