Анимировать содержимое CALayer в методе рисования - PullRequest
0 голосов
/ 08 января 2019

Хорошо, возможно, этот вопрос был задан 1000 раз на SO. И я, должно быть, попробовал 500 ответов. Ничто не работает, что заставляет меня думать, что это может быть невозможно.

Еще до того, как я начну, мне не нужно настраиваемое свойство с анимацией, я просто хочу нарисовать изменение границ. Большинство ответов относятся только к первому. Но что, если мой контент зависит только от размера слоя и ничего более?

Разве это не возможно? Я попробовал:

  • override class func needsDisplay(forKey key: String) -> Bool
  • needsDisplayOnBoundsChange
  • переопределяет каждый тип action(for:) или animation(for:) метод
  • contentMode = .redraw
  • тонн хаков, таких как переопределение setBounds и добавление неявной анимации (что, кстати, бесполезно, так как слой имеет анимацию, он просто не перерисовывается при изменении размера)
  • может быть, пару других вещей я забыл, потратил немало времени на это

Самое дальнее, что я достиг: содержимое прорисовывается в начале анимации, а затем в конце. Что, конечно, выглядит плохо (также пиксельно) при переходе от большого к маленькому.

Не обязательно важно, но вот метод рисования для справки:

override func draw(in ctx: CGContext) {
        UIGraphicsPushContext(ctx)
        paths = [Dimension: [UIBezierPath]]()
        cuts = [Dimension: [UIBezierPath]]()

        let centerTranslate = CGAffineTransform.init(translationX: frame.width * 0.5,
                                                     y: frame.height * 0.5)
        let dimRotateAndTranslate = centerTranslate.rotated(by: CGFloat.pi / 2)

        let needlePath = UIBezierPath(rect: CGRect(x: 0,
                                                   y: 0,
                                                   width: circularSpacing,
                                                   height: frame.height * 0.5 - centerRadius))

        let piProgress = CGFloat(progress * 2 * Float.pi)
        var needleRotateAndTranslate = CGAffineTransform(rotationAngle: piProgress)
        needleRotateAndTranslate = needleRotateAndTranslate.translatedBy(x: -needlePath.bounds.width * 0.5,
                                                                         y: -needlePath.bounds.height - centerRadius)
        needlePath.apply(needleRotateAndTranslate.concatenating(centerTranslate))

        let sortedDims = states.sorted(by: {$0.key < $1.key})
        for (offset: i, element: (key: dim, value: beats)) in sortedDims.enumerated() {
            var pathsAtDim = paths[dim] ?? []
            var cutAtDim = cuts[dim] ?? []
            let thickness = (frame.width * 0.5 - centerRadius - circularSpacing * CGFloat(states.count - 1)) / CGFloat(states.count)
            let xSpacing = thickness + circularSpacing
            let radius = centerRadius + thickness * 0.5 + CGFloat(i) * xSpacing
            for (offset: j, beat) in beats.enumerated() {
                let length = CGFloat.pi * 2 / CGFloat(beats.count) * CGFloat(beat.relativeDuration)
                var path = UIBezierPath(arcCenter: .zero,
                                        radius: radius,
                                        startAngle: CGFloat(j) * length,
                                        endAngle: CGFloat(j + 1) * length,
                                        clockwise: true)
                path = UIBezierPath(cgPath: path.cgPath.copy(strokingWithWidth: thickness,
                                                             lineCap: .butt,
                                                             lineJoin: .miter,
                                                             miterLimit: 0,
                                                             transform: dimRotateAndTranslate))
                func cutAtIndex(index: Int, offset: CGFloat) -> UIBezierPath {
                    let cut = UIBezierPath(rect: CGRect(x: 0,
                                                        y: 0,
                                                        width: thickness,
                                                        height: circularSpacing))
                    let offsetRadians = offset / radius
                    let x = (radius - thickness / 2) * cos(CGFloat(index) * length + offsetRadians)
                    let y = (radius - thickness / 2) * sin(CGFloat(index) * length + offsetRadians)
                    let translate = CGAffineTransform.init(translationX: x,
                                                           y: y)
                    let rotate = CGAffineTransform.init(rotationAngle: CGFloat(index) * length)
                    cut.apply(rotate
                        .concatenating(translate)
                        .concatenating(dimRotateAndTranslate))
                    return cut
                }

                cutAtDim.append(cutAtIndex(index: j, offset: -circularSpacing * 0.5))

                pathsAtDim.append(path)
            }
            paths[dim] = pathsAtDim
            cuts[dim] = cutAtDim
        }

        let clippingMask = UIBezierPath.init(rect: bounds)
        for (dim, _) in states {
            for cut in cuts[dim] ?? [] {
                clippingMask.append(cut.reversing())
            }
        }
        clippingMask.addClip()
        for (dim, states) in states {
            for i in 0..<states.count {
                let state = states[i]
                if let path = paths[dim]?[i] {
                    let color = dimensionsColors[dim]?[state.state] ?? .darkGray
                    color.setFill()
                    path.fill()
                }
            }
        }

        ctx.resetClip()

        needleColor.setFill()
        needlePath.fill()

        UIGraphicsPopContext()
    }

1 Ответ

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

Прежде всего, вопрос не очень ясен во многих аспектах. Если мое понимание верно, вы хотели, чтобы изменение границ автоматически перерисовывало CALayer во время анимации.

Способ первый: Это может быть достигнуто общим способом, который включает в себя установку таймера и незначительное изменение границ (из массива CGRECT) и перерисовку каждый раз. Поскольку вы можете получить доступ к функции рисования, это не должно быть проблемой для разработки такой анимации самостоятельно.

Способ второй: Идея та же, что и раньше, но если вы хотите использовать способ CAAnimation. следующий фрагмент может помочь вам. В этом используется внешний таймер (CADisplayLink). Также изменение границ должно быть достигнуто с помощью функции updateBounds для удобства.

Имейте в виду, структура фрагмента должна быть тщательно обработана. Любая модификация может привести к потере анимации.

Успешный результат покажет вам продолжение перерисовки во время изменения границ.

Проверка viewController:

  class ViewController: UIViewController {

         override func viewDidAppear(_ animated: Bool) {
   super.viewDidAppear(animated)
    layer1 = MyLayer()
    layer1.frame = CGRect.init(x: 0, y: 0, width: 300, height: 500)
    layer1.backgroundColor = UIColor.red.cgColor
    view.layer.addSublayer(layer1)
    layer1.setNeedsDisplay()
}

  @IBAction   func updateLayer1(){
    CATransaction.begin()
    CATransaction.setAnimationDuration(3.0)

   let nextBounds = CGRect.init(origin: layer1.bounds.origin, size:    CGSize.init(width: layer1.bounds.width + 100, height: layer1.bounds.height + 150))
   layer1.updateBounds(nextBounds)
   CATransaction.commit()
   }
 }

Подкласс CALayer, перечисляющий только часть с анимацией изменения границ.

 class MyLayer: CALayer, CAAnimationDelegate{

func updateBounds(_ bounds: CGRect){
    nextBounds = bounds
    self.bounds = bounds
}
private var nextBounds : CGRect!

func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
    if let keyPath = (anim as! CABasicAnimation).keyPath  , let displayLink = animationLoops[keyPath]{
        displayLink?.invalidate()
      animationLoops.removeValue(forKey: keyPath)    }
}

@objc func updateDrawing(displayLink: CADisplayLink){
   bounds = presentation()!.bounds
   setNeedsDisplay()
}

 var animationLoops: [String : CADisplayLink?] = [:]

  override func action(forKey event: String) -> CAAction? {
    let result = super.action(forKey: event)
    if(event == "bounds"){
       guard let result = result  as? CABasicAnimation,
        animationLoops[event] == nil
        else{return nil}
       let anim = CABasicAnimation.init(keyPath: result.keyPath)
          anim.fromValue = result.fromValue
          anim.toValue = nextBounds
         let caDisplayLink = CADisplayLink.init(target: self, selector: #selector(updateDrawing))
        caDisplayLink.add(to: .main,   forMode: RunLoop.Mode.default)
        anim.delegate = self
        animationLoops[event] = caDisplayLink
        return anim
    }
  return result
}

Надеюсь, это то, что вы хотите. Хорошего дня.

Сравнение анимации: Без таймера:

enter image description here

Добавление таймера CADsiaplayLinker: enter image description here

...