当动画CALayer的shadowPath时实现流畅动画

7

我的应用中有一个UIView,使用CALayer实现阴影效果:

@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

如果我想要接近合理的性能,我必须定义CALayershadowPath

@implementation MyUIView

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

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

@end

我注意到,每当我给这个UIView加动画时,有两件事情需要注意:
  1. 如果我没有使用shadowPath,阴影会随着旋转和框架大小的变化而平稳地动画化。缺点是动画非常慢,性能一般。

  2. 如果我使用shadowPath,每当UIView被动画化时,动画是平滑和及时的,但阴影的过渡本身比没有shadowPath时更加块状(不够平滑)。

示例:

编辑:

值得注意的是,这些动画是隐式的 - 我没有主动调用它们。它们是UIViewController随着设备方向旋转的结果。阴影位于一个在旋转过程中变化大小的UIView上。


问题的一部分是,虽然路径是可动画的,但它们没有隐式动画(即由属性更改触发的动画)。因此,在旋转过程中,框架会进行动画处理,但路径立即更改为其最终值。 - David Rönnqvist
@DavidRönnqvist 噢,那很有道理。有没有手动完成这个操作的方法? - mattsven
是的,您可以手动添加动画。但是,除非您获得与帧动画相同的时间和持续时间,否则它看起来不正确。 - David Rönnqvist
@DavidRönnqvist 我之前做过类似的事情(从另一个图层借用动画)https://gist.github.com/matt-curtis/ed23259c07f5bf08d471 有什么想法可以将其适应于路径动画吗?我正在研究,但也想听听你的意见。 - mattsven
2个回答

4

我尝试重现您提供的两个gif中显示的行为,但没有成功(也许您可以编辑您的问题并附上动画代码,例如UIView animateWithDuration:animations:)。

然而,在我的脑海深处,我记得偶尔遇到过类似的问题,结果发现必须栅格化视图以使其平滑。

因此,我不能保证它能解决您的问题,但请尝试一下:

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

我并没有手动进行动画,这只是一个视图控制器随着设备方向旋转。我会尝试你的建议。 - mattsven
1
你好Dennis,除了使用shadowPath之外,这个方法解决了问题。这解决了通常不使用shadowPath会导致的性能问题。谢谢!(7小时时间限制结束后我会授予奖励) - mattsven
很高兴能帮到你!感谢指出你旋转了设备,并且在最终版本中没有使用shadowPath - Dennis
实际上,我发现这对作为子层添加的单独CALayer无效。我暂时取消将其标记为答案,以防在赏金期限内出现更好的解决方案。 - mattsven
@mattsven:你找到了其他的解决方案吗?如果你提供更多关于如何创建“作为子层添加的单独CALayer”的细节,我可以更加深入地研究它。 - Dennis
你的解决方案在应用于单个UIView的阴影情况下非常有效。但是,如果阴影应用于添加到UIView层([parentView.layer addSublayer:shadowLayer])的CALayer上,则无法正常工作。您可以在启用旋转的UIViewController中复制此问题,并使用我上面提到的CALayer设置。 - mattsven

0

当视图调整大小时,shadowPath需要设置附加动画。

您可以直接使用我的类。

/*
 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
}

通过https://github.com/BB9z/iOS-Project-Template/blob/master/App/General/Effect/Shadow.swift实现


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接