【问题标题】:How to make the background color property of a custom view animatable?如何使自定义视图的背景颜色属性可动画化?
【发布时间】:2017-09-23 15:42:26
【问题描述】:

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

我的绘图代码很简单。 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 方法只返回调用它的颜色的较暗版本。

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

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

我第一次尝试:

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 的可可豆荚。它是这样实现的:

// 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
}

【问题讨论】:

  • 嗯,只有 3 个点的形状不可能是正方形。 move(to:..) 在不绘制的情况下移动光标,然后你有两个 addLine(to:...) 绘制两侧,你似乎至少缺少一侧,因此一个形状是闭合的,它形成一个正方形(前提是您的意图)。
  • 至于动画颜色,您是在向您的视图指示它需要刷新自身,但没有任何东西将方形颜色绑定到图层背景颜色。
  • @ekscrypto 我的意图是绘制一个水平倒置的“L”形。你的意思是我应该画出倒“L”形的六个边并填充?
  • 我对 L 形状不是 100% 确定,我还没有做足够多的 CALayer 动画来说明。你要么必须确保你的形状是封闭的,要么告诉它不要自动关闭路径;我无法回答。

标签: ios swift animation calayer


【解决方案1】:

CABasicAnimation 让您走在正确的轨道上。

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

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

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

【讨论】:

  • 我画的倒“L”形呢?我怎样才能改变它的颜色?
  • 你能提供你对darker的实现吗?
  • 已编辑。请检查。
【解决方案2】:

你能用半透明的黑色代替 L 形吗?

那你就不用再为重绘和颜色计算乱七八糟了,可以使用基本的 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()
    }
}

【讨论】:

  • 这行得通!我最终创建了另一个名为LShapedOverlayUIView 子类,并将其添加为SquareView 的子视图。然后我可以使用UIView.animate
  • 您可以编辑答案以添加一些代码,以便我可以给您赏金吗?
【解决方案3】:

使用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()
    }
}

注意:我已经编辑了答案,以便形状始终适合从情节提要添加的父视图

【讨论】:

  • 很抱歉没有正确描述我的目标。我想要的不是一个会改变颜色的三角形。我想要一个方形视图,带有一个变暗的倒 L 形区域,逐渐改变颜色。请参阅我的问题中的前两张图片。
【解决方案4】:

我有一个使用动画更改视图背景颜色的实现,并在源代码中进行了一些调整,以使用 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) 

这是结果

【讨论】:

    【解决方案5】:

    我们在下面的代码中有两个不同的 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)
        }
    }
    

    这是结果,

    【讨论】:

      【解决方案6】:

      目标-C

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

      斯威夫特

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

      【讨论】:

      • 这与我的第一次尝试(没有成功)有何不同?
      • 为什么要更新 Objective-C 代码层,而在 Swift 代码中直接操作视图?同样,只是没有任何解释的代码也不能成为一个好的答案。
      猜你喜欢
      • 2011-02-06
      • 2022-08-06
      • 2011-06-09
      • 2012-05-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-01-02
      • 2017-04-30
      相关资源
      最近更新 更多