Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to animate a fill color change on UIBezierPath?

Tags:

The shape is filled in correctly to begin with but I can't figure out how to change the fill color or, even better, how to animate a fill color change on a UIBezierPath. Just like changing the background color of a UIView is what I'm looking for.

var fillColor = UIColor()

func changeBackgroundColor() {

    let animcolor = CABasicAnimation(keyPath: "fillColor")
    animcolor.fromValue = UIColor.greenColor()
    animcolor.toValue = UIColor.orangeColor()
    animcolor.duration = 1.0;
    animcolor.repeatCount = 0;
    animcolor.autoreverses = true
    shapeLayer.addAnimation(animcolor, forKey: "fillColor")

}


var fillColor = UIColor()
let clipPath = UIBezierPath()
let shapeLayer = CAShapeLayer()

override func drawRect(rect: CGRect) {

        clipPath.moveToPoint(CGPointMake(self.bounds.minX + 7.65, self.bounds.minY - 0.25))
        clipPath.addCurveToPoint(CGPointMake(self.bounds.minX + 7.65, self.bounds.minY + 36.1), controlPoint1: CGPointMake(self.bounds.minX - 2.38, self.bounds.minY + 9.79), controlPoint2: CGPointMake(self.bounds.minX - 2.38, self.bounds.minY + 26.06))
        clipPath.addCurveToPoint(CGPointMake(self.bounds.minX + 43.99, self.bounds.minY + 36.1), controlPoint1: CGPointMake(self.bounds.minX + 17.69, self.bounds.minY + 46.13), controlPoint2: CGPointMake(self.bounds.minX + 33.96, self.bounds.minY + 46.13))
        clipPath.addLineToPoint(CGPointMake(self.bounds.minX + 43.99, self.bounds.minY + 36.1))
        clipPath.addLineToPoint(CGPointMake(self.bounds.minX + 44.01, self.bounds.minY + 0.19))
        clipPath.addLineToPoint(CGPointMake(self.bounds.minX + 7.58, self.bounds.minY + 0.19))
        clipPath.usesEvenOddFillRule = true
        fillColor = userColor
    }

clipPath.addClip()
fillColor.setFill()
clipPath.fill()
shapeLayer.path = clipPath.CGPath
self.layer.mask = shapeLayer
like image 832
Phil Andrews Avatar asked Apr 10 '16 20:04

Phil Andrews


1 Answers

Just like changing the background color of a UIView is what I'm looking for.

But why can't you do exactly that? You're already masking the view to your path – so animating your background color would achieve the exact effect you want. Just get rid of the Core Graphics path filling – and also remove the drawRect override, as you can't use drawRect with an animation of the background.

If you really need to use drawRect and animate the color, you could consider splitting up your UIView into two UIViews - one to draw the background layer (which you could animate), and one to draw the custom drawing. Alternatively, you could setup a CADisplayLink to re-draw the view every frame with an intermediate background color – but that's not amazing in terms of performance.

As I say below, you can't animate the fill color of a UIBezierPath directly. If you only want to change (instead of animate) the fill color with your existing code, then you'd just want to call setNeedsDisplay with a different userColor.

If you're asking this because you're using two different paths for the masking and filling, see my below (overcomplicated in hindsight) answer.


The problem is the fill color of a UIBezierPath isn't animatable. But the fill color on a CAShapeLayer is.

Therefore you want to use another CAShapeLayer for the filling instead. I suggest you create a fillLayer and a maskLayer property in order to make their functions clear. Now because you're using a 100% layered approach, you can move your code out of the drawRect, as you're no longer doing any Core Graphics drawing.

Also, it's worth noting that you need to use a CGColor for the animation to and from values.

Something like this should do the trick:

var fillColor = UIColor.greenColor()
let maskLayer = CAShapeLayer()
let fillLayer = CAShapeLayer()

func changeBackgroundColor() {

    let animcolor = CABasicAnimation(keyPath: "fillColor")
    animcolor.fromValue = UIColor.greenColor().CGColor
    animcolor.toValue = UIColor.orangeColor().CGColor
    animcolor.duration = 1.0;
    animcolor.repeatCount = 0;
    animcolor.autoreverses = true
    fillLayer.addAnimation(animcolor, forKey: "fillColor")

}

// re-adjust the clipping path when the view bounds changes
override func layoutSubviews() {

    let clipPath = UIBezierPath()
    clipPath.moveToPoint(CGPointMake(self.bounds.minX + 7.65, self.bounds.minY - 0.25))
    clipPath.addCurveToPoint(CGPointMake(self.bounds.minX + 7.65, self.bounds.minY + 36.1), controlPoint1: CGPointMake(self.bounds.minX - 2.38, self.bounds.minY + 9.79), controlPoint2: CGPointMake(self.bounds.minX - 2.38, self.bounds.minY + 26.06))
    clipPath.addCurveToPoint(CGPointMake(self.bounds.minX + 43.99, self.bounds.minY + 36.1), controlPoint1: CGPointMake(self.bounds.minX + 17.69, self.bounds.minY + 46.13), controlPoint2: CGPointMake(self.bounds.minX + 33.96, self.bounds.minY + 46.13))
    clipPath.addLineToPoint(CGPointMake(self.bounds.minX + 43.99, self.bounds.minY + 36.1))
    clipPath.addLineToPoint(CGPointMake(self.bounds.minX + 44.01, self.bounds.minY + 0.19))
    clipPath.addLineToPoint(CGPointMake(self.bounds.minX + 7.58, self.bounds.minY + 0.19))

    // update layer paths
    fillLayer.path = clipPath.CGPath
    maskLayer.path = clipPath.CGPath

}

override init(frame: CGRect) {
    super.init(frame: frame)

    // configure filling
    fillLayer.fillColor = fillColor.CGColor
    fillLayer.fillRule = kCAFillRuleEvenOdd

    // add fill path to superlayer
    layer.addSublayer(fillLayer)

    // configure masking layer
    maskLayer.fillRule = kCAFillRuleEvenOdd
    layer.mask = maskLayer
}
like image 127
Hamish Avatar answered Sep 28 '22 02:09

Hamish