简体   繁体   中英

Smooth animation when animating a CALayer's shadowPath

In my app I have a UIView that is using CALayer in order to achieve a shadow:

@implementation MyUIView

    - (instancetype) initWithFrame:(CGRect)frame {
        self = [super initWithFrame:frame];
        if(!self) return self;

        self.layer.shadowColor = [UIColor colorWithWhite:0 alpha:.2].CGColor;
        self.layer.shadowOffset = CGSizeMake(0, 2);
        self.layer.shadowOpacity = 1;
        self.layer.shadowRadius = 1;

        return self;
    }

@end

If I want anything approaching reasonable performance, I have to define the CALayer 's shadowPath :

@implementation MyUIView

    - (void) setFrame:(CGRect)frame {
        [super setFrame:frame];

        self.layer.shadowPath = CGPathCreateWithRect(self.bounds, NULL);
    }

@end

I've noticed two things whenever I animate this UIView :

  1. If I don't use a shadowPath , the shadow animates along nicely with rotations and frame size changes. The caveat here being very slow animation and a general lack of performance.

  2. If I do use a shadowPath whenever the UIView is animated the animation is smooth and timely, however the shadow's transition itself is much more block-like (and less smooth) than it is without a shadowPath .

Examples:

Edit:

It's worth noting that these animations are implicit - I'm not invoking them myself. They are the result of the UIViewController rotating with the device orientation. The shadow is on a UIView that changes size during rotation.

I tried to reproduce the behavior shown in the two gifs you supplied, but without success (maybe you could edit your question with the animation code, eg UIView animateWithDuration:animations: ).

However, somewhere at the back of my mind I remember that once in a while I encountered a similar issue, and it turned out that I had to rasterize the view to make it smooth.

So I cannot guarantee that it solves the problem for you, but give it a try:

self.layer.shouldRasterize = YES;
self.layer.rasterizationScale = [[UIScreen mainScreen] scale];

The shadowPath needs to set additional animation when view resizing.

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