Слой Animate UIView со связями (анимация автоматического макета) - PullRequest
0 голосов
/ 10 января 2019

Я работаю над проектом, в котором мне нужно анимировать высоту обзора, состоящую из тени, градиента и закругленных углов (не всех углов). Поэтому я использовал layerClass свойство зрения.

Ниже приведен простой пример демонстрации. Проблема в том, что когда я изменял высоту вида, изменяя его ограничение, это приводило к немедленной анимации класса слоя, что немного неловко.

Ниже приведен пример кода

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(), что приведет к пересчету границ теневого слоя.

Итак, я проверил, если вид в настоящее время анимируется, тогда не пересчитывайте границы слоя тени.

Правильный ли этот подход? или есть лучший способ сделать это?

Ответы [ 2 ]

0 голосов
/ 11 января 2019

Код ниже также работал для меня, так как я хочу использовать подпредставления макета без каких-либо флагов.

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
    }
0 голосов
/ 10 января 2019

Я знаю, что это сложный вопрос. На самом деле, вам вообще не нужно заботиться о 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
    } 
   ....

Тогда тебе пора. Хорошего дня.

...