动画 CALayer 的 ShadowPath 属性

发布于 2024-11-05 13:09:43 字数 1449 浏览 2 评论 0原文

我知道 CALayer 的 ShadowPath 只能使用显式动画进行动画处理,但是我仍然无法使其工作。我怀疑我没有正确传递 toValue - 据我所知,这必须是一个 id,但该属性采用 CGPathRef。将其存储在 UIBezierPath 中似乎不起作用。我正在使用以下代码进行测试:(

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"];

我使用负值以确保阴影延伸到位于其顶部的视图之外...该图层的 masksToBounds 属性设置为 )。

ShadowPath的动画是如何实现的?

更新

问题几乎解决了。不幸的是,主要问题是一个有点粗心的错误...

我犯的错误是将动画添加到视图控制器的根层,而不是我专用于阴影的层。另外,@pe8ter 是正确的,因为 toValue 需要将 CGPathRef 转换为 id (显然,当我在尝试此操作之前,我仍然有由于图层错误而没有动画)。该动画使用以下代码:

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

我很高兴从我提供的示例代码中很难发现这一点。希望它对处于类似情况的人仍然有用。

但是,当我尝试添加线条时,

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

动画停止工作,并且阴影立即跳到最终位置。文档说使用与要更改的属性相同的键添加动画,以便覆盖设置属性值时创建的隐式动画,但是shadowPath无法生成隐式动画...那么我如何获取新属性停留在动画之后

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

尽揽少女心 2024-11-12 13:09:43

首先,您没有设置动画的 fromValue

其次,你是对的:toValue接受CGPathRef,但它需要转换为id。执行如下操作:

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

如果您希望在动画之后保留更改,则还需要显式设置图层的 shadowPath 属性。

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.

胡大本事 2024-11-12 13:09:43
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")
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")
风筝在阴天搁浅。 2024-11-12 13:09:43

我在阴影动画中遇到了同样的问题,并找到了动画结束后持久阴影的解决方案。在开始动画之前,您需要设置阴影层的最终路径。恢复到初始阴影是因为 CoreAnimation 不会更新原始图层的属性,它会创建该图层的副本并在此副本上显示动画 (查看 Neko1kat 答案以获取更多详细信息)。动画结束后,系统会删除该动画图层并返回原始图层,该图层尚未更新路径,并且会出现旧的阴影。试试这个代码:

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

    shadowLayer.shadowPath = newShadowPath
    shadowLayer.add(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")
空心↖ 2024-11-12 13:09:43

虽然没有直接回答这个问题,但是如果你只需要一个可以投影的视图,你可以直接使用我的类。

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

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文