【问题标题】:Create a custom animatable property创建自定义动画属性
【发布时间】:2012-12-20 23:56:09
【问题描述】:

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

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

如果我的UIView 中有一个CGPath,那么我可以通过更改路径绘制的百分比来为它的绘制设置动画。

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

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

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

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

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

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

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

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

【问题讨论】:

    标签: iphone ios uiview core-animation


    【解决方案1】:

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

    - (void) drawInContext:(CGContextRef) context
    

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

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

    当然,您还需要有一个名为percentage@property。从现在开始,您可以使用核心动画为百分比属性设置动画。我没有检查它是否也可以使用[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"];
    

    哦,要将yourCustomLayermyCircleView 一起使用,请执行以下操作:

    [myCircleView.layer addSublayer:myCustomLayer];
    

    【讨论】:

    • 感谢您的回复。我今晚去看看。
    • @Fogmeister 嗨,聚会有点晚了。希望问:我们将如何实现- (void) drawInContext:(CGContextRef) context?这是绘制新自定义属性的地方吗?提前致谢。
    • @Tom 在您的示例中,您将持续时间手动设置为 1...您将如何计算 UIViewAnimation 块提供的持续时间/曲线/延迟等...?跨度>
    【解决方案2】:

    完整的 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 文章 here,详细介绍了它的工作原理

    还有一个使用相同概念的objc项目here

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

    【讨论】:

    • 你好。你试过用CGAffineTransform实现@NSManaged吗?
    【解决方案3】:

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

    有一个很酷的example from Apple 涵盖了这个问题。

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

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

    希望它对某人有所帮助。

    【讨论】:

    • 苹果的示例代码有问题。即使您禁用动画,它仍然会为灯泡设置动画:[self setOn:!self.on animated:NO];
    【解决方案4】:

    您必须自己实施百分比部分。您可以覆盖图层绘图代码以根据设置的百分比值绘制 cgpath。查看core animation programming guideanimation types and timing guide

    【讨论】:

    • 感谢您的回复。我今晚去看看。
    • 不知道。与我无关。
    【解决方案5】:

    @David Rees answer 让我走上正轨,但有一个问题。就我而言 在动画开始之后,动画的完成总是返回 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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-03-11
      • 1970-01-01
      • 2015-08-13
      • 1970-01-01
      • 2014-05-23
      • 1970-01-01
      相关资源
      最近更新 更多