[Из песочницы] Насколько удобным может быть API для рисования в iOS?
Здравствуйте, меня зовут Виктор, я работаю в компании Exyte. Недавно мы выложили в open source нашу внутреннюю разработку — библиотеку для работы с векторной графикой и ее анимации Macaw. Я хочу поделиться впечатлениями от применения ее в реальном проекте и рассказать о ее преимуществах над нативным API.
Как разработчикам, нам часто приходится создавать нестандартные контролы и повторять одни и те же рутинные действия даже для простых эффектов:
- Отнаследоваться от UIView, чтобы переопределить drawRect
- Описать «сцену» используя устаревший Core Graphics API
Давайте попробуем создать нестандартный контрол и используем его как пример:
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()
Слева: 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()
Да, это весь код.
Есть ли оверхэд по сравнению с «чистым» 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)
Даже если сцена будет сложной, во время анимации перерисовываться будет только область изменяемого нами поддерева.
Как видите, используя Macaw, мы можем достичь производительности «чистого» Core Graphics, но с более читаемым и легче поддерживаемым кодом. Многое осталось не разобранным, но надеюсь, что этот обзор воодушевит вас на использование нашей библиотеки в вашем проекте. Мы постоянно работаем над улучшениями и будем рады вашим советам и помощи.