Elastic interactive transition using UIBezierPath

It’s been a while and after some long due time of hard work I’m finally back with a new iOS post.

I want to explain you how I made my FlowingMenu interactive transition library by showing you some implementation details. I suppose that you are already familiar with the transition and more specifically the interactive transitions.

This gif shows a preview of the elastic/bouncing effect I’m going to explain you with this post:

Flowing Menu in action

First of all,  the idea of a flowing menu came from this Android lib: FlowingDrawer. Then when I tried to reproduce it to iOS, i stumbled upon this great tutorial of Danil Gontovnik which helped me to start the adventure.

Custom interactive transitions

Before to start, here the steps to follow to create a custom and interactive transition:

  1. Creating a UIViewControllerTransitioningDelegate. Its main role is returning an object responsible for the animation.
  2. Creating that object. It has to implement the UIViewControllerAnimatedTransitioning protocol and especially the animationTransition method.
  3. Creating a UIViewControllerInteractiveTransitioning which drives the interactive transition. Think to define it in the UIViewControllerTransitioningDelegate.

Understand the logic

Like with the DGElasticPullToRefresh, when the interactive transition begins we create a CAShapeLayer with an UIBezierPath. The bezier path drawn the “elastic” shape and is drawn thanks to control points. The control points are views which follow the finger when it moves on the screen. The pan gesture also help us to determine the transition progress and to update it. Here is an example of how it would look if I made all control points views red:

Flowing Menu - Control Points

When the finger is released we determine whether the transition is cancelled. If it is the case we update the control points to the most left edge position, in other case we update them to the most right position. Then we start an UIView spring animation which will move all our control point views to their destination location with a nice bounce. While views are being animated we need to somehow update our bezier path. For this purpose we are going to use CADisplayLink, which will run on a main run loop and will call required function once per frame.

Introspection

Now we are looking for the main parts of the code which are responsible of the flowing effect.

Lets begin with the shape and control point definitions:

/// 1: Control views aims to build the elastic shape.
var controlViews = (0 ..< 8).map { _ in UIView() }
/// Shaper layer used to draw the elastic view.
var shapeLayer = CAShapeLayer()
/// 2: Mask to used to create the bubble effect.
var shapeMaskLayer = CAShapeLayer()
/// 3: The display link used to create the bouncing effect.
lazy var displayLink: CADisplayLink = {
  let displayLink    = CADisplayLink(target: self, selector: Selector("updateShapeLayer"))
  displayLink.paused = true
  displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)

  return displayLink
}()
/// Flag to pause/run the display link.
var animating = false {
  didSet {
    displayLink.paused = !animating
  }
}

What we did here was:

  1. Declared a shapeLayer variable – the layer, which will be used to display bezier path.
  2. Declared another layer which will help us to create the bubble effect.
  3. Declared the display link for the animation.
  4. The animating variable will pause/unpause displayLink.

Now we are updating the animateTransition method to create the shape layer when the transition start in the interactive mode:

if interactive {
  // Last control points help us to know the menu height
  controlViews[7].center = CGPoint(x: 0, y: menuView.bounds.height)

  // Be sure there is no animation running
  shapeMaskLayer.removeAllAnimations()

  // Retrieve the shape color
  let shapeColor = source.colorOfElasticShapeInFlowingMenu(self) ?? menuView.backgroundColor ?? .blackColor()
  shapeMaskLayer.path        = UIBezierPath(rect: ov.bounds).CGPath
  shapeLayer.actions         = ["position" : NSNull(), "bounds" : NSNull(), "path" : NSNull()]
  shapeLayer.backgroundColor = shapeColor.CGColor
  shapeLayer.fillColor       = shapeColor.CGColor

  // Add the mask to create the bubble effect
  shapeLayer.mask = shapeMaskLayer

  // Add the shape layer to container view
  containerView.layer.addSublayer(shapeLayer)

  // If the container view change, we update the control points parent
  for view in controlViews {
    view.removeFromSuperview()
    containerView.addSubview(view)
  }
}

Here we basically prepare the shape layer and the control points to display them in the current container view.

Now we need to update the control points while the finger moves. So we need to track the touch location in the gesture callback:

func panToPresentAction(panGesture: UIScreenEdgePanGestureRecognizer) {
  let view        = panGesture.view!
  let translation = panGesture.translationInView(view)
  let menuWidth   = (delegate ?? self).flowingMenu(self, widthOfMenuView: view)

  let yLocation  = panGesture.locationInView(panGesture.view).y
  let percentage = min(max(translation.x / (menuWidth / 2), 0), 1)

  switch panGesture.state {
  case .Began:
    interactive = true

    // Asking the delegate the present the menu
    delegate?.flowingMenuNeedsPresentMenu(self)

    fallthrough
  case .Changed:
    updateInteractiveTransition(percentage)

    let waveWidth = translation.x * 0.9
    let left      = waveWidth * 0.1

    // Update the control points
    moveControlViewsToPoint(CGPoint(x: left, y: yLocation), waveWidth: waveWidth)

    // Update the shape layer
    updateShapeLayer()
  default:
    animating = true

    if percentage < 1 {
      moveControlViewsToPoint(CGPoint(x: 0, y: yLocation), waveWidth: 0)
        
      cancelInteractiveTransition()
    }
    else {
      finishInteractiveTransition()
    }
  }
}

To make simple, here we simply track the touch location, update the control points and to finish it redraws the shape layer. When the finger untouched the screen is finished we either cancel or finish the transition. We set the animating flag to true to enable the display link to animate the shape transformation.

Now it only remains one thing: complete the animation when the finger release the screen. The animation is made inside the animateTransition after the interactive one is completed:

if self.interactive && !status.transitionWasCancelled() {
  self.interactive = false

  let bubbleAnim                 = CAKeyframeAnimation(keyPath: "path")
  bubbleAnim.values              = [beginRect, middleRect, endRect].map { UIBezierPath(ovalInRect: $0).CGPath }
  bubbleAnim.keyTimes            = [0, 0.4, 1]
  bubbleAnim.duration            = duration
  bubbleAnim.removedOnCompletion = false
  bubbleAnim.fillMode            = kCAFillModeForwards
  maskLayer.addAnimation(bubbleAnim, forKey: "bubbleAnim")

  let anim                 = CAKeyframeAnimation(keyPath: "path")
  anim.values              = [beginPath, middlePath, endPath].map { $0.CGPath }
  anim.keyTimes            = [0, 0.4, 1]
  anim.duration            = duration
  anim.removedOnCompletion = false
  anim.fillMode            = kCAFillModeForwards
  self.shapeMaskLayer.addAnimation(anim, forKey: "bubbleAnim")

  UIView.animateWithDuration(duration, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0, options: [], animations: {
     for view in self.controlViews {
        view.center.x = menuWidth
     }
     }, completion: { _ in
        self.shapeLayer.removeFromSuperlayer()

        menuView.layer.mask = nil
        self.animating      = false

        completion()
        })
     }
     else {
     menuView.layer.mask = nil
     self.animating      = false

     completion()
  }
}

When the interaction is finished we check whether the it was an interactive one, and if so we complete the animation by adding the bubble animation and update the position of the control points.

Conclusion

I hope you enjoyed this post. Please leave your comments and suggestions.

Source code of FlowingMenu can be found here

1 Star2 Stars3 Stars4 Stars5 Stars (3 votes, average: 5.00 out of 5)
Loading...

One comment

Time limit is exhausted. Please reload CAPTCHA.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

  1. khushboo · December 13, 2015

    Thank you @yannick