Search code examples
iosswiftcore-animationcalayercashapelayer

Animate transparency of part of mask with CAShapeLayer


I have a view with .brown backgroundColor.

In that view I have a backgroundView, which has backgroundColor: UIColor.black.withAlphaComponent(0.5).

I then want to have a transparent box in this backgroundView which shows the underlaying view. And I have achieved that, but I can't do it by animating its transparency, my code only grows the "cut out area" until desired size instead of animating it's transparency.

Brown view with the backgroundView and it's 0.5 alpha

Brown view with the backgroundView and it's 0.5 alpha

Added layer to backgroundView shows part of the brown view

Added layer to backgroundView shows part of the brown view

The code I've written to get what I have now is this:

func addIndicatorTo(global frame: CGRect) {
    let shape = CAShapeLayer()
    let targetRect = convert(frame, from: nil).insetBy(dx: -4, dy: -4)
    let animation = CABasicAnimation(keyPath: "path")

    let toPath = CGMutablePath()
    toPath.addRect(bounds)
    toPath.addPath(UIBezierPath(roundedRect: targetRect,
                                byRoundingCorners: .allCorners,
                                cornerRadii: CGSize(width: 4, height: 4)).cgPath)
    toPath.closeSubpath()

    let fromPath = CGMutablePath()
    fromPath.addRect(bounds)
    fromPath.addPath(UIBezierPath(roundedRect: targetRect.insetBy(dx: targetRect.width / 2,
                                                                  dy: targetRect.height / 2),
                                  byRoundingCorners: .allCorners,
                                  cornerRadii: CGSize(width: 4, height: 4)).cgPath)
    fromPath.closeSubpath()

    animation.fromValue = fromPath
    animation.toValue = toPath
    animation.duration = 0.5

    shape.fillRule = .evenOdd
    shape.path = animation.fromValue as! CGPath

    backgroundView.layer.mask = shape
    shape.add(animation, forKey: nil)

    CATransaction.begin()
    CATransaction.setDisableActions(true)
    shape.path = toPath
    CATransaction.commit()
}

Solution

  • Not sure if this is a "good" solution. But I will go with this until someone tells shows me how to solve it more elegantly!

    Basically I just create a mask with the box cut out, then I create a new boxLayer with draws in the area of the "cutout". Then I just animate the opacity of the boxLayer.

    func addIndicatorTo(global frame: CGRect) {
        let maskLayer = CAShapeLayer()
        let boxLayer = CAShapeLayer()
    
        let targetRect = convert(frame, from: nil).insetBy(dx: -4, dy: -4)
        let animation = CABasicAnimation(keyPath: "opacity")
    
        let toPath = CGMutablePath()
    
        let boxPath = UIBezierPath(roundedRect: targetRect,
                                   byRoundingCorners: .allCorners,
                                   cornerRadii: CGSize(width: 4, height: 4)).cgPath
        toPath.addRect(bounds)
        toPath.addPath(boxPath)
        toPath.closeSubpath()
    
        boxLayer.fillColor = UIColor.black.withAlphaComponent(0.5).cgColor
        boxLayer.path = boxPath
    
        animation.fromValue = 1.0
        animation.toValue = 0.0
        animation.duration = 0.5
    
        maskLayer.fillRule = .evenOdd
        maskLayer.path = toPath
    
        backgroundView.layer.mask = maskLayer
        layer.addSublayer(boxLayer)
        boxLayer.add(animation, forKey: nil)
    
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        boxLayer.opacity = 0.0
        CATransaction.commit()
    }