Categories
Manuals

Youtube play button animation using POP

In this tutorial we will create a Youtube play button animation using Facebook POP. The source code will be shared on Github, it also includes an alternative way of doing this animation using CoreAnimation.

Prerequisites / Requirements

  1. CocoaPods. If you are not already familiar with pods I would recommend reading this tutorial or at least CocoaPods Getting Started;
  2. Xcode 7 with Swift 2.0.

To begin with, we need to create a new Single View Application project (File -> New -> Project) and add POP pod by placing this line in Podfile:

pod 'pop'  

POP still does not have Swift support, so we have to create a bridging header which allows us to use both Swift and Objective-C in the same project:

  1. Create header file (File -> New -> File -> Source -> Header File) and name it BridgingHeader;
  2. Go to target build settings and search for Objective-C Bridging Header;
  3. Provide path to your bridging header. It should be ProjectName/BridgingHeader.h

Import Facebook POP to your project by adding this line to BridgingHeader.h:

#import "POP.h"

Build the project to make sure that the bridging header works properly and there are no warnings. It may fail because of enabled bitcode, as POP does not support it yet. To fix this error, you have to disable bitcode by going to your Target -> Build Settings -> Search for “bitcode”:


First, create a subclass of UIButton and name it PlayButton.

As you have seen, this button will animate between two states: Paused and Playing. We need to define them using enum. Copy and paste this code above the PlayButton class declaration:

enum PlayButtonState {  
  case Paused
  case Playing

  var value: CGFloat {
    return (self == .Paused) ? 1.0 : 0.0
  }
}

The reason why I added var value: CGFloat will be described later.

Now I would like to go through the theory of how the button will animate between these two states.

POP allows us to create animatable property — and that is exactly what we are going to do. We will create a CGFloat variable named animationValue and will animate it from 1.0 to 0.0 when the button state changes from Paused to Playing, and animate from 0.0 to 1.0 when the button state changes from Playing to Paused. Every time the value has changed we will call setNeedsDisplay which will force our view to redraw.

Lets declare some variables! Put this code at the beginning of the class declaration:

// MARK: -
// MARK: Vars
private(set) var buttonState = PlayButtonState.Paused  
private var animationValue: CGFloat = 1.0 {  
  didSet {
    setNeedsDisplay()
  }
}

Now we have two variables:

  • buttonState – defines button state using enumeration defined above. The value can be accessed outside the class, but it can be set only inside the class;
  • animationValue – defines current animation value which will help us to draw the path. On every value change it calls setNeedsDisplay.

The next step is to create a method responsible for setting up animation or only updating animationValue when animation is not needed (f.e. when cell is reused, you probably will not want to animate content on scroll).

// MARK: -
// MARK: Methods
func setButtonState(buttonState: PlayButtonState, animated: Bool) {  
  // 1
  if self.buttonState == buttonState {
    return
  }
  self.buttonState = buttonState

  // 2
  if pop_animationForKey("animationValue") != nil {
    pop_removeAnimationForKey("animationValue")
  }

  // 3
  let toValue: CGFloat = buttonState.value

  // 4
  if animated {
    let animation: POPBasicAnimation = POPBasicAnimation()
    if let property = POPAnimatableProperty.propertyWithName("animationValue", initializer: { (prop: POPMutableAnimatableProperty!) -> Void in
      prop.readBlock = { (object: AnyObject!, values: UnsafeMutablePointer<CGFloat>) -> Void in
        if let button = object as? PlayButton {
          values[0] = button.animationValue
        }
      }
      prop.writeBlock = { (object: AnyObject!, values: UnsafePointer<CGFloat>) -> Void in
        if let button = object as? PlayButton {
          button.animationValue = values[0]
        }
      }
      prop.threshold = 0.01
    }) as? POPAnimatableProperty {
      animation.property = property
    }
    animation.fromValue = NSNumber(float: Float(self.animationValue))
    animation.toValue = NSNumber(float: Float(toValue))
    animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
    animation.duration = 0.25
    pop_addAnimation(animation, forKey: "percentage")
  } else {
    animationValue = toValue
  }
}
  1. We update buttonState and proceed further only if it has changed;
  2. We remove previous animation if it exists;
  3. We create immutable variable toValue and set value of the buttonState which is calculated in var value: CGFloat (swift allows us to have very clean and nice implementation of vars in enumerations);
  4. If animation is required, then we initialise POPBasicAnimation with POPAnimatableProperty. Otherwise we only update animation value.

Lets understand how POPBasicAnimation is initialised in our example. FromValue, toValue, timingFunction, duration — these variables do not require explanation, right? The only complex part of POPBasicAnimation is POPAnimatableProperty. As we animate our own property, we have to initialise our own animation block which contains:

  1. readBlock — reads values from a property and stores in an array of floats CGFloat values[];
  2. writeBlock — writes values from array of floats into property;
  3. threshold — defines the smallest increment of the value.
    More information can be found here.

Animating built-in properties is easier. If we would like to animate alpha we could just use POPAnimatableProperty.propertyWithName(kPOPViewAlpha). A list of built-in properties can be found here.

Now we have everything prepared for animation I would like to explain the logic of animation, so we understand what to write in drawRect.

Assume that we are going to animate from Paused to Playing (1 to 3). Our “triangle” button will split in two halves — trapeziums (2), and will animate them to rectangles. The easiest way to achieve this effect is to always have 2 geometrical figures consisting of four points:

The next step is to understand what should be calculated. I have prepared a special image which describes all values very well:

  1. minWidth — this value defines the minimum width of left and right halves;
  2. aWidth — this value is calculated using animationValue. When animationValue = 1, this value is zero. When animationValue = 0, this value is equal to half of button width minus minimum width (minWidth);
  3. width — equals to minimum width plus additional width (minWidth + aWidth);
  4. H1 — padding from top to the left half top right point and the right half top left point, AND padding from bottom to the left half bottom right point and the right half bottom left point. When animationValue = 0, this value will be equal to height / 4. When animationValue = 1, this value will be equal 0;
  5. H2 — padding from top to the right half top right point AND padding from bottom to the right half bottom right point. When animationValue = 0, this value will be equal to height / 2. When animation value = 1, this value will be equal 0.

Okay, I think it is enough theory and good time to override drawRect. If you did not understand something — please do not be scared, you will understand it once we override drawRect. Copy and paste this code in PlayButton:

// MARK: -
// MARK: Draw
override func drawRect(rect: CGRect) {  
  super.drawRect(rect)

  // 1
  let height = rect.height
  let minWidth = rect.width * 0.32
  let aWidth = (rect.width / 2.0 - minWidth) * animationValue
  let width = minWidth + aWidth
  let h1 = height / 4.0 * animationValue
  let h2 = height / 2.0 * animationValue

  // 2
  let context = UIGraphicsGetCurrentContext()

  // 3
  CGContextMoveToPoint(context, 0.0, 0.0)
  CGContextAddLineToPoint(context, width, h1)
  CGContextAddLineToPoint(context, width, height - h1)
  CGContextAddLineToPoint(context, 0.0, height)
  CGContextMoveToPoint(context, rect.width - width, h1)
  CGContextAddLineToPoint(context, rect.width, h2)
  CGContextAddLineToPoint(context, rect.width, height - h2)
  CGContextAddLineToPoint(context, rect.width - width, height - h1)

  // 4
  CGContextSetFillColorWithColor(context, tintColor.CGColor)
  CGContextFillPath(context)
}

This code is straightforward:

  1. We calculate all values as per image above. As I mentioned before, minWidth value is always the same, despite the fact that animationValue changes. I like when it is equal to 32 percent of the whole width. You can adjust this value as you want, but keep in mind, that it should be less or equal to half of width;
  2. As we are overriding drawRect, we should not to create a new context — we should get and use existing;
  3. Create CGPath of two trapezius, as it was described above;
  4. Set fill colour (I am using tintColor, feel free to change it).

The last step is to add PlayButton to our ViewController and test out animations. Delete everything inside ViewController class and paste this:

// MARK: -
// MARK: Vars

private var playButton: PlayButton!

// MARK: -

override func viewDidLoad() {  
  super.viewDidLoad()

  playButton = PlayButton()
  playButton.frame = CGRect(x: floor((view.bounds.width - 150.0) / 2.0), y: 50.0, width: 150.0, height: 150.0)
  playButton.addTarget(self, action: Selector("playButtonPressed"), forControlEvents: .TouchUpInside)
  view.addSubview(playButton)
}

// MARK: -
// MARK: Methods

func playButtonPressed() {  
  if playButton.buttonState == .Playing {
    playButton.setButtonState(.Paused, animated: true)
  } else {
    playButton.setButtonState(.Playing, animated: true)
  }
}

Now build your project and enjoy! This animation is unbreakable — you can tap the button as quick as possible and it will always smoothly animate between two states without any visual breaks. Pretty good, isn’t it?


If you are excited about drawRect animations, please let me know, I will write the second part of this tutorial with two more nice animations!

All source code from this tutorial PLUS CoreAnimation implementation can be found here.

Thank you for reading, I hope you enjoyed it! Please let me know which animation you would use in your project — drawRect, or CA. Cannot wait to read your comments!

Leave a Reply

Your email address will not be published. Required fields are marked *