如何使自定义视图的背景颜色 属性 可动画化?

How to make the background color property of a custom view animatable?

我想为自定义视图的背景色设置动画。

我的绘图代码很简单。 draw(in:) 方法被重写为:

@IBDesignable
class SquareView: UIView {
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        let strokeWidth = self.width / 8
        let path = UIBezierPath()
        path.move(to: CGPoint(x: self.width - strokeWidth / 2, y: 0))
        path.addLine(to: CGPoint(x: self.width - strokeWidth / 2, y: self.height - strokeWidth / 2))
        path.addLine(to: CGPoint(x: 0, y: self.height - strokeWidth / 2))
        self.backgroundColor?.darker().setStroke()
        path.lineWidth = strokeWidth
        path.stroke()
    }
}

darker 方法只是 returns 它所调用颜色的较暗版本。

当我将背景设置为蓝色时,它会绘制如下内容:

我想为它的背景色设置动画,使其逐渐变为红色。这将是最终结果:

我第一次尝试:

UIView.animate(withDuration: 1) { 
    self.square.backgroundColor = .red
}

只是瞬间变红,没有动画。

然后我研究了一下,发现需要用到CALayers。因此,我尝试分层绘制:

@IBDesignable
class SquareViewLayers: UIView {
    dynamic var squareColor: UIColor = .blue {
        didSet {
            setNeedsDisplay()
        }
    }

    func setupView() {
        layer.backgroundColor = squareColor.cgColor
        let sublayer = CAShapeLayer()
        let path = UIBezierPath()
        let strokeWidth = self.width / 8
        path.move(to: CGPoint(x: self.width - strokeWidth / 2, y: 0))
        path.addLine(to: CGPoint(x: self.width - strokeWidth / 2, y: self.height - strokeWidth / 2))
        path.addLine(to: CGPoint(x: 0, y: self.height - strokeWidth / 2))
        self.tintColor.darker().setStroke()
        path.lineWidth = strokeWidth
        sublayer.path = path.cgPath
        sublayer.strokeColor = squareColor.darker().cgColor
        sublayer.lineWidth = strokeWidth
        sublayer.backgroundColor = UIColor.clear.cgColor
        layer.addSublayer(sublayer)

    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

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

但结果很糟糕

我用这段代码来尝试制作动画:

let anim = CABasicAnimation(keyPath: "squareColor")
anim.fromValue = UIColor.blue
anim.toValue = UIColor.red
anim.duration = 1
square.layer.add(anim, forKey: nil)

虽然没有任何反应。

我很困惑。正确的做法是什么?

编辑:

darker 方法来自名为 SwiftyColor 的 cocoa pod。这是它的实现方式:

// in an extension of UIColor
public func darker(amount: CGFloat = 0.25) -> UIColor {
    return hueColor(withBrightnessAmount: 1 - amount)
}

private func hueColor(withBrightnessAmount amount: CGFloat) -> UIColor {
    var hue: CGFloat = 0
    var saturation: CGFloat = 0
    var brightness: CGFloat = 0
    var alpha: CGFloat = 0

    if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
        return UIColor(hue: hue, saturation: saturation,
                           brightness: brightness * amount,
                           alpha: alpha)
    }
    return self
}

CABasicAnimation 的方向是正确的。

  1. let anim = CABasicAnimation(keyPath: "squareColor")中的keyPath:应该是backgroundColor.

  2. anim.fromValueanim.toValue 需要 CGColor 值(因为您在使用 CGColorCALayer 上操作) .你可以在这里使用UIColor.blue.cgcolor

很确定此动画不会为您保留颜色更改,因此您可能需要手动更改 属性。

使用 CAKeyframeAnimation 你需要动画 backgroundColor 属性:

class SquareView: UIView
{
    let sublayer = CAShapeLayer()

    required init?(coder aDecoder: NSCoder)
    {
        super.init(coder: aDecoder)
        let frame = self.frame
        let strokeWidth = self.frame.width / 8
        sublayer.frame = self.bounds

        let path = UIBezierPath()
        path.move(to: CGPoint(x: frame.width  - strokeWidth / 2 , y: 0))
        path.addLine(to: CGPoint(x: frame.width - strokeWidth / 2, y: frame.height - strokeWidth / 2))
        path.addLine(to: CGPoint(x: 0, y: frame.height - strokeWidth / 2))
        path.addLine(to: CGPoint(x: 0 , y: 0))
        path.lineWidth = strokeWidth
        sublayer.path = path.cgPath
        sublayer.fillColor = UIColor.blue.cgColor
        sublayer.lineWidth = strokeWidth
        sublayer.backgroundColor = UIColor.blue.cgColor
        layer.addSublayer(sublayer)
    }

    //Action
    func animateIt()
    {
        let animateToColor = UIColor(cgColor: sublayer.backgroundColor!).darker().cgColor
        animateColorChange(mLayer: self.sublayer, colors: [sublayer.backgroundColor!, animateToColor], duration: 1)
    }

    func animateColorChange(mLayer:CALayer ,colors:[CGColor], duration:CFTimeInterval)
    {
        let animation = CAKeyframeAnimation(keyPath: "backgroundColor")
        CATransaction.begin()
        animation.keyTimes = [0.2, 0.5, 0.7, 1]
        animation.values = colors
        animation.calculationMode = kCAAnimationPaced
        animation.fillMode = kCAFillModeForwards
        animation.isRemovedOnCompletion = false
        animation.repeatCount = 0
        animation.duration = duration
        mLayer.add(animation, forKey: nil)
        CATransaction.commit()
    }
}

注意:我已经编辑了答案,以便形状始终适合从故事板添加的父视图

Objective-C

view.backgroundColor = [UIColor whiteColor].CGColor;

[UIView animateWithDuration:2.0 animations:^{
    view.layer.backgroundColor = [UIColor greenColor].CGColor;
} completion:NULL];

Swift

view.backgroundColor = UIColor.white.cgColor
UIView.animate(withDuration: 2.0) { 
    view.backgroundColor = UIColor.green.cgColor
}

你能用 semi-transparent 黑色代替 L-shape 吗?

那就不用再纠结于重绘和颜色计算了,基本的UIView.animate()

就可以了

下面的 "LShadow" 被添加到 SquareView 上,因此您可以像往常一样使用 UIView.animate()

设置 SquareView 背景颜色

ViewController.swift

import UIKit

class ViewController: UIViewController {
    var squareView: SquareView!

    override func viewDidLoad() {
        super.viewDidLoad()

        squareView = SquareView.init(frame: CGRect(x:100, y:100, width:200, height:200))
        self.view.addSubview(squareView)

        squareView.backgroundColor = UIColor.blue
        UIView.animate(withDuration: 2.0, delay: 1.0, animations: {
            self.squareView.backgroundColor = UIColor.red
        }, completion:nil)
    }
}

SquareView.swift

import UIKit

class SquareView: UIView {
    func setupView() {
        let shadow = LShadow.init(frame: self.bounds)
        self.addSubview(shadow)
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupView()
    }
}


class LShadow: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = UIColor.clear
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.backgroundColor = UIColor.clear
    }
    override func draw(_ rect: CGRect) {
        let strokeWidth = self.frame.size.width / 8
        let path = UIBezierPath()
        path.move(to: CGPoint(x: self.frame.size.width - strokeWidth / 2, y: 0))
        path.addLine(to: CGPoint(x: self.frame.size.width - strokeWidth / 2, y: self.frame.size.height - strokeWidth / 2))
        path.addLine(to: CGPoint(x: 0, y: self.frame.size.height - strokeWidth / 2))
        UIColor.init(white: 0, alpha: 0.5).setStroke()
        path.lineWidth = strokeWidth
        path.stroke()
    }
}

我们在下面的代码中有两个不同的 CAShapeLayer,前景和背景。背景是一个矩形路径,绘制到倒 L 实际开始的范围。 L 是使用简单的 UIBezierPath 几何图形创建的路径。设置颜色后,我将每个图层的 fillColor 更改设置为动画。

class SquareViewLayers: UIView, CAAnimationDelegate {

    var color: UIColor = UIColor.blue {
        didSet {
            animate(layer: backgroundLayer,
                    from: oldValue,
                    to: color)
            animate(layer: foregroundLayer,
                    from: oldValue.darker(),
                    to: color.darker())
        }
    }

    let backgroundLayer = CAShapeLayer()
    let foregroundLayer = CAShapeLayer()

    func setupView() {

        foregroundLayer.frame = frame
        layer.addSublayer(foregroundLayer)

        backgroundLayer.frame = frame
        layer.addSublayer(backgroundLayer)


        let strokeWidth = width / 8

        let backgroundPathRect = CGRect(origin: .zero, size: CGSize(width: width - strokeWidth,
                                                                    height: height - strokeWidth))
        let backgroundPath = UIBezierPath(rect: backgroundPathRect)
        backgroundLayer.fillColor = color.cgColor
        backgroundLayer.path = backgroundPath.cgPath

        let foregroundPath = UIBezierPath()
        foregroundPath.move(to: CGPoint(x: backgroundPathRect.width,
                                        y: 0))
        foregroundPath.addLine(to: CGPoint(x: width,
                                           y: 0))
        foregroundPath.addLine(to: CGPoint(x: width, y: height))
        foregroundPath.addLine(to: CGPoint(x: 0,
                                           y: height))
        foregroundPath.addLine(to: CGPoint(x: 0,
                                           y: backgroundPathRect.height))
        foregroundPath.addLine(to: CGPoint(x: backgroundPathRect.width,
                                           y: backgroundPathRect.height))
        foregroundPath.addLine(to: CGPoint(x: backgroundPathRect.width,
                                           y: 0))
        foregroundPath.close()

        foregroundLayer.path = foregroundPath.cgPath
        foregroundLayer.fillColor = color.darker().cgColor
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

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

    func animate(layer: CAShapeLayer,
                 from: UIColor,
                 to: UIColor) {
        let fillColorAnimation = CABasicAnimation(keyPath: "fillColor")
        fillColorAnimation.fromValue = from.cgColor
        layer.fillColor = to.cgColor
        fillColorAnimation.duration = 1.0
        layer.add(fillColorAnimation,
                  forKey: nil)
    }
}

这是结果,

我实现了使用动画更改视图的背景颜色,并在源代码中做了一些调整以使用 CAShapeLayer 添加倒 L 形。我实现了所需的动画。这是视图示例。

class SquareView: UIView, CAAnimationDelegate {

    fileprivate func setup() {
        self.layer.addSublayer(self.sublayer)
    }

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

    let sublayer = CAShapeLayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.setup()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        let strokeWidth = self.bounds.width / 8
        var frame = self.bounds
        frame.size.width -= strokeWidth
        frame.size.height -= strokeWidth
        self.sublayer.path = UIBezierPath(rect: frame).cgPath
    }



    var color : UIColor = UIColor.clear {
        willSet {
            self.sublayer.fillColor = newValue.cgColor
            self.backgroundColor = newValue.darker()
        }
    }



    func animation(color: UIColor, duration: CFTimeInterval = 1.0) {
        let animation = CABasicAnimation()
        animation.keyPath = "backgroundColor"
        animation.fillMode = kCAFillModeForwards
        animation.duration = duration
        animation.repeatCount = 1
        animation.autoreverses = false
        animation.fromValue = self.backgroundColor?.cgColor
        animation.toValue = color.darker().cgColor
        animation.delegate = self;

        let shapeAnimation = CABasicAnimation()
        shapeAnimation.keyPath = "fillColor"
        shapeAnimation.fillMode = kCAFillModeForwards
        shapeAnimation.duration = duration
        shapeAnimation.repeatCount = 1
        shapeAnimation.autoreverses = false
        shapeAnimation.fromValue = self.sublayer.fillColor
        shapeAnimation.toValue = color.cgColor
        shapeAnimation.delegate = self;


        self.layer.add(animation, forKey: "kAnimation")
        self.sublayer.add(shapeAnimation, forKey: "kAnimation")
        self.color = color
    }



    public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        self.layer.removeAnimation(forKey: "kAnimation")
        self.sublayer.removeAnimation(forKey: "kAnimation")
    }
}

您可以使用方法 animation(color: duration:) 在所需的动画持续时间内更改动画颜色。

例子

let view = SquareView(frame: CGRect(x: 10, y: 100, width: 200, height: 300))
view.color = UIColor.blue
view.animation(color: UIColor.red) 

这是结果