Swift - Color fill animation

2019-02-17 03:26发布

问题:

I'm relatively new to ios animations and I believe there's something wrong with the approach I took to animate UIView.

I will start with a UI screenshot to picture my problem more precisely:

There is a tableView cell with two labels and colorful filled circle

Anytime I introduce new value to the cell, I'd like to animate this left-most bulb so it looks like it's getting filled with red color.

This is the implementation od BadgeView, which is basically the aforementioned leftmost filled circle

class BadgeView: UIView {

var coeff:CGFloat = 0.5

override func drawRect(rect: CGRect) {
    let topRect:CGRect = CGRectMake(0, 0, rect.size.width, rect.size.height*(1.0 - self.coeff))
    UIColor(red: 249.0/255.0, green: 163.0/255.0, blue: 123.0/255.0, alpha: 1.0).setFill()
    UIRectFill(topRect)

    let bottomRect:CGRect = CGRectMake(0, rect.size.height*(1-coeff), rect.size.width, rect.size.height*coeff)
    UIColor(red: 252.0/255.0, green: 95.0/255.0, blue: 95.0/255.0, alpha: 1.0).setFill()
    UIRectFill(bottomRect)
            self.layer.cornerRadius = self.frame.height/2.0
    self.layer.masksToBounds = true
   }
}

This is the way I achieve uneven fill - I introduced coefficient which I modify in viewController.

Inside my cellForRowAtIndexPath method I try to animate this shape using custom button with callback

let btn:MGSwipeButton = MGSwipeButton(title: "", icon: img, backgroundColor: nil, insets: ins, callback: {
        (sender: MGSwipeTableCell!) -> Bool in
        print("Convenience callback for swipe buttons!")
        UIView.animateWithDuration(4.0, animations:{ () -> Void in
            cell.pageBadgeView.coeff = 1.0
            let frame:CGRect = cell.pageBadgeView.frame
            cell.pageBadgeView.drawRect(frame)
        })
        return true
        }) 

But it does nothing but prints to console

: CGContextSetFillColorWithColor: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.

Although I'd love to know the right answer and approach, it would be great to know, for education purpose, why this code doesn't work. Thanks in advance

回答1:

The error part of the problem seems to be this part of the code:

cell.pageBadgeView.drawRect(frame)

From the Apple docs on UIView drawRect:

This method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. You should never call this method directly yourself. To invalidate part of your view, and thus cause that portion to be redrawn, call the setNeedsDisplay or setNeedsDisplayInRect: method instead.

So if you'd change your code to:

cell.pageBadgeView.setNeedsDisplay() 

You'll get rid of the error and see the badgeView filled correctly. However this won't animate it, since drawRect isn't animatable by default.

The easiest workaround to your problem would be for BadgeView to have an internal view for the fill color. I'd refactor the BadgeView as so:

class BadgeView: UIView {

    private let fillView = UIView(frame: CGRectZero)

    private var coeff:CGFloat = 0.5 {
        didSet {
            // Make sure the fillView frame is updated everytime the coeff changes
            updateFillViewFrame()
        }
    }

    // Only needed if view isn't created in xib or storyboard
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    // Only needed if view isn't created in xib or storyboard
    override init(frame: CGRect) {
        super.init(frame: frame)

        setupView()
    }

    override func awakeFromNib() {
        setupView()
    }

    private func setupView() {
        // Setup the layer
        layer.cornerRadius = bounds.height/2.0
        layer.masksToBounds = true

        // Setup the unfilled backgroundColor
        backgroundColor = UIColor(red: 249.0/255.0, green: 163.0/255.0, blue: 123.0/255.0, alpha: 1.0)

        // Setup filledView backgroundColor and add it as a subview
        fillView.backgroundColor = UIColor(red: 252.0/255.0, green: 95.0/255.0, blue: 95.0/255.0, alpha: 1.0)
        addSubview(fillView)

        // Update fillView frame in case coeff already has a value
        updateFillViewFrame()
    }

    private func updateFillViewFrame() {
        fillView.frame = CGRectMake(0, bounds.height*(1-coeff), bounds.width, bounds.height*coeff)
    }

    // Setter function to set the coeff animated. If setting it not animated isn't necessary at all, consider removing this func and animate updateFillViewFrame() in coeff didSet
    func setCoeff(coeff: CGFloat, animated: Bool) {
        if animated {
            UIView.animateWithDuration(4.0, animations:{ () -> Void in
                self.coeff = coeff
            })
        } else {
            self.coeff = coeff
        }
    }

}

In your button callback you'll just have to do:

cell.pageBadgeView.setCoeff(1.0, animated: true)


回答2:

try this playground code

import UIKit
import CoreGraphics

var str = "Hello, playground"

class BadgeView: UIView {

    var coeff:CGFloat = 0.5

    func drawCircleInView(){
    // Set up the shape of the circle
        let size:CGSize = self.bounds.size;
        let layer = CALayer();
        layer.frame = self.bounds;
        layer.backgroundColor = UIColor.blue().cgColor



        let initialRect:CGRect = CGRect.init(x: 0, y: size.height, width: size.width, height: 0)

        let finalRect:CGRect = CGRect.init(x: 0, y: size.height/2, width: size.width, height: size.height/2)

        let sublayer = CALayer()
        sublayer.frame = initialRect
        sublayer.backgroundColor = UIColor.orange().cgColor
        sublayer.opacity = 0.5


        let mask:CAShapeLayer = CAShapeLayer()
        mask.frame = self.bounds
        mask.path = UIBezierPath(ovalIn: self.bounds).cgPath
        mask.fillColor = UIColor.black().cgColor
        mask.strokeColor = UIColor.yellow().cgColor

        layer.addSublayer(sublayer)
        layer.mask = mask

        self.layer.addSublayer(layer)

        let boundsAnim:CABasicAnimation  = CABasicAnimation(keyPath: "bounds")
        boundsAnim.toValue = NSValue.init(cgRect:finalRect)

        let anim = CAAnimationGroup()
        anim.animations = [boundsAnim]
        anim.isRemovedOnCompletion = false
        anim.duration = 3
        anim.fillMode = kCAFillModeForwards
        sublayer.add(anim, forKey: nil)
    }
}

var badgeView:BadgeView = BadgeView(frame:CGRect.init(x: 50, y: 50, width: 50, height: 50))
var window:UIWindow = UIWindow(frame: CGRect.init(x: 0, y: 0, width: 200, height: 200))
window.backgroundColor = UIColor.red()
badgeView.backgroundColor = UIColor.green()
window.becomeKey()
window.makeKeyAndVisible()
window.addSubview(badgeView)
badgeView.drawCircleInView()


回答3:

More Modification to Above code , anchor point code was missing in above code

   ```
    var str = "Hello, playground"

 class BadgeView: UIView {

var coeff:CGFloat = 0.7

func drawCircleInView(){
    // Set up the shape of the circle
    let size:CGSize = self.bounds.size;

    let layerBackGround = CALayer();
    layerBackGround.frame = self.bounds;
    layerBackGround.backgroundColor = UIColor.blue.cgColor
   self.layer.addSublayer(layerBackGround)


    let initialRect:CGRect = CGRect.init(x: 0, y: size.height , width: size.width, height: 0)

    let finalRect:CGRect = CGRect.init(x: 0, y: 0, width: size.width, height:  size.height)

    let sublayer = CALayer()
    //sublayer.bounds = initialRect
    sublayer.frame = initialRect
    sublayer.anchorPoint = CGPoint(x: 0.5, y: 1)
    sublayer.backgroundColor = UIColor.orange.cgColor
    sublayer.opacity = 1


    let mask:CAShapeLayer = CAShapeLayer()
    mask.frame = self.bounds
    mask.path = UIBezierPath(ovalIn: self.bounds).cgPath
    mask.fillColor = UIColor.black.cgColor
    mask.strokeColor = UIColor.yellow.cgColor

    layerBackGround.addSublayer(sublayer)
    layerBackGround.mask = mask

    self.layer.addSublayer(layerBackGround)

    let boundsAnim:CABasicAnimation  = CABasicAnimation(keyPath: "bounds")
    boundsAnim.toValue = NSValue.init(cgRect:finalRect)

    let anim = CAAnimationGroup()
    anim.animations = [boundsAnim]
    anim.isRemovedOnCompletion = false
    anim.duration = 1
    anim.fillMode = kCAFillModeForwards
    sublayer.add(anim, forKey: nil)
}