5

I have an animation of drawing a rectangle to a specific percentage of it.

For that i am drawing a CAShapeLayer:

func drawTheLayer() -> CAShapeLayer {

    let lineWidth: CGFloat = borderWidth * bounds.size.width / standardSizeWidth
    let cornerRadiusResized: CGFloat = cornerRadiusRanged * bounds.size.width / standardSizeWidth
    let insetRect = CGRectInset(bounds, lineWidth/2.0, lineWidth/2.0)
    let apath = ShapeDraw.createRoundedCornerPath(insetRect, cornerRadius: cornerRadiusResized, percent: percentageRanged)
    let apathLayer = CAShapeLayer()

    apathLayer.frame = bounds
    apathLayer.bounds = insetRect
    apathLayer.path = apath
    apathLayer.strokeColor = AppColor.OnColor.CGColor
    apathLayer.fillColor = nil
    apathLayer.lineWidth = lineWidth
    apathLayer.lineJoin = kCALineJoinRound
    apathLayer.lineCap = kCALineCapRound
    apathLayer.geometryFlipped = true

    let flipTransform = CGAffineTransformMakeScale(1, -1)

    apathLayer.setAffineTransform(flipTransform)
    return apathLayer

}

To animate the drawing:

func animateToLevel() {
    if percentage <= 0 {
        return
    }

    pathLayer?.removeFromSuperlayer()
    pathLayer?.removeAllAnimations()

    animating = true
    let apathLayer = drawTheLayer()
    layer.addSublayer(apathLayer)
    let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
    pathAnimation.delegate = self
    pathAnimation.duration = 0.5
    pathAnimation.fromValue = 0.0
    pathAnimation.setValue("levelAnimation", forKey: "animationId")
    apathLayer.addAnimation(pathAnimation, forKey: nil)

    pathLayer = apathLayer
}

Irrelevant to this animation, there is another animation which can happen. Scaling of the rectangle's superView. Problem occurs when i start drawing the path is drawn according to the small sized superView and then when the frame becomes bigger the drawing animation's path remains same which is expected but is there a logical not hacky solution for this? Or do i have to do it in drawRect

For superview the animation is changing the UILayout height constant in an UIView animation:

heightOfContainerConstraint.constant = 100 // or 400 when it is expanded
UIView.animateWithDuration(animated ? animationDuration : 0) {
    self.view.layoutIfNeeded()
}

Code of creating a rounded corner:

import UIKit

class ShapeDraw {

static func createRoundedCornerPath(rect: CGRect, cornerRadius: CGFloat, percent: CGFloat) -> CGMutablePathRef {

    let piNumber: CGFloat = CGFloat(M_PI)

    // get the 4 corners of the rect
    let topLeft = CGPointMake(rect.origin.x, rect.origin.y)
    let topRight = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y)
    let bottomRight = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y + rect.size.height)
    let bottomLeft = CGPointMake(rect.origin.x, rect.origin.y + rect.size.height)

    // Set 4 corner arc starting angles
    let startAngleTopRight: CGFloat = 3 * piNumber/2
    let startAngleBottomRight: CGFloat = 0
    let startAngleBottomLeft: CGFloat = piNumber / 2
    let startAngleTopLeft: CGFloat = piNumber

    let slices = (CGRectGetWidth(rect) / cornerRadius) * 4
    let partValue: CGFloat = 100 / slices // %100 is total -> 1 piece is 100/16 percent

    let wayToGoLine = CGRectGetWidth(rect) - 2 * cornerRadius
    let linePartValue = partValue * (slices/4 - 2)

    var remainingPercent: CGFloat = percent
    let path = CGPathCreateMutable()
    // move to top left
    CGPathMoveToPoint(path, nil, topRight.x/2, topRight.y)

    // add top right half line
    remainingPercent = addLine(path, x: topRight.x/2 + (wayToGoLine/2 * getConstantForThis(remainingPercent, partValue: linePartValue/2)), y: topRight.y, remainingPercent: remainingPercent, currentPartPercent: linePartValue/2)

    // add top right curve
    let endingAngleTopRight = endingAngleForThis(startAngleTopRight, remainingPercent: remainingPercent, partValue: partValue)
    remainingPercent =  addArc(path, x: topRight.x - cornerRadius, y: topRight.y + cornerRadius, radius: cornerRadius, startAngle: startAngleTopRight, endingAngle: endingAngleTopRight, remainingPercent: remainingPercent, currentPartPercent: partValue * 2)

    // add right line
    remainingPercent = addLine(path, x: bottomRight.x, y: topRight.y + cornerRadius + (wayToGoLine * getConstantForThis(remainingPercent, partValue: linePartValue)), remainingPercent: remainingPercent, currentPartPercent: linePartValue)

    // add bottom right curve
    let endingAngleBottomRight = endingAngleForThis(startAngleBottomRight, remainingPercent: remainingPercent, partValue: partValue)
    remainingPercent = addArc(path, x: bottomRight.x - cornerRadius, y: bottomRight.y - cornerRadius, radius: cornerRadius, startAngle: startAngleBottomRight, endingAngle: endingAngleBottomRight, remainingPercent: remainingPercent, currentPartPercent: partValue * 2)

    // add bottom line
    remainingPercent = addLine(path, x: bottomRight.x - cornerRadius - (wayToGoLine * getConstantForThis(remainingPercent, partValue: linePartValue)), y: bottomLeft.y, remainingPercent: remainingPercent, currentPartPercent: linePartValue)

    // add bottom left curve
    let endingAngleBottomLeft = endingAngleForThis(startAngleBottomLeft, remainingPercent: remainingPercent, partValue: partValue)
    remainingPercent = addArc(path, x: bottomLeft.x + cornerRadius, y: bottomLeft.y - cornerRadius, radius: cornerRadius, startAngle: startAngleBottomLeft, endingAngle: endingAngleBottomLeft, remainingPercent: remainingPercent, currentPartPercent: partValue * 2)

    // add left line
    remainingPercent = addLine(path, x: topLeft.x, y: bottomLeft.y - cornerRadius - (wayToGoLine * getConstantForThis(remainingPercent, partValue: linePartValue)), remainingPercent: remainingPercent, currentPartPercent: linePartValue)

    // add top left curve
    let endingAngleTopLeft = endingAngleForThis(startAngleTopLeft, remainingPercent: remainingPercent, partValue: partValue)
    remainingPercent = addArc(path, x: topLeft.x + cornerRadius, y: topLeft.y + cornerRadius, radius: cornerRadius, startAngle: startAngleTopLeft, endingAngle: endingAngleTopLeft, remainingPercent: remainingPercent, currentPartPercent: partValue * 2)

    // add top left half line
    remainingPercent = addLine(path, x: topLeft.x + cornerRadius + (wayToGoLine/2 * getConstantForThis(remainingPercent, partValue: linePartValue/2)), y: topRight.y, remainingPercent: remainingPercent, currentPartPercent: linePartValue/2)

    return path
}

static func endingAngleForThis(startAngle: CGFloat, remainingPercent: CGFloat, partValue: CGFloat) -> CGFloat {
    return startAngle + (CGFloat(M_PI) * getConstantForThis(remainingPercent, partValue: partValue * 2) / 2)
}

static func getConstantForThis(percent: CGFloat, partValue: CGFloat) -> CGFloat {
    let percentConstant = percent - partValue > 0 ? 1 : percent / partValue
    return percentConstant
}

static func addLine(path: CGMutablePath?, x: CGFloat, y: CGFloat, remainingPercent: CGFloat, currentPartPercent: CGFloat) -> CGFloat {
    if remainingPercent > 0 {
        CGPathAddLineToPoint(path, nil, x, y)
        return remainingPercent - currentPartPercent
    }
    return 0
}

static func addArc(path: CGMutablePath?, x: CGFloat, y: CGFloat, radius: CGFloat, startAngle: CGFloat, endingAngle: CGFloat, remainingPercent: CGFloat, currentPartPercent: CGFloat) -> CGFloat {
    if remainingPercent > 0 {

        CGPathAddArc(path, nil, x, y, radius, startAngle, endingAngle, false)
        return remainingPercent - currentPartPercent
    }
    return 0
}

}
Mihriban Minaz
  • 3,043
  • 2
  • 32
  • 52

1 Answers1

1

Essentially, we have two animations:

  • follow the rectangle path to a specific percentage
  • scaling of the rectangle superview or the animation of bounds that change

The objective is: these animations must work together in the same time (a group), not sequentially.

The code below is just an example and don't follow the exact properties or custom objectives, I want to explain what I would do in this case:

    // follow the rectangle path
    let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
    let cornerRadiusResized: CGFloat = cornerRadiusRanged * bounds.size.width / standardSizeWidth
    let apath = ShapeDraw.createRoundedCornerPath(insetRect, cornerRadius: cornerRadiusResized, percent: percentageRanged)
    pathAnimation.toValue = apath

    // scaling of the rectangle superview
    let newBounds = CGRectMake(self.view.bounds.origin.x, self.view.bounds.origin.y, self.view.bounds.width, self.view.bounds.height)
    let boundsAnimation = CABasicAnimation(keyPath: "bounds")
    boundsAnimation.toValue = NSValue(CGRect:newBounds)

    // The group
    var theGroup: CAAnimationGroup = CAAnimationGroup()
    theGroup.animations = [pathAnimation,boundsAnimation]
    theGroup.duration = 2.0
    theGroup.repeatCount = 1
    theGroup.fillMode = kCAFillModeForwards
    apathLayer.addAnimation(theGroup, forKey: "theGroup")

EDIT:

If you need a third animation, as you speak in your comments, to change UIButton dimension you can also add:

var buttonAnimation:CABasicAnimation = CABasicAnimation(keyPath: "transform.scale")
pulseAnimation.duration = 2.0
pulseAnimation.toValue = NSNumber(float: 0.5)
pulseAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)

and add it in the array :

theGroup.animations = [pathAnimation,buttonAnimation, boundsAnimation]
Alessandro Ornano
  • 34,887
  • 11
  • 106
  • 133
  • I tried it before but the path i create with createRoundedCornerPath does not scale while view is scaling. When i use dispatch_after(delayTime, dispatch_get_main_queue()) { drawing animation starts after scaling. – Mihriban Minaz Jun 09 '16 at 11:36
  • Both animations must run in main queue. They run sequentially or is it just a problem of duration (different NSTimeInterval) ? Have you tried to change both duration parameters, for example pathAnimation more long than boundAnimation? – Alessandro Ornano Jun 09 '16 at 11:58
  • actually there are 3 animations 1. drawing a path inside button, 2. scaling of the button 3. scaling of container View. and if i set animation main queue first 3 is working than 1&2 of course works at the same time since it is a group animation. they all work same duration, i already checked that 10.0 seconds duration for all the animations to see better. – Mihriban Minaz Jun 09 '16 at 12:01
  • What about your situation? Do you solve the synchronicity between the three animations? – Alessandro Ornano Jun 13 '16 at 08:56
  • Unfortunately no. Because the view's constraint change animation and button's drawing a line animation can not be processed at the same time. It draws the line according to the already scaled size of button which cause the problem in the gif. I tried creating a custom Bezier path to change path's bounds when the view's bounds change. but didnt work also :/ – Mihriban Minaz Jun 13 '16 at 09:06
  • According to the official apple docs https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html#//apple_ref/doc/uid/TP40004514-CH8-SW1 you can also try to change perspective to your animations (CATransform3DIdentity) instead of scale. Think about it, it can be a valid solution. – Alessandro Ornano Jun 13 '16 at 09:27
  • I tried that one also. But the problem is when the animation starts i create a path and add it to shapelayer and animate it.. When the view scales, path changes also. And for ex. normally it know that it should go 20 pixel and then draw an arc and then go down... so scaling the bezierpath during the animation is not possible. For that i have to customize bezierpath.. i tried that also and it didnt work. – Mihriban Minaz Jun 13 '16 at 09:29
  • In this case, I think you must customize the scale animation: during your view scale, you must recalculate cgpath elements like this post http://stackoverflow.com/a/20322817/1894067 – Alessandro Ornano Jun 13 '16 at 09:44
  • I checked that one already. didnt work. My problem is what i need is not just scaling a path. Because i have an animation of drawing that path. So scaling a path is not a problem but when and how are my problems. I have to scale it when superview scales. According to that i customize shapelayer, and add willSet/didSet functions to redraw the path whenever bounds are changing. – Mihriban Minaz Jun 13 '16 at 09:45
  • If you have a GIST, a downloadable example to reproduce this part of code I can try to give you a hand – Alessandro Ornano Jun 13 '16 at 09:49
  • I will create a separate project when i have time. thanks a lot for helping. I will set the bounty again. – Mihriban Minaz Jun 14 '16 at 14:35
  • Thank you too. When you have your project in gist please link me here so I'll can see it. Good luck. – Alessandro Ornano Jun 15 '16 at 13:29