Глюк при запуске CABasicAnimation

У меня есть анимация масштабирования/импульса, которая работает с cgPaths в цикле for, как показано ниже. Этот код работает, но только когда вы добавляете animatedLayers к пути circleLayer, когда circleLayer уже добавлено к subLayer, и это создает статический круг (подобный сбоям) перед началом анимации (DispatchQueue)...

...
self.layer.addSublayer(animatedLayer)
animatedLayers.append(animatedLayer)
...

введите здесь описание изображения

Можно ли добавить CAShapeLayer с аргументами в подуровень? Если нет, то рекомендуемая альтернатива?

import UIKit
import Foundation

@IBDesignable
class AnimatedCircleView: UIView {

    // MARK: - Initializers

    var animatedLayers = [CAShapeLayer]()

    // MARK: - Methods

    override func draw(_ rect: CGRect) {

        // Animated circle
        for _ in 0...3 {
            let animatedPath = UIBezierPath(arcCenter: .zero, radius: self.layer.bounds.size.width / 2.3,
            startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
            let animatedLayer = CAShapeLayer()

            animatedLayer.path = animatedPath.cgPath
            animatedLayer.strokeColor = UIColor.black.cgColor
            animatedLayer.lineWidth = 0
            animatedLayer.fillColor = UIColor.gray.cgColor
            animatedLayer.lineCap = CAShapeLayerLineCap.round
            animatedLayer.position = CGPoint(x: self.layer.bounds.size.width / 2, y: self.layer.bounds.size.width / 2)
            self.layer.addSublayer(animatedLayer)
            animatedLayers.append(animatedLayer)
        }

        // Dispatch animation for circle _ 0...3
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            self.animateCircle(index: 0)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                self.animateCircle(index: 1)
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                    self.animateCircle(index: 2)
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
                        self.animateCircle(index: 3)
                    }
                }
            }
        }
    }


    func animateCircle(index: Int) {

        let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
        scaleAnimation.duration = 1.8
        scaleAnimation.fromValue = 0
        scaleAnimation.toValue = 1
        scaleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
        scaleAnimation.repeatCount = Float.infinity
        animatedLayers[index].add(scaleAnimation, forKey: "scale")

        let opacityAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
        opacityAnimation.duration = 1.8
        opacityAnimation.fromValue = 0.7
        opacityAnimation.toValue = 0
        opacityAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
        opacityAnimation.repeatCount = Float.infinity
        animatedLayers[index].add(opacityAnimation, forKey: "opacity")
    }
}

person axelmukwena    schedule 30.03.2020    source источник


Ответы (2)


Ключевая проблема заключается в том, что ваша анимация запускается с задержкой от 0,1 до 1,0 секунды. Пока не начнется эта последняя анимация, этот слой просто сидит там, в полном размере и со 100% непрозрачностью.

Поскольку вы анимируете шкалу преобразования от 0 до 1, я предлагаю установить начальное преобразование на 0 (или изменить opacity на 0). Тогда вы не увидите их сидящими там, пока не начнется их соответствующая анимация.

Несколько других наблюдений:

  • draw(_:) — неподходящее место для добавления слоев, запуска анимации и т. д. Этот метод можно вызывать несколько раз, и он должен представлять представление в данный момент времени. Я бы удалил draw(_:), так как это неподходящее место для начала, и вам вообще не нужен этот метод.

  • Вы начинаете свою первую анимацию через 0,1 секунды. Почему бы не начать это немедленно?

  • Вы должны обрабатывать настройки кадра, откладывая настройку свойств path и position до layoutSubviews.

Таким образом:

@IBDesignable
class AnimatedCircleView: UIView {
    private var animatedLayers = [CAShapeLayer]()

    override init(frame: CGRect = .zero) {
        super.init(frame: frame)
        configure()
    }

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

    override func layoutSubviews() {
        super.layoutSubviews()

        let path = UIBezierPath(arcCenter: .zero, radius: bounds.width / 2.3, startAngle: 0, endAngle: 2 * .pi, clockwise: true)

        for animatedLayer in animatedLayers {
            animatedLayer.path = path.cgPath
            animatedLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
        }
    }
}

// MARK: - Methods

private extension AnimatedCircleView {
    func configure() {
        for _ in 0...3 {
            let animatedLayer = CAShapeLayer()
            animatedLayer.strokeColor = UIColor.black.cgColor
            animatedLayer.lineWidth = 0
            animatedLayer.fillColor = UIColor.gray.cgColor
            animatedLayer.lineCap = .round
            animatedLayer.transform = CATransform3DMakeScale(0, 0, 1)
            layer.addSublayer(animatedLayer)
            animatedLayers.append(animatedLayer)
        }

        self.animateCircle(index: 0)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            self.animateCircle(index: 1)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                self.animateCircle(index: 2)
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                    self.animateCircle(index: 3)
                }
            }
        }
    }

    func animateCircle(index: Int) {
        let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
        scaleAnimation.duration = 1.8
        scaleAnimation.fromValue = 0
        scaleAnimation.toValue = 1
        scaleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
        scaleAnimation.repeatCount = .greatestFiniteMagnitude
        animatedLayers[index].add(scaleAnimation, forKey: "scale")

        let opacityAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
        opacityAnimation.duration = 1.8
        opacityAnimation.fromValue = 0.7
        opacityAnimation.toValue = 0
        opacityAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
        opacityAnimation.repeatCount = .greatestFiniteMagnitude
        animatedLayers[index].add(opacityAnimation, forKey: "opacity")
    }
}
person Rob    schedule 30.03.2020
comment
Спасибо за решение, проблема была в настройке задержки отправки. И спасибо за настройку и структурные рекомендации. - person axelmukwena; 30.03.2020
comment
О, да, обычно я просто голосую «за», поэтому я подумал, что система автоматически помечает конкретный ответ как правильный в зависимости от количества голосов, которые он набрал. Спасибо за это. - person axelmukwena; 30.03.2020

Пробовали ли вы начать с прозрачного цвета заливки, а затем сделать его серым в начале анимации?

    // Animated circle
    for _ in 0...3 {
        let animatedPath = UIBezierPath(arcCenter: .zero, radius: self.layer.bounds.size.width / 2.3,
        startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
        let animatedLayer = CAShapeLayer()

        animatedLayer.path = animatedPath.cgPath
        animatedLayer.strokeColor = UIColor.black.cgColor
        animatedLayer.lineWidth = 0
        animatedLayer.fillColor = UIColor.clear.cgColor
        animatedLayer.lineCap = CAShapeLayerLineCap.round
        animatedLayer.position = CGPoint(x: self.layer.bounds.size.width / 2, y: self.layer.bounds.size.width / 2)
        self.layer.addSublayer(animatedLayer)
        animatedLayers.append(animatedLayer)
    }

    // Dispatch animation for circle _ 0...3
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {

        self.animateCircle(index: 0)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
            self.animateCircle(index: 1)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                self.animateCircle(index: 2)
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
                    self.animateCircle(index: 3)
                }
            }
        }
    }
}
person HalR    schedule 30.03.2020
comment
Я получаю сообщение об ошибке Use of unresolved identifier 'animatedLayer'. Это потому, что оператор находится вне цикла for? - person axelmukwena; 30.03.2020
comment
Да, это было далеко от меня. Вы должны полностью отключить эту строку. Ваша анимация, похоже, все равно устанавливает цвет для круга. Если он не устанавливает цвет как часть анимации - если он играет только с полупрозрачностью / альфой, установите цвет для круга в анимации. - person HalR; 30.03.2020