Здравствуйте, меня зовут Виктор, я работаю в компании Exyte. Недавно мы выложили в open source нашу внутреннюю разработку — библиотеку для работы с векторной графикой и ее анимации Macaw. Я хочу поделиться впечатлениями от применения ее в реальном проекте и рассказать о ее преимуществах над нативным API.


Как разработчикам, нам часто приходится создавать нестандартные контролы и повторять одни и те же рутинные действия даже для простых эффектов:


  • Отнаследоваться от UIView, чтобы переопределить drawRect
  • Описать "сцену" используя устаревший Core Graphics API

Давайте попробуем создать нестандартный контрол и используем его как пример:


ControlImage



let circlePath1 = UIBezierPath(
            arcCenter: center,
            radius: r,
            startAngle: offset, endAngle: offset + CGFloat(angle), clockwise: true)
let circlePath2 = UIBezierPath(
            arcCenter: center,
            radius: r,
            startAngle: offset, endAngle: offset + CGFloat(angle), clockwise: false)

UIColor.init(colorLiteralRed: 0.784, green: 0.784, blue: 0.784, alpha: 1.0).setStroke()
UIColor.clear.setFill()
circlePath1.lineWidth = 10.0
circlePath1.stroke()

UIColor.white.setStroke()
circlePath2.lineWidth = 10.0
circlePath2.stroke()

Мы решили избавиться от этой рутины и создали Macaw. С помощью нее мы можем описать сцену выше в простом, функциональном стиле:


let circle = Circle(cx: Double(center.x), cy: Double(center.y), r: r)

self.node = [
     circle.arc(shift: -1.0 * M_PI_2, extent: angle)
                .stroke(fill: Color.rgb(r: 200, g: 200, b: 200), width: 10.0),
     circle.arc(shift: -1.0 * M_PI_2 + angle, extent: 2 * M_PI - angle)
                .stroke(fill: Color.white, width: 10.0)
    ].group()

Controls


Слева: Core Graphics, справа: Macaw


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


let sceneNode = [
    shape1,
    shape2,
    ...
    [
        subshape1,
        subshape2
        ....
        [...].group()
    ].group(place: Transform.move(dx: 10.0, dy: 10.0).scale(sx: 0.5, sy: 0.5), opacity: 0.9)
].group()

Сложно даже представить сколько потребуется усилий, чтобы создать подобную сцену, используя "чистый" Core Graphics API. Чтобы использовать эту красоту, достаточно просто отнаследоваться от MacawView, или использовать MacawView как контейнер. После этого начнется "магия" из коробки, например, изменения модели автоматом стриггерят перерисовку контента.


Но чтобы создать красивый эффект, нужна еще одна вещь — анимация. Используя Core Graphics, у нас есть два пути:


  • CAShapeLayer в комбинации CABasicAnimation. Это достаточно просто, но код, все же, выглядит устаревшим:

let scaleTransform = CGAffineTransform.init(scaleX: 0.1, y: 0.1)
let scaleAnimation = CABasicAnimation(keyPath: "transform")
scaleAnimation.toValue = CATransform3DMakeAffineTransform(scaleTransform)
scaleAnimation.duration = 1.0
scaleAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
scaleAnimation.autoreverses = true

self.layer.add(scaleAnimation, forKey: "scale_animation")

  • Использовать коллбэк от CADisplayLink, для того, чтобы вручную перерисовать контент UIView. Но, как разработчику, мне не хочется уходить в такие крайности для простого эффекта.

Используя Macaw, подобная анимация создается гораздо приятней:


let scaleAnimation = self.node.placeVar.animation(to: GeomUtils.centerScale(node: self.node, sx: 0.1, sy: 0.1), during: 2.0).easing(.easeOut)
scaleAnimation.autoreversed().play()

Animations


Да, это весь код. Есть ли оверхэд по сравнению с "чистым" Core Graphics? Под капотом Macaw использует CAKeyframeAnimation, и требуется небольшое время (зависит от сложности модели) для расчета фреймов анимции. В остальном — это те же вызовы Core Graphics.


Хорошо, а как на счет анимации для контента сцены. Есть ли возможность анимировать состояние объектов модели или анимированно заменить все дерево модели? К сожалению, нет возможности как-то оптимизировать этот процесс, единственное решение -перерисовать всю модель вручную. Хорошая новость — у Macaw для этого есть очень удобное API.


Давайте отрефакторим код нашего контрола так, чтобы создавать дерево объектов было проще:


contentNode(angle: Double) -> [Node] {
    ...

    let circle = Circle(cx: Double(center.x), cy: Double(center.y), r: r)
    let text = Text(text: "\(value)", font: Font(name: "System", size: 38), fill: Color.white)
    let textCenter = GeomUtils.center(node: text)
    text.place = Transform.move(dx: Double(center.x) - textCenter.x, dy: Double(center.y) - textCenter.y)

    return [
        text,
        circle.arc(shift: -1.0 * M_PI_2, extent: angle)
            .stroke(fill: Color.rgb(r: 200, g: 200, b: 200), width: 10.0),
        circle.arc(shift: -1.0 * M_PI_2 + angle, extent: 2 * M_PI - angle)
            .stroke(fill: Color.white, width: 10.0)
    ]
}

...

self.node = contentNode(angle: angle).group()

Теперь мы можем создать анимации контента, заменяя контент поддерева внутри модели каждый кадр:


guard let rootNode = self.node as? Group else {
    return
}

rootNode.contentsVar.animate({ t -> [Node] in
    return self.contentNode(angle: 2 * M_PI * t)
}, during: 2.0)

Content animation


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


Как видите, используя Macaw, мы можем достичь производительности "чистого" Core Graphics, но с более читаемым и легче поддерживаемым кодом. Многое осталось не разобранным, но надеюсь, что этот обзор воодушевит вас на использование нашей библиотеки в вашем проекте. Мы постоянно работаем над улучшениями и будем рады вашим советам и помощи.

Поделиться с друзьями
-->

Комментарии (6)


  1. RedRover
    29.12.2016 12:58

    устаревший Core Graphics API


    Устаревший и низкоуровневый все таки совсем разные вещи, по такой терминологии Macaw тоже устаревший т.к. рисует все методами «устаревшего» Core Graphics API


    1. Ordohydra
      29.12.2016 16:25

      Согласен с Вами, низкоуровневый более удачный термин.


  1. sashabeep
    29.12.2016 13:58

    А какой размер у такого контрола? Во весь экран?
    Во всяких музыкальных программах часто используются ручки, и в большинстве случаев мышь не надо таскать по кругу, потому что это нецелесообразно


    1. Ordohydra
      29.12.2016 16:28

      Размер можно сделать любым, в моем случае это был interval picker 150x150 пикселей.


      1. sashabeep
        29.12.2016 16:29

        Маловато для того, чтобы крутить пальцем, свайп будет работать адекватнее


  1. AlexMorgun
    29.12.2016 20:26

    Спасибо, за статью.
    P.S. Картинки пропали, может стоит перезалить на https://habrastorage.org?