在 Swift 中动画自定义 UIView 属性

Posted

技术标签:

【中文标题】在 Swift 中动画自定义 UIView 属性【英文标题】:Animate Custom UIView Property in Swift 【发布时间】:2016-05-26 19:07:40 【问题描述】:

全部!

我使用CoreGraphics 创建了一个循环进度视图,其外观和更新如下:

50% 75%

该类是一个UIView 类,它有一个名为“progress”的变量,用于确定圆圈的填充量。

效果很好,但我希望能够对进度变量的更改进行动画处理,以使条形图平滑地动画化。

我从无数示例中读到,我需要一个 CALayer 类以及我创建的 View 类,但是它根本没有动画。

两个问题:

    我可以保留我在CoreGraphics 中绘制的图形,还是需要以某种方式在CALayer 中重新绘制它?

    我当前(尝试的)解决方案在以下位置崩溃:anim.fromValue = pres.progress。怎么了?

    class CircleProgressView: UIView 
        @IBInspectable var backFillColor: UIColor = UIColor.blueColor()
        @IBInspectable var fillColor: UIColor = UIColor.greenColor()
        @IBInspectable var strokeColor: UIColor = UIColor.greenColor()
    
        dynamic var progress: CGFloat = 0.00 
            didSet 
                self.layer.setValue(progress, forKey: "progress")
            
        
    
        var distToDestination: CGFloat = 10.0
        @IBInspectable var arcWidth: CGFloat = 20
        @IBInspectable var outlineWidth: CGFloat = 5
    
        override class func layerClass() -> AnyClass 
            return CircleProgressLayer.self
        
    
        override func drawRect(rect: CGRect) 
    
            var fillColor = self.fillColor
    
            if distToDestination < 3.0 
                fillColor = UIColor.greenColor()
             else 
                fillColor = self.fillColor
            
    
            //Drawing the inside of the container
    
            //Drawing in the container
            let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
            let radius: CGFloat = max(bounds.width, bounds.height) - 10
            let startAngle: CGFloat = 3 * π / 2
            let endAngle: CGFloat = 3 * π / 2 + 2 * π
            let path = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
            path.lineWidth = arcWidth
            backFillColor.setStroke()
            path.stroke()
            let fill = UIColor.blueColor().colorWithAlphaComponent(0.15)
            fill.setFill()
            path.fill()
    
            //Drawing the fill path. Same process
            let fillAngleLength =  (π) * progress
            let fillStartAngle = 3 * π / 2 - fillAngleLength
            let fillEndAngle = 3 * π / 2 + fillAngleLength
    
            let fillPath_fill = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: fillStartAngle, endAngle: fillEndAngle, clockwise: true)
            fillPath_fill.lineWidth = arcWidth
            fillColor.setStroke()
            fillPath_fill.stroke()
    
            //Drawing container outline on top
            let outlinePath_outer = UIBezierPath(arcCenter: center, radius: radius / 2 - outlineWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
            let outlinePath_inner = UIBezierPath(arcCenter: center, radius: radius / 2 - arcWidth + outlineWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
            outlinePath_outer.lineWidth = outlineWidth
            outlinePath_inner.lineWidth = outlineWidth
            strokeColor.setStroke()
            outlinePath_outer.stroke()
            outlinePath_inner.stroke()
        
    
    
    class CircleProgressLayer: CALayer 
        @NSManaged var progress: CGFloat
    
        override class func needsDisplayForKey(key: String) -> Bool 
            if key == "progress" 
                return true
            
            return super.needsDisplayForKey(key)
        
    
        override func actionForKey(key: String) -> CAAction? 
            if (key == "progress") 
                if let pres = self.presentationLayer() 
                    let anim: CABasicAnimation = CABasicAnimation.init(keyPath: key)
                    anim.fromValue = pres.progress
                    anim.duration = 0.2
                    return anim
                
    
            return super.actionForKey(key)
             else 
                return super.actionForKey(key)
            
        
    
    

感谢您的帮助!

【问题讨论】:

你有没有试过做类似的事情:informit.com/articles/article.aspx?p=1431312&seqNum=5? 我相信这就是 actionForKey() -> CAAction 中发生的事情? 【参考方案1】:

试试这个:)

class ViewController: UIViewController 

    let progressView = CircleProgressView(frame:CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200))

    override func viewDidLoad() 
        super.viewDidLoad()
        let button = UIButton()
        button.frame = CGRectMake(0, 300, 200, 100)
        button.backgroundColor = UIColor.yellowColor()
        button.addTarget(self, action: #selector(ViewController.tap), forControlEvents: UIControlEvents.TouchUpInside)
        view.addSubview(button)
        view.addSubview(progressView)

        progressView.progress = 1.0
   

    func tap() 
        if  progressView.progress == 0.5 
            progressView.progress = 1.0
         else 
            progressView.progress = 0.5
        
    


class CircleProgressView: UIView 

    dynamic var progress: CGFloat = 0.00 
        didSet 
            let animation = CABasicAnimation()
            animation.keyPath = "progress"
            animation.fromValue = circleLayer().progress
            animation.toValue = progress
            animation.duration = Double(0.5)
            self.layer.addAnimation(animation, forKey: "progress")
            circleLayer().progress = progress
        
    

    func circleLayer() ->  CircleProgressLayer 
        return self.layer as! CircleProgressLayer
    

    override class func layerClass() -> AnyClass 
        return CircleProgressLayer.self
    


class CircleProgressLayer: CALayer 
    @NSManaged var progress: CGFloat

    override class func needsDisplayForKey(key: String) -> Bool 
        if key == "progress" 
            return true
        
        return super.needsDisplayForKey(key)
    

    var backFillColor: UIColor = UIColor.blueColor()
    var fillColor: UIColor = UIColor.greenColor()
    var strokeColor: UIColor = UIColor.greenColor()
    var distToDestination: CGFloat = 10.0
    var arcWidth: CGFloat = 20
    var outlineWidth: CGFloat = 5

    override func drawInContext(ctx: CGContext) 
        super.drawInContext(ctx)

        UIGraphicsPushContext(ctx)

        //Drawing in the container
        let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
        let radius: CGFloat = max(bounds.width, bounds.height) - 10
        let startAngle: CGFloat = 3 * CGFloat(M_PI) / 2
        let endAngle: CGFloat = 3 * CGFloat(M_PI) / 2 + 2 * CGFloat(M_PI)
        let path = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        path.lineWidth = arcWidth
        backFillColor.setStroke()
        path.stroke()
        let fill = UIColor.blueColor().colorWithAlphaComponent(0.15)
        fill.setFill()
        path.fill()

        //Drawing the fill path. Same process
        let fillAngleLength =  (CGFloat(M_PI)) * progress
        let fillStartAngle = 3 * CGFloat(M_PI) / 2 - fillAngleLength
        let fillEndAngle = 3 * CGFloat(M_PI) / 2 + fillAngleLength

        let fillPath_fill = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: fillStartAngle, endAngle: fillEndAngle, clockwise: true)
        fillPath_fill.lineWidth = arcWidth
        fillColor.setStroke()
        fillPath_fill.stroke()

        //Drawing container outline on top
        let outlinePath_outer = UIBezierPath(arcCenter: center, radius: radius / 2 - outlineWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        let outlinePath_inner = UIBezierPath(arcCenter: center, radius: radius / 2 - arcWidth + outlineWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        outlinePath_outer.lineWidth = outlineWidth
        outlinePath_inner.lineWidth = outlineWidth
        strokeColor.setStroke()
        outlinePath_outer.stroke()
        outlinePath_inner.stroke()

        UIGraphicsPopContext()
    

【讨论】:

谢谢!我知道提供示例代码可能会令人不悦,但这就像一个魅力。看起来我在错误的地方有正确的元素。感谢您向我展示如何在 CA 层中绘制核心图形元素,并将动画嵌入到 didSet 中:) 干得好,我真的尝试了很多次才能让它工作,最重要的是用 @NSManaged 标记您的自定义属性【参考方案2】:

虽然 AntonTheDev 提供了一个很好的答案,但他的解决方案不允许您在动画块中为 CircularProgressView 设置动画,因此您不能做以下整洁的事情:

UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut, 
animations: 
    circularProgress.progress = 0.76
, completion: nil)

这里有一个类似的问题,根据接受的答案和this 帖子的想法,提供了最新的 Swift 3 答案。 这就是最终解决方案的样子。

Swift 3 Solution

【讨论】:

以上是关于在 Swift 中动画自定义 UIView 属性的主要内容,如果未能解决你的问题,请参考以下文章

UIView 使用自定义属性有条件地为子视图设置动画

无法使用 animator() [ OSX ] 为 Swift 自定义属性设置动画

UIView beginAnimations 在自定义属性上太快了

Swift 4为UIView添加自定义圆形[关闭]

iOS - 如何在没有第三方框架的情况下在 Swift 中创建自定义动画横幅

如何在 iOS 中为自定义属性设置动画