CALayer с анимированным содержимым контура, возможность проверки/проектирования

Я создал собственное круговое представление прогресса, создав подкласс UIView, добавив подслой CAShapeLayer к его слою и переопределив drawRect(), чтобы обновить свойство path слоя формы.

Сделав представление @IBDesignable и свойство progress @IBInspectable, я смог отредактировать его значение в Interface Builder и увидеть обновленный путь Безье в реальном времени. Необязательно, но очень круто!

Затем я решил сделать путь анимированным: всякий раз, когда вы устанавливаете новое значение в коде, дуга, указывающая прогресс, должна «расти» от нулевой длины до любого процента круга, достигнутого (вспомните дуги в приложении «Активность» в Apple Watch). ).

Чтобы достичь этого, я заменил свой подслой CAShapeLayer на пользовательский подкласс CALayer, который имеет свойство @dynamic (@NSManaged), наблюдаемое как ключ для анимации (я реализовал needsDisplayForKey(), actionForKey(), drawInContext() и т. д.).

Мой код View (соответствующие части) выглядит примерно так:

// Triggers path update (animated)
private var progress: CGFloat = 0.0 {
    didSet {
        updateArcLayer()
    }
}

// Programmatic interface:
// (pass false to achieve immediate change)
func setValue(newValue: CGFloat, animated: Bool) {
    if animated {
        self.progress = newValue
    } else {
        arcLayer.animates = false
        arcLayer.removeAllAnimations()
        self.progress = newValue
        arcLayer.animates = true
    }
}

// Exposed to Interface Builder's  inspector: 
@IBInspectable var currentValue: CGFloat {
    set(newValue) {
        setValue(newValue: currentValue, animated: false)
        self.setNeedsLayout()
    }
    get {
        return progress
    }
}

private func updateArcLayer() {
    arcLayer.frame = self.layer.bounds
    arcLayer.progress = progress
}

И код layer:

var animates: Bool = true
@NSManaged var progress: CGFloat

override class func needsDisplay(forKey key: String) -> Bool {
    if key == "progress" {
        return true
    }
    return super.needsDisplay(forKey: key)
}

override func action(forKey event: String) -> CAAction? {
    if event == "progress" && animates == true {
        return makeAnimation(forKey: event)
    }
    return super.action(forKey: event)
}

override func draw(in ctx: CGContext) {
    ctx.beginPath()

    // Define the arcs...
    ctx.closePath()
    ctx.setFillColor(fillColor.cgColor)
    ctx.drawPath(using: CGPathDrawingMode.fill)
}

private func makeAnimation(forKey event: String) -> CABasicAnimation? {
    let animation = CABasicAnimation(keyPath: event)

    if let presentationLayer = self.presentation() {
        animation.fromValue = presentationLayer.value(forKey: event)
    }

    animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
    animation.duration = animationDuration
    return animation
}

Анимация работает, но теперь я не могу отобразить свои пути в Interface Builder.

Я попытался реализовать свое представление prepareForInterfaceBuilder() следующим образом:

override func prepareForInterfaceBuilder() {
    super.prepareForInterfaceBuilder()

    self.topLabel.text = "Hello, Interface Builder!"
    updateArcLayer()
}

... и изменение текста метки отражается в Interface Builder, но путь не отображается.

Я что-то упустил?


person Nicolas Miari    schedule 02.12.2016    source источник


Ответы (1)


Ну, разве это не забавно... Оказывается, в объявлении моего свойства @Inspectable была очень глупая ошибка.

Вы можете заметить это?

@IBInspectable var currentValue: CGFloat {
    set(newValue) {
        setValue(newValue: currentValue, animated: false)
        self.setNeedsLayout()
    }
    get {
        return progress
    }
}

Так должно быть:

@IBInspectable var currentValue: CGFloat {
    set(newValue) {
        setValue(newValue: newValue, animated: false)
        self.setNeedsLayout()
    }
    get {
        return progress
    }
}

То есть я отбрасывал переданное значение (newValue) и вместо этого использовал текущее (currentValue) для установки внутренней переменной. «Запись» значения (всегда 0,0) в Interface Builder (через свойство text моей метки!) дало мне подсказку.

Теперь он работает нормально, не нужно drawRect() и т.д.

person Nicolas Miari    schedule 02.12.2016
comment
drawRect() — проклятие существования Core Animation. Он должен быть изолирован от тех, кто использует Core Graphics, и исключен из всех аспектов использования Core Animation. Отличная работа! - person Confused; 13.12.2016