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
}
}
Essentially, we have two animations:
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]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With