27

UIView你可以改变背景颜色动画。在 a 上,UISlideView您可以更改动画值。

您可以将自定义属性添加到您自己的UIView子类中以使其可以动画化吗?

如果我有一个CGPathUIView那么我可以通过更改路径绘制的百分比来动画它的绘制。

我可以将该动画封装到子类中吗?

即我有一个UIView创建CGPath一个圆圈的。

如果圆圈不存在,则表示 0%。如果圆圈已满,则表示 100%。我可以通过更改路径绘制的百分比来绘制任何值。我还可以通过动画百分比CGPath和重绘路径来动画变化(在 UIView 子类中)。

我可以设置一些属性(即百分比),UIView以便我可以将更改粘贴到一个UIView animateWithDuration块中并动画化路径百分比的更改吗?

我希望我已经解释了我想做的事情。

本质上,我想做的只是......

[UIView animateWithDuration:1.0
 animations:^{
     myCircleView.percentage = 0.7;
 }
 completion:nil];

并且圆形路径动画到给定的百分比。

4

5 回答 5

26

如果您扩展 CALayer 并实现您的自定义

- (void) drawInContext:(CGContextRef) context

needsDisplayForKey您可以通过覆盖(在您的自定义 CALayer 类中)来制作动画属性,如下所示:

+ (BOOL) needsDisplayForKey:(NSString *) key {
    if ([key isEqualToString:@"percentage"]) {
        return YES;
    }
    return [super needsDisplayForKey:key];
}

当然,您还需要有一个@property被调用的percentage. 从现在开始,您可以使用核心动画为百分比属性设置动画。我没有检查它是否也可以使用[UIView animateWithDuration...]调用。它可能会起作用。但这对我有用:

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"percentage"];
animation.duration = 1.0;
animation.fromValue = [NSNumber numberWithDouble:0];
animation.toValue = [NSNumber numberWithDouble:100];

[myCustomLayer addAnimation:animation forKey:@"animatePercentage"];

哦,要使用yourCustomLayerwith myCircleView,请执行以下操作:

[myCircleView.layer addSublayer:myCustomLayer];
于 2013-01-07T09:59:01.743 回答
17

完整的 Swift 3 示例:

完成品

public class CircularProgressView: UIView {

    public dynamic var progress: CGFloat = 0 {
        didSet {
            progressLayer.progress = progress
        }
    }

    fileprivate var progressLayer: CircularProgressLayer {
        return layer as! CircularProgressLayer
    }

    override public class var layerClass: AnyClass {
        return CircularProgressLayer.self
    }


    override public func action(for layer: CALayer, forKey event: String) -> CAAction? {
        if event == #keyPath(CircularProgressLayer.progress),
            let action = action(for: layer, forKey: #keyPath(backgroundColor)) as? CAAnimation, 
            let animation: CABasicAnimation = (action.copy() as? CABasicAnimation) {
            animation.keyPath = #keyPath(CircularProgressLayer.progress)
            animation.fromValue = progressLayer.progress
            animation.toValue = progress
            self.layer.add(animation, forKey: #keyPath(CircularProgressLayer.progress))
            return animation
        }
        return super.action(for: layer, forKey: event)
    }

}



/*
* Concepts taken from:
* https://stackoverflow.com/a/37470079
*/
fileprivate class CircularProgressLayer: CALayer {
    @NSManaged var progress: CGFloat
    let startAngle: CGFloat = 1.5 * .pi
    let twoPi: CGFloat = 2 * .pi
    let halfPi: CGFloat = .pi / 2


    override class func needsDisplay(forKey key: String) -> Bool {
        if key == #keyPath(progress) {
            return true
        }
        return super.needsDisplay(forKey: key)
    }

    override func draw(in ctx: CGContext) {
        super.draw(in: ctx)

        UIGraphicsPushContext(ctx)

        //Light Grey
        UIColor.lightGray.setStroke()

        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        let strokeWidth: CGFloat = 4
        let radius = (bounds.size.width / 2) - strokeWidth
        let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: twoPi, clockwise: true)
        path.lineWidth = strokeWidth
        path.stroke()


        //Red
        UIColor.red.setStroke()

        let endAngle = (twoPi * progress) - halfPi
        let pathProgress = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle , clockwise: true)
        pathProgress.lineWidth = strokeWidth
        pathProgress.lineCapStyle = .round
        pathProgress.stroke()

        UIGraphicsPopContext()
    }
}

let circularProgress = CircularProgressView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut, animations: {
    circularProgress.progress = 0.76
}, completion: nil)

这里有一篇很棒的 objc 文章,详细介绍了它的工作原理

以及在这里使用相同概念的 objc 项目:

本质上,当从动画块中为对象设置动画时,将调用 action(for layer:),我们可以使用相同的属性(从 backgroundColor 属性中窃取)开始我们自己的动画并为更改设置动画。

于 2017-07-07T02:04:47.193 回答
3

对于那些像我一样需要更多详细信息的人:

Apple提供了一个很酷的例子来解决这个问题。

例如,多亏了它,我发现您实际上不需要将自定义层添加为子层(正如@Tom van Zummeren所建议的那样)。相反,将类方法添加到您的 View 类就足够了:

+ (Class)layerClass
{
    return [CustomLayer class];
}

希望它可以帮助某人。

于 2015-09-27T20:08:01.470 回答
0

您必须自己实施百分比部分。您可以覆盖图层绘图代码以根据设置的百分比值绘制您的 cgpath。查看核心动画编程指南动画类型和时间指南

于 2013-01-07T09:50:56.833 回答
0

@David Rees的回答让我走上了正确的道路,但有一个问题。在我的情况下,动画的完成总是在动画开始后返回 false。

UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut, animations: {
    circularProgress.progress = 0.76
}, completion: { finished in
   // finished - always false
})

这就是它为我工作的方式——动画的动作是在 CALayer 内部处理的。

我还包括了一个小例子,如何制作具有“颜色”等附加属性的图层。在这种情况下,如果没有复制值的初始化程序,更改颜色只会影响非动画视图。在动画期间,它会在“默认设置”下可见。

public class CircularProgressView: UIView {

    @objc public dynamic var progress: CGFloat {
        get {
            return progressLayer.progress
        }
        set {
            progressLayer.progress = newValue
        }
    }

    fileprivate var progressLayer: CircularProgressLayer {
        return layer as! CircularProgressLayer
    }

    override public class var layerClass: AnyClass {
        return CircularProgressLayer.self
    }
}


/*
* Concepts taken from:
* https://stackoverflow.com/a/37470079
*/
fileprivate class CircularProgressLayer: CALayer {
    @NSManaged var progress: CGFloat
    let startAngle: CGFloat = 1.5 * .pi
    let twoPi: CGFloat = 2 * .pi
    let halfPi: CGFloat = .pi / 2


    var color: UIColor = .red
    // preserve layer properties
    // without this specyfic init, if color was changed to sth else
    // animation would still use .red
    override init(layer: Any) {
        super.init(layer: layer)
        if let layer = layer as? CircularProgressLayer {
            self.color = layer.color
            self.progress = layer.progress
        }
    }

    override init() {
        super.init()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override class func needsDisplay(forKey key: String) -> Bool {
        if key == #keyPath(progress) {
            return true
        }
        return super.needsDisplay(forKey: key)
    }

    override func action(forKey event: String) -> CAAction? {
        if event == #keyPath(CircularProgressLayer.progress) {
            guard let animation = action(forKey: #keyPath(backgroundColor)) as? CABasicAnimation else {
                setNeedsDisplay()
                return nil
            }

            if let presentation = presentation() {
                animation.keyPath = event
                animation.fromValue = presentation.value(forKeyPath: event)
                animation.toValue = nil
            } else {
                return nil
            }

            return animation
        }

        return super.action(forKey: event)
    }

    override func draw(in ctx: CGContext) {
        super.draw(in: ctx)

        UIGraphicsPushContext(ctx)

        //Light Gray
        UIColor.lightGray.setStroke()

        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        let strokeWidth: CGFloat = 4
        let radius = (bounds.size.width / 2) - strokeWidth
        let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: twoPi, clockwise: true)
        path.lineWidth = strokeWidth
        path.stroke()

        // Red - default
        self.color.setStroke()

        let endAngle = (twoPi * progress) - halfPi
        let pathProgress = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle , clockwise: true)
        pathProgress.lineWidth = strokeWidth
        pathProgress.lineCapStyle = .round
        pathProgress.stroke()

        UIGraphicsPopContext()
    }
}

我在这篇文章中找到了以不同方式处理动画和复制图层属性的方法: https ://medium.com/better-programming/make-apis-like-apple-animatable-view-properties-in-swift-4349b2244cea

于 2020-06-01T14:41:22.897 回答