- PVSM.RU - https://www.pvsm.ru -
Привет! Современные интерфейсы мобильных приложений содержат тонны иллюстраций и анимаций, начиная от хитрых градиентов [1] и заканчивая диаграммами акций [2]. Поэтому мобильным разработчикам приходится тратить огромное количество времени, чтобы превратить красивый дизайн в функциональное приложение, которое, к тому же, будет работать на устройствах различных размеров.
Именно с этой проблемой мы столкнулись во время разработки iOS приложений. Чтобы упростить задачу, мы разработали графическую библиотеку Macaw [3], которая позволяет описывать сложные интерфейсы в виде понятных объектов сцены [4] и даже напрямую отображать SVG графику с поддержкой событий и анимации.
Интересно? В этой статье мы познакомим вас с базовыми понятиями Macaw и вместе создадим диаграмму с анимацией, используя минимум кода.
MacawView
– это основной класс, который используется для отображения всей графики Macaw в мире Cocoa. Чтобы начать работать с Macaw, нам необходимо создать свой класс, унаследовать его от MacawView и описать внутри необходимый интерфейс. Поскольку MacawView уже реализует UIView
, созданный нами класс можно будет легко интегрировать в интерфейс Cocoa. Вот так будет выглядеть простейший "Hello, World!" на Macaw:
class MyView: MacawView {
required init?(coder aDecoder: NSCoder) {
let text = Text(text: "Hello, World!", place: .move(dx: 145, dy: 100))
super.init(node: text, coder: aDecoder)
}
}
Macaw описывает интерфейсы в виде комбинации текста, изображений и геометрических объектов. Такая комбинация называется граф сцены [5] или же просто сцена. Давайте пройдемся по основным элементам сцены.
Shape – это элемент сцены, который представляет геометрическую фигуру. У этого элемента есть три основных свойства:
form
– геометрическое место точек, определяющее форму фигуры. Этим свойством мы определяем, что хотим нарисовать: прямоугольник, круг, полигон или что-то другое.fill
– цвета внутри фигурыstroke
– цвета границы вокруг фигурыДавайте рассмотрим простейший прямоугольник:
class MyView: MacawView {
required init?(coder aDecoder: NSCoder) {
let shape = Shape(form: Rect(x: 100, y: 75, w: 175, h: 30),
fill: Color(val: 0xfcc07c),
stroke: Stroke(fill: Color(val: 0xff9e4f), width: 2))
super.init(node: shape, coder: aDecoder)
}
}
Macaw использует стандартную систему координат Cocoa [6], поэтому на примере выше мы рисуем прямоугольник 175x30 точек в центре экрана iPhone 6/6s (ширина которого 375 точек). Для поддержки различных размеров экрана у нас есть несколько вариантов:
MacawView
и отцентрировать её с помощью autolayout в Cocoa.Также Macaw поддерживает и другие геометрические примитивы:
Помимо этого в Macaw есть элемент Path
, позволяющий описывать фигуры любой сложности в виде набора кривых.
Вернёмся к нашему примеру. Попробуем теперь добавить скругление нашему прямоугольнику:
let shape = Shape(
form: RoundRect(
rect: Rect(x: 100, y: 75, w: 175, h: 30),
rx: 5, ry: 5),
fill: Color(val: 0xfcc07c))
Такое описание сцены называется декларативным. При таком подходе мы описываем сцену, разбивая её на дерево примитивов. Macaw также позволяет описывать сцену в функциональном стиле. В этом случае, пример выше будет выглядеть следующим образом:
let shape = Rect(x: 100, y: 75, w: 175, h: 30).round(r: 5).fill(with: Color(val: 0xfcc07c))
Вы сами решаете, какой подход лучше использовать в каждом конкретном случае, однако мы рекомендуем использовать тот, который делает ваш код более читаемым.
Следующий базовый элемент сцены — это текст. Вот его основные свойства:
text
– текст для отображенияfill
– цвет текстаfont
– имя и размер шрифтаalign
/baseline
— свойства, отвечающие за выравнивание текстаclass MyView: MacawView {
required init?(coder aDecoder: NSCoder) {
let text = Text(text: "Sample",
font: Font(name: "Serif", size: 72),
fill: Color.blue)
super.init(node: text, coder: aDecoder)
}
}
Как вы могли заметить, у текста нет специальных свойств для положения, в отличии от Shape
. Однако у каждого элемента сцены есть свойство place
которое позволяет расположить элемент сцены относительно его родителя или даже повернуть и изменить его размер. Мы вернёмся к этому свойству позднее, а пока давайте просто добавим следующую строчку:
text.place = .move(dx: 100, dy: 75)
По умолчанию, текст располагается относительно верхнего левого угла. Чтобы отцентрировать текст, мы можем использовать свойство align
:
text.place = .move(dx: 375 / 2, dy: 75)
text.align = .mid
Для вертикального центрирования, мы также можем использовать свойство baseline
.
Теперь можно перейти к комбинированию элементов. Самое важное свойство группы – это contents
: список элементов, из которых состоит группа:
class MyView: MacawView {
required init?(coder aDecoder: NSCoder) {
let shape = Shape(
form: Rect(x: -100, y: -15, w: 200, h: 30).round(r: 3),
fill: Color(val: 0xff9e4f),
place: .move(dx: 375 / 2, dy: 75))
let text = Text(
text: "Show",
font: Font(name: "Serif", size: 21),
fill: Color.white,
align: .mid,
baseline: .mid,
place: .move(dx: 375 / 2, dy: 75))
let group = Group(contents: [shape, text])
super.init(node: group, coder: aDecoder)
}
}
Заметьте, что каждый элемент в группе мы определяем таким образом, чтобы центр элемента совпадал с началом координат (0, 0), а потом переносим эту точку в центр экрана с помощью .move(dx: 375 / 2, dy: 75)
. Однако, нам не обязательно делать это для каждого элемента, ведь теперь мы может перемещать саму группу:
class MyView: MacawView {
required init?(coder aDecoder: NSCoder) {
let shape = Shape(
form: Rect(x: -100, y: -15, w: 200, h: 30).round(r: 5),
fill: Color(val: 0xff9e4f))
let text = Text(
text: "Show",
font: Font(name: "Serif", size: 21),
fill: Color.white,
align: .mid,
baseline: .mid)
let group = Group(contents: [shape, text], place: .move(dx: 375 / 2, dy: 75))
super.init(node: group, coder: aDecoder)
}
}
Последний элемент в нашем арсенале — это изображение. У него есть следующие свойства:
src
– путь до файлаw
/h
– фактическая высота/ширина картинкиxAlign
/yAlign
/aspectRatio
– свойства для центрирования изображенияДавайте добавим изображение в нашу сцену:
let image = Image(src: "charts.png", w: 30, place: .move(dx: -55, dy: -15))
let group = Group(contents: [shape, text, image], place: .move(dx: 375 / 2, dy: 75))
По-умолчанию, высота и ширина берутся из размеров оригинального изображения. Если определено только одно свойство, тогда второе будет рассчитано автоматически в соответствии с пропорциями изображения.
В примерах выше мы уже использовали несколько вариантов задания цвета:
let color1 = Color.blue
let color2 = Color(val: 0xfcc07c)
В классе Color
можно найти и другие полезные методы:
let color3 = Color.rgb(r: 123, g: 17, b: 199)
let color4 = Color.rgba(r: 46, g: 142, b: 17, a: 0.2)
Также Macaw поддерживает линейные и радиальные градиенты, которые можно использовать для задания свойств fill
/stroke
у элементов сцены. Каждый градиент определяется набором цветов со смещениями. Пример градиента:
let fill = LinearGradient(
// определяем линейное направление градиента из точки (x1, y1) в точку (x2, y2)
// в данном случае это вертикальная линия сверху вниз
x1: 0, y1: 0, x2: 0, y2: 1,
// если параметр userSpace выставлен в true, то направление будет указано в системе координат текущего элемента
// иначе, направление будет задаваться в абстрактной системе координат, где
// (0,0) - это верхний левый угол области текущего элемента
// (1,1) - это нижний правый угол области текущего элемента
userSpace: false,
stops: [
// значения смещений должны быть в диапазоне 0 (старт) и 1 (финиш)
Stop(offset: 0, color: Color(val: 0xfcc07c)),
Stop(offset: 1, color: Color(val: 0xfc7600))])
Может показаться, что такое определение градиента выглядит громоздко, однако для простых градиентов можно использовать и более простые конструкторы. В частности, наш пример можно переписать следующим образом:
let fill = LinearGradient(degree: 90, from: Color(val: 0xfcc07c), to: Color(val: 0xfc7600))
Отсчет всех углов в Macaw идет по часовой стрелке и начинается с 3 часов. Поэтому 90 градусов – это как раз направление сверху вниз.
Давайте теперь вместо обычного цвета заполним нашу кнопку градиентом:
let shape = Shape(
form: Rect(x: -100, y: -15, w: 200, h: 30).round(r: 5),
fill: LinearGradient(degree: 90, from: Color(val: 0xfcc07c), to: Color(val: 0xfc7600)),
stroke: Stroke(fill: Color(val: 0xff9e4f), width: 1))
События позволяют пользователю взаимодействовать со сценой. Macaw может обрабатывать такие события, как tap
, rotate
и pan
. Добавим следующую строчку в конце метода init
:
_ = shape.onTap.subscribe(onNext: { event in text.fill = Color.maroon })
Теперь, как только пользователь кликнет по кнопке, она поменяет цвет на тёмно-бордовый.
Для событий Macaw использует очень мощную библиотеку RxSwift [7]. В частности, каждый метод subscribe
возвращает специальный протокол Disposable
, который позволяет удобно управлять всеми зарегистрированными слушателями. Поскольку в данном случае мы хотим обрабатывать событие всё время жизни фигуры, то мы просто используем _ =
чтобы это показать.
Если вы запустите наш пример, то заметите, что клик в середину кнопки не работает. Это возникает из-за того, что клик на фигуру кнопки перехватывает текст. Это можно легко починить, добавив такой же обработчик для текста кнопки и её картинки. Однако, более правильным решением будет указать, что эти элементы не могут получать события. Это можно легко сделать, используя свойство opaque
:
let text = Text(
text: "Show", font: Font(name: "Serif", size: 21),
fill: Color.white, align: .mid, baseline: .mid,
place: .move(dx: 15, dy: 0), opaque: false)
let image = Image(src: "charts.png", w: 30, place: .move(dx: -40, dy: -15), opaque: false)
Как мы уже видели, свойство place можно использовать для расположения любого элемента на сцене. На самом деле, это свойство является матрицей аффинных преобразований, позволяющей перенести точки из одной системы координат в другую. По сути, класс Transform в Macaw предоставляет интерфейс очень похожий на CGAffineTransform [8] из пакета Core Graphics, поэтому мы не будем детально на нём останавливаться. Для общего представления будет достаточно следующей анимации:
В Macaw нет прямой поддержки графиков и диаграмм, потому что их крайне легко сделать без дополнительных библиотек. Сначала мы немного наведём порядок в изменениях, которые делали всё это время. Вот что у нас в итоге получилось:
class MyView: MacawView {
required init?(coder aDecoder: NSCoder) {
let button = MyView.createButton()
super.init(node: Group(contents: [button]), coder: aDecoder)
}
private static func createButton() -> Group {
let shape = Shape(
form: Rect(x: -100, y: -15, w: 200, h: 30).round(r: 5),
fill: LinearGradient(degree: 90, from: Color(val: 0xfcc07c), to: Color(val: 0xfc7600)),
stroke: Stroke(fill: Color(val: 0xff9e4f), width: 1))
let text = Text(
text: "Show", font: Font(name: "Serif", size: 21),
fill: Color.white, align: .mid, baseline: .mid,
place: .move(dx: 15, dy: 0), opaque: false)
let image = Image(src: "charts.png", w: 30, place: .move(dx: -40, dy: -15), opaque: false)
return Group(contents: [shape, text, image], place: .move(dx: 375 / 2, dy: 75))
}
}
Теперь добавим оси координат чуть ниже нашей кнопки:
required init?(coder aDecoder: NSCoder) {
let button = MyView.createButton()
let chart = MyView.createChart(button.contents[0])
super.init(node: Group(contents: [button, chart]), coder: aDecoder)
}
private static func createChart(_ button: Node) -> Group {
var items: [Node] = []
for i in 1...6 {
let y = 200 - Double(i) * 30.0
items.append(Line(x1: -5, y1: y, x2: 275, y2: y).stroke(fill: Color(val: 0xF0F0F0)))
items.append(Text(text: "(i*30)", align: .max, baseline: .mid, place: .move(dx: -10, dy: y)))
}
items.append(createBars(button))
items.append(Line(x1: 0, y1: 200, x2: 275, y2: 200).stroke())
items.append(Line(x1: 0, y1: 0, x2: 0, y2: 200).stroke())
return Group(contents: items, place: .move(dx: 50, dy: 200))
}
private static func createBars(_ button: Node) -> Group {
// здесь мы будем рисовать нашу диаграмму
return Group()
}
А теперь пора добавить саму гистограмму:
static let data: [Double] = [101, 142, 66, 178, 92]
static let palette = [0xf08c00, 0xbf1a04, 0xffd505, 0x8fcc16, 0xd1aae3].map { val in Color(val: val)}
private static func createBars(_ button: Node) -> Group {
var items: [Node] = []
for (i, item) in data.enumerated() {
let bar = Shape(
form: Rect(x: Double(i) * 50 + 25, y: 0, w: 30, h: item),
fill: LinearGradient(degree: 90, from: palette[i], to: palette[i].with(a: 0.3)),
place: .move(dx: 0, dy: -data[i]))
items.append(bar)
}
return Group(contents: items, place: .move(dx: 0, dy: 200))
}
По сути, в методе createBars
мы превратили исходные данные в красивую гистограмму, и для этого нам понадобилось меньше 10 строк понятного, декларативного кода! Теперь время привести эту диаграмму в движение.
С точки зрения Macaw, анимация – это процесс изменения свойств сцены с течением времени. Если внимательно посмотреть на интерфейсы элементов сцены, то можно заметить, что помимо таких свойств как opacity
или place
, там есть ещё свойства opacityVar
и placeVar
. Эти свойства как раз можно использовать для анимации. Например, для анимации свойства opacity
, мы будем использовать свойство opacityVar
. Самый простой способ запустить анимацию – это вызвать функцию animate
:
node.opacityVar.animate(to: 0)
В этом случае, анимация начнётся сразу же и элемент node
будет постепенно исчезать в течении одной секунды, пока не пропадёт вовсе.
Можно представить любую анимацию как набор из трёх частей:
Macaw позволяет определить эту функцию самому, однако как правило проще определить ее с помощью трех значений:
from
– изначальное значение, которое будет установлено до начала анимации. Если не установлено, тогда будет использоваться текущее значение свойстваto
– финальное значениеeasing
– функция, которая определяет скорость изменения значений в зависимости от времениТеперь давайте добавим анимацию к нашей диаграмме. Сначала, добавим всем элементам гистограммы opacity: 0
, чтобы скрыть их и запустим нашу анимацию по нажатию на кнопку:
_ = button.onTap.subscribe(onNext: { _ in bar.opacityVar.animate(to: 1.0) })
Результат в действии:
Всего одной строчкой мы привели наше приложение в движение! Давайте теперь попробуем другой эффект: вместо постепенного появления, наши столбцы будут вырастать из координатной оси X. Для этого мы можем изменять масштаб элементов от нуля до оригинального значения.
let bar = Shape(
form: Rect(x: Double(i) * 50 + 25, y: 0, w: 30, h: item),
fill: LinearGradient(degree: 90, from: palette[i], to: palette[i].with(a: 0.3)),
// масштабируем y до 0
place: .scale(sx: 1, sy: 0))
items.append(bar)
_ = button.onTap.subscribe(onNext: { _ in
// анимируем до оригинального значения
bar.placeVar.animate(to: .move(dx: 0, dy: -data[i]))
})
Помимо этого, мы можем показывать разные столбцы с разной задержкой. Для этого мы будем использовать параметр delay
:
bar.placeVar.animate(to: .move(dx: 0, dy: -data[i]), delay: Double(i) * 0.1)
Вуаля! Теперь при клике на кнопку Show мы увидим желаемое:
Как мы уже упоминали ранее, в Macaw есть встроенная поддержка SVG. Вы можете использовать метод SVGParser.parse
чтобы прочитать SVG файл как элемент сцены, который можно комбинировать с другими элементами, или же передать напрямую в MacawView.
class SVGTigerView: MacawView {
required init?(coder aDecoder: NSCoder) {
super.init(node: SVGParser.parse(path: "tiger"), coder: aDecoder)
}
}
Усвоив базовые концепции Macaw, можно создавать ещё более интересные примеры. Например, за несколько часов нам удалось получить следующее:
Больше информации о проекте вы можете найти на нашей страничке github [3]. Мы активно работаем над документацией и новыми примерами, ждите обновлений!
Автор: ystrot
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios/203135
Ссылки в тексте:
[1] хитрых градиентов: https://itunes.apple.com/us/app/fresh-air-beautiful-personalized/id933439687?mt=8
[2] диаграммами акций: https://itunes.apple.com/us/app/portfolio-finance-news-stock/id974217998?mt=8
[3] Macaw: https://github.com/exyte/macaw
[4] сцены: https://en.wikipedia.org/wiki/Scene_graph
[5] граф сцены: https://ru.wikipedia.org/wiki/%D0%93%D1%80%D0%B0%D1%84_%D1%81%D1%86%D0%B5%D0%BD%D1%8B
[6] систему координат Cocoa: https://developer.apple.com/library/content/documentation/General/Conceptual/Devpedia-CocoaApp/CoordinateSystem.html
[7] RxSwift: https://github.com/ReactiveX/RxSwift
[8] CGAffineTransform: https://developer.apple.com/reference/coregraphics/1666405-cgaffinetransform
[9] Источник: https://habrahabr.ru/post/313630/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.