CABasicAnimation для эмуляции анимации эффекта «импульса» на форме, отличной от круга.

Я использую CBasicAnimation для создания пульсирующего эффекта на кнопке. Эффект пульсирует в форме UIView только с границей.

Хотя анимация работает правильно, я не получаю желаемого эффекта, используя CABasicAnimation(keyPath: "transform.scale").

Я использую группу анимаций с тремя анимациями: borderWidth, transform.scale и opacity.

class Pulsing: CALayer {

var animationGroup = CAAnimationGroup()

var initialPulseScale:Float = 1
var nextPulseAfter:TimeInterval = 0
var animationDuration:TimeInterval = 1.5
var numberOfPulses:Float = Float.infinity


override init(layer: Any) {
    super.init(layer: layer)
}

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


init (numberOfPulses:Float = Float.infinity, position:CGPoint, pulseFromView:UIView, rounded: CGFloat) {
    super.init()


    self.borderColor = UIColor.black.cgColor



    self.contentsScale = UIScreen.main.scale
    self.opacity = 1
    self.numberOfPulses = numberOfPulses
    self.position = position

    self.bounds = CGRect(x: 0, y: 0, width: pulseFromView.frame.width, height: pulseFromView.frame.height)
    self.cornerRadius = rounded


    DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
        self.setupAnimationGroup(view: pulseFromView)

        DispatchQueue.main.async {
             self.add(self.animationGroup, forKey: "pulse")
        }
    }



}

func borderWidthAnimation() -> CABasicAnimation {
    let widthAnimation = CABasicAnimation(keyPath: "borderWidth")
    widthAnimation.fromValue = 2
    widthAnimation.toValue = 0.5
    widthAnimation.duration = animationDuration

    return widthAnimation
}


func createScaleAnimation (view:UIView) -> CABasicAnimation {
    let scale = CABasicAnimation(keyPath: "transform.scale")
    DispatchQueue.main.async {
            scale.fromValue = view.layer.value(forKeyPath: "transform.scale")
    }

    scale.toValue = NSNumber(value: 1.1)
    scale.duration = 1.0
    scale.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)

    return scale
}

func createOpacityAnimation() -> CABasicAnimation {
    let opacityAnimation = CABasicAnimation(keyPath: "opacity")
    opacityAnimation.duration = animationDuration
    opacityAnimation.fromValue = 1
    opacityAnimation.toValue = 0
    opacityAnimation.fillMode = .removed

    return opacityAnimation
}

func setupAnimationGroup(view:UIView) {
    self.animationGroup = CAAnimationGroup()
    self.animationGroup.duration = animationDuration + nextPulseAfter
    self.animationGroup.repeatCount = numberOfPulses


    self.animationGroup.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.default)

    self.animationGroup.animations = [createScaleAnimation(view: view), borderWidthAnimation(), createOpacityAnimation()]
}



}



class ViewController: UIViewController {


@IBOutlet weak var pulsingView: UIView!

let roundd:CGFloat = 20

override func viewDidLoad() {
    super.viewDidLoad()
    pulsingView.layer.cornerRadius = roundd
    let pulse = Pulsing(
        numberOfPulses: .greatestFiniteMagnitude,
        position: CGPoint(x: pulsingView.frame.width/2,
                          y: pulsingView.frame.height/2)
        , pulseFromView: pulsingView, rounded: roundd)

    pulse.zPosition = -10
    self.pulsingView.layer.insertSublayer(pulse, at: 0)
}




}

Моя проблема заключается в том, что transform.scale поддерживает соотношение сторон UIView, от которого он пульсирует во время анимации.

Как сделать так, чтобы импульс рос так, чтобы интервалы были одинаковыми как по высоте, так и по ширине? Смотрите скриншот.

Масштабирование кнопки


person Joe    schedule 01.05.2020    source источник
comment
поделитесь, пожалуйста, своим путем....   -  person Jawad Ali    schedule 01.05.2020
comment
Путь — это просто UIView с фиксированной шириной/высотой и закругленными углами. Скругленные углы даже не нужны. Квадратный UIView должен давать такие же результаты.   -  person Joe    schedule 01.05.2020
comment
Вы можете также опубликовать этот код? мне нужно запустить его с моей стороны   -  person Jawad Ali    schedule 01.05.2020
comment
@jawadAli - теперь все опубликовано.   -  person Joe    schedule 02.05.2020
comment
вы масштабируете пульс, который является CALayer... но не вид... который в белом цвете   -  person Jawad Ali    schedule 03.05.2020
comment
Неважно, что я масштабирую... CALayer или View. Проблема в расстоянии вокруг пульса. Опубликуйте ответ, если считаете, что у вас есть решение проблемы с интервалом.   -  person Joe    schedule 04.05.2020


Ответы (1)


Масштабирование ширины и высоты с одинаковым коэффициентом приведет к неравному отступу по краям. Вам нужно увеличить ширину и высоту слоя на одно и то же значение. Это операция сложения, а не умножения. Теперь для этого пульсирующего эффекта вам нужно анимировать границы слоя.

Если вы хотите, чтобы интервал между краями был динамическим, выберите масштабный коэффициент и примените его к одному измерению. Выбираете ли вы ширину или высоту, не имеет значения, пока это применяется только к одному. Допустим, вы выбираете ширину для увеличения в 1,1 раза. Вычислите целевую ширину, затем вычислите дельту.

let scaleFactor: CGFloat = 1.1
let targetWidth = view.bounds.size.width * scaleFactor
let delta = targetWidth - view.bounds.size.width

Получив дельту, примените ее к границам слоя в измерениях x и y. Воспользуйтесь методом insetBy(dx:) для вычисления результирующего прямоугольника.

let targetBounds = self.bounds.insetBy(dx: -delta / 2, dy: -delta / 2)

Для ясности я переименовал ваш метод createScaleAnimation(view:) в createExpansionAnimation(view:). Связывая все вместе, мы имеем:

func createExpansionAnimation(view: UIView) -> CABasicAnimation {

    let anim = CABasicAnimation(keyPath: "bounds")

    DispatchQueue.main.async {

        let scaleFactor: CGFloat = 1.1
        let targetWidth = view.bounds.size.width * scaleFactor
        let delta = targetWidth - view.bounds.size.width

        let targetBounds = self.bounds.insetBy(dx: -delta / 2, dy: -delta / 2)

        anim.duration = 1.0
        anim.fromValue = NSValue(cgRect: self.bounds)
        anim.toValue = NSValue(cgRect: targetBounds)
    }

    return anim
}
person Rob    schedule 04.05.2020
comment
Отличный ответ. Ваше объяснение имело смысл, и ваше решение сработало идеально. (Придется подождать еще 11 часов, чтобы наградить вас наградой). Спасибо за вашу помощь! - person Joe; 04.05.2020
comment
@Joe Я хотел упомянуть об этом, но вам действительно не нужно использовать DispatchQueues. Ваш код значительно упростился бы, если бы вы просто сохранили все в основном потоке. - person Rob; 04.05.2020
comment
Похоже, мне это нужно. Без него происходит сбой: Средство проверки основного потока: UI API вызывается в фоновом потоке: -[UIView bounds] в этой строке: let targetWidth = view.bounds.size.width * scaleFactor - person Joe; 05.05.2020
comment
@Joe Причина сбоя в этой строке: DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async. Если вы удалите этот и другие вызовы DispatchQueue, все будет хорошо. - person Rob; 05.05.2020