带约束的动画 UIView 层(自动布局动画)

Animate UIView's Layer with constrains (Auto Layout Animations)

我正在做一个项目,我需要为由阴影、渐变和圆角(不是所有角)组成的视图高度设置动画。 所以我用了layerClass属性的观点。

下面是简单的示例演示。 问题是,当我通过修改它的约束来改变视图的高度时,它会导致层 class 的立即动画,这有点尴尬。

下面是我的示例代码

import UIKit

class CustomView: UIView{

    var isAnimating: Bool = false

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

    func setupView(){

        self.layoutMargins = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)

        guard let layer = self.layer as? CAShapeLayer else { return }

        layer.fillColor = UIColor.green.cgColor
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
        layer.shadowOpacity = 0.6
        layer.shadowRadius = 5

    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override class var layerClass: AnyClass {
        return CAShapeLayer.self
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // While animating `myView` height, this method gets called
        // So new bounds for layer will be calculated and set immediately
        // This result in not proper animation

        // check by making below condition always true

        if !self.isAnimating{ //if true{
            guard let layer = self.layer as? CAShapeLayer else { return }

            layer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 10).cgPath
            layer.shadowPath = layer.path
        }
    }

}

class TestViewController : UIViewController {

    // height constraint for animating height
    var heightConstarint: NSLayoutConstraint?

    var heightToAnimate: CGFloat = 200

    lazy var myView: CustomView = {
        let view = CustomView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .clear
        return view
    }()

    lazy var mySubview: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .yellow
        return view
    }()

    lazy var button: UIButton = {
        let button = UIButton(frame: .zero)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.setTitle("Animate", for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.addTarget(self, action: #selector(self.animateView(_:)), for: .touchUpInside)
        return button
    }()


    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.backgroundColor = .white
        self.view.addSubview(self.myView)
        self.myView.addSubview(self.mySubview)
        self.view.addSubview(self.button)

        self.myView.leadingAnchor.constraint(equalTo: self.view.layoutMarginsGuide.leadingAnchor).isActive = true
        self.myView.trailingAnchor.constraint(equalTo: self.view.layoutMarginsGuide.trailingAnchor).isActive = true
        self.myView.topAnchor.constraint(equalTo: self.view.layoutMarginsGuide.topAnchor, constant: 100).isActive = true
        self.heightConstarint = self.myView.heightAnchor.constraint(equalToConstant: 100)
        self.heightConstarint?.isActive = true

        self.mySubview.leadingAnchor.constraint(equalTo: self.myView.layoutMarginsGuide.leadingAnchor).isActive = true
        self.mySubview.trailingAnchor.constraint(equalTo: self.myView.layoutMarginsGuide.trailingAnchor).isActive = true
        self.mySubview.topAnchor.constraint(equalTo: self.myView.layoutMarginsGuide.topAnchor).isActive = true
        self.mySubview.bottomAnchor.constraint(equalTo: self.myView.layoutMarginsGuide.bottomAnchor).isActive = true

        self.button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
        self.button.bottomAnchor.constraint(equalTo: self.view.layoutMarginsGuide.bottomAnchor, constant: -20).isActive = true
    }

    @objc func animateView(_ sender: UIButton){

        CATransaction.begin()
        CATransaction.setAnimationDuration(5.0)
        CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut))

        UIView.animate(withDuration: 5.0, animations: {

            self.myView.isAnimating = true
            self.heightConstarint?.constant = self.heightToAnimate
            // this will call `myView.layoutSubviews()`
            // and layer's new bound will set automatically
            // this causing layer to be directly jump to height 200, instead of smooth animation
            self.view.layoutIfNeeded()

        }) { (success) in
            self.myView.isAnimating = false
        }

        let shadowPathAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.shadowPath))
        let pathAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.path))

        let toValue = UIBezierPath(
            roundedRect:CGRect(x: 0, y: 0, width: self.myView.bounds.width, height: heightToAnimate),
            cornerRadius: 10
        ).cgPath


        shadowPathAnimation.fromValue = self.myView.layer.shadowPath
        shadowPathAnimation.toValue = toValue

        pathAnimation.fromValue = (self.myView.layer as! CAShapeLayer).path
        pathAnimation.toValue = toValue


        self.myView.layer.shadowPath = toValue
        (self.myView.layer as! CAShapeLayer).path = toValue

        self.myView.layer.add(shadowPathAnimation, forKey: #keyPath(CAShapeLayer.shadowPath))
        self.myView.layer.add(pathAnimation, forKey: #keyPath(CAShapeLayer.path))

        CATransaction.commit()

    }

}

在动画视图时,它将调用它的 layoutSubviews() 方法,这将导致重新计算阴影层的边界。

所以我检查了视图当前是否正在设置动画,然后不重新计算阴影层的边界。

这种做法对吗?或者有更好的方法吗?

我知道这是一个棘手的问题。实际上,您根本不需要关心 layoutSubViews。这里的关键是当你设置 shapeLayer 的时候。如果它设置得很好,即在约束全部起作用之后,您不需要在动画期间关心它。

//在CustomView中,注释掉layoutSubViews(),添加updateLayer()

 func updateLayer(){
    guard let layer = self.layer as? CAShapeLayer else { return }
    layer.path = UIBezierPath(roundedRect: layer.bounds, cornerRadius: 10).cgPath
    layer.shadowPath = layer.path
}

  //    override func layoutSubviews() {
  //        super.layoutSubviews()
  //
  //        // While animating `myView` height, this method gets called
  //        // So new bounds for layer will be calculated and set immediately
  //        // This result in not proper animation
  //
 //        // check by making below condition always true
 //
 //        if !self.isAnimating{ //if true{
//            guard let layer = self.layer as? CAShapeLayer else { return }
//
//            layer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 10).cgPath
//            layer.shadowPath = layer.path
//        }
//    }

在ViewController中:添加viewDidAppear()并移除其他动画块

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    myView.updateLayer()
}

@objc func animateView(_ sender: UIButton){

    CATransaction.begin()
    CATransaction.setAnimationDuration(5.0)
    CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut))

    UIView.animate(withDuration: 5.0, animations: {

        self.heightConstarint?.constant = self.heightToAnimate
        // this will call `myView.layoutSubviews()`
        // and layer's new bound will set automatically
        // this causing layer to be directly jump to height 200, instead of smooth animation
        self.view.layoutIfNeeded()

    }) { (success) in
        self.myView.isAnimating = false
    } 
   ....

那你就可以开始了。祝你有美好的一天。

下面的代码也适用于我,因为我想使用不带任何标志的布局子视图。

UIView.animate(withDuration: 5.0, animations: {

            let shadowPathAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.shadowPath))
            let pathAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.path))

            let toValue = UIBezierPath(
                roundedRect:CGRect(x: 0, y: 0, width: self.myView.bounds.width, height: self.heightToAnimate),
                cornerRadius: 10
                ).cgPath


            shadowPathAnimation.fromValue = self.myView.layer.shadowPath
            shadowPathAnimation.toValue = toValue

            pathAnimation.fromValue = (self.myView.layer as! CAShapeLayer).path
            pathAnimation.toValue = toValue

            self.heightConstarint?.constant = self.heightToAnimate
            self.myView.layoutIfNeeded()

            self.myView.layer.shadowPath = toValue
            (self.myView.layer as! CAShapeLayer).path = toValue

            self.myView.layer.add(shadowPathAnimation, forKey: #keyPath(CAShapeLayer.shadowPath))
            self.myView.layer.add(pathAnimation, forKey: #keyPath(CAShapeLayer.path))

            CATransaction.commit()

        })

并重写 layoutSubview 如下

override func layoutSubviews() {
        super.layoutSubviews()

        guard let layer = self.layer as? CAShapeLayer else { return }

        layer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 10).cgPath
        layer.shadowPath = layer.path
    }