简体   繁体   中英

Animating CALayer's shadowPath property

I am aware that a CALayer's shadowPath is only animatable using explicit animations, however I still cannot get this to work. I suspect that I am not passing the toValue properly - as I understand this has to be an id , yet the property takes a CGPathRef. Storing this in a UIBezierPath does not seem to work. I am using the following code to test:

CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:@"shadowPath"];
theAnimation.duration = 3.0;
theAnimation.toValue = [UIBezierPath bezierPathWithRect:CGRectMake(-10.0, -10.0, 50.0, 50.0)];
[self.view.layer addAnimation:theAnimation forKey:@"animateShadowPath"];

(I am using minus values so as to ensure the shadow extends beyond a view that lies on top of it... the layer's masksToBounds property is set to NO ).

How is animation of the shadowPath achieved?

UPDATE

Problem nearly solved. Unfortunately, the main problem was a somewhat careless error...

The mistake I made was to add the animation to the root layer of the view controller, rather than the layer I had dedicated to the shadow. Also, @pe8ter was correct in that the toValue needs to be a CGPathRef cast to id (obviously when I had tried this before I still had no animation due to the wrong layer mistake). The animation works with the following code:

CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:@"shadowPath"];
theAnimation.duration = 3.0;
theAnimation.toValue = (id)[UIBezierPath bezierPathWithRect:myRect].CGPath;
[controller.shadowLayer addAnimation:theAnimation forKey:@"shadowPath"];

I appreciate this was difficult to spot from the sample code I provided. Hopefully it can still be of use to people in a similar situation though.

However, when I try and add the line

controller.shadowLayer.shadowPath = [UIBezierPath bezierPathWithRect:myRect].CGPath;

the animation stops working, and the shadow just jumps to the final position instantly. Docs say to add the animation with the same key as the property being changed so as to override the implicit animation created when setting the value of the property, however shadowPath can't generate implicit animations... so how do I get the new property to stay after the animation?

Firstly, you did not set the animation's fromValue .

Secondly, you're correct: toValue accepts a CGPathRef , except it needs to be cast to id . Do something like this:

theAnimation.toValue = (id)[UIBezierPath bezierPathWithRect:newRect].CGPath;

You'll also need to set the shadowPath property of the layer explicitly if you want the change to remain after animation.

let cornerRadious = 10.0
        //
        let shadowPathFrom = UIBezierPath(roundedRect: rect1, cornerRadius: cornerRadious)
        let shadowPathTo = UIBezierPath(roundedRect: rect2, cornerRadius: cornerRadious)
        //
        layer.masksToBounds = false
        layer.shadowColor = UIColor.yellowColor().CGColor
        layer.shadowOpacity = 0.6
        //
        let shadowAnimation = CABasicAnimation(keyPath: "shadowPath")
        shadowAnimation.fromValue = shadowPathFrom.CGPath
        shadowAnimation.toValue = shadowPathTo.CGPath
        shadowAnimation.duration = 0.4
        shadowAnimation.autoreverses = true
        shadowAnimation.removedOnCompletion = true
        shadowAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        layer.addAnimation(shadowAnimation, forKey: "shadowAnimation")

I've met same problem for shadow animation and find solution for persisting shadow after end of animation. You need to set final path to your shadow layer before you start animation. Reverting to initial shadow happens because CoreAnimation does not update properties of original layer, it creates copy of this layer and display animation on this copy ( check Neko1kat answer for more details ). After animation ended system removes this animated layer and return original layer, that has not updated path and your old shadow appears. Try this code:

    let shadowAnimation = CABasicAnimation(keyPath: "shadowPath")
    shadowAnimation.fromValue = currentShadowPath
    shadowAnimation.toValue = newShadowPath
    shadowAnimation.duration = 3.0

    shadowLayer.shadowPath = newShadowPath
    shadowLayer.add(shadowAnimation, forKey: "shadowAnimation")

Although not directly answer this question, if you just needs a view can drop shadow, you can use my class directly.

/*
 Shadow.swift

 Copyright © 2018, 2020-2021 BB9z
 https://github.com/BB9z/iOS-Project-Template

 The MIT License
 https://opensource.org/licenses/MIT
 */

/**
 A view drops shadow.
 */
@IBDesignable
class ShadowView: UIView {
    @IBInspectable var shadowOffset: CGPoint = CGPoint(x: 0, y: 8) {
        didSet { needsUpdateStyle = true }
    }
    @IBInspectable var shadowBlur: CGFloat = 10 {
        didSet { needsUpdateStyle = true }
    }
    @IBInspectable var shadowSpread: CGFloat = 0 {
        didSet { needsUpdateStyle = true }
    }
    /// Set nil can disable shadow
    @IBInspectable var shadowColor: UIColor? = UIColor.black.withAlphaComponent(0.3) {
        didSet { needsUpdateStyle = true }
    }
    @IBInspectable var cornerRadius: CGFloat {
        get { layer.cornerRadius }
        set { layer.cornerRadius = newValue }
    }

    private var needsUpdateStyle = false {
        didSet {
            guard needsUpdateStyle, !oldValue else { return }
            DispatchQueue.main.async { [self] in
                if needsUpdateStyle { updateLayerStyle() }
            }
        }
    }

    private func updateLayerStyle() {
        needsUpdateStyle = false
        if let color = shadowColor {
            Shadow(view: self, offset: shadowOffset, blur: shadowBlur, spread: shadowSpread, color: color, cornerRadius: cornerRadius)
        } else {
            layer.shadowColor = nil
            layer.shadowPath = nil
            layer.shadowOpacity = 0
        }
    }

    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        updateLayerStyle()
    }

    override func layoutSublayers(of layer: CALayer) {
        super.layoutSublayers(of: layer)
        lastLayerSize = layer.bounds.size
        if shadowColor != nil, layer.shadowOpacity == 0 {
            updateLayerStyle()
        }
    }

    private var lastLayerSize = CGSize.zero {
        didSet {
            if oldValue == lastLayerSize { return }
            guard shadowColor != nil else { return }
            updateShadowPathWithAnimationFixes(bonuds: layer.bounds)
        }
    }

    // We needs some additional step to achieve smooth result when view resizing
    private func updateShadowPathWithAnimationFixes(bonuds: CGRect) {
        let rect = bonuds.insetBy(dx: shadowSpread, dy: shadowSpread)
        let newShadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
        if let resizeAnimation = layer.animation(forKey: "bounds.size") {
            let key = #keyPath(CALayer.shadowPath)
            let shadowAnimation = CABasicAnimation(keyPath: key)
            shadowAnimation.duration = resizeAnimation.duration
            shadowAnimation.timingFunction = resizeAnimation.timingFunction
            shadowAnimation.fromValue = layer.shadowPath
            shadowAnimation.toValue = newShadowPath
            layer.add(shadowAnimation, forKey: key)
        }
        layer.shadowPath = newShadowPath
    }
}

/**
 Make shadow with the same effect as Sketch app.
 */
func Shadow(view: UIView?, offset: CGPoint, blur: CGFloat, spread: CGFloat, color: UIColor, cornerRadius: CGFloat = 0) {  // swiftlint:disable:this identifier_name
    guard let layer = view?.layer else {
        return
    }
    layer.shadowColor = color.cgColor
    layer.shadowOffset = CGSize(width: offset.x, height: offset.y)
    layer.shadowRadius = blur
    layer.shadowOpacity = 1
    layer.cornerRadius = cornerRadius

    let rect = layer.bounds.insetBy(dx: spread, dy: spread)
    layer.shadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
}

via https://github.com/BB9z/iOS-Project-Template/blob/master/App/General/Effect/Shadow.swift

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM