Наскільки зручним може бути API для малювання в iOS?

Привіт, мене звати Віктор, я працюю в компанії 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, але з більш читабельним і легше підтримується кодом. Багато чого залишилося не розібраним, але сподіваюся, що цей огляд надихне вас на використання нашої бібліотеки у вашому проекті. Ми постійно працюємо над поліпшенням і будемо раді ваших порад і допомоги.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.