Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CABasicAnimation drawing a path does not scale when superView's scale changes

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
}

}
like image 380
Mihriban Minaz Avatar asked Nov 08 '22 14:11

Mihriban Minaz


1 Answers

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]
like image 155
Alessandro Ornano Avatar answered Nov 15 '22 06:11

Alessandro Ornano