Добро пожаловать во вторую часть серии статей по AsyncDisplayKit (Texture)!
Система компоновки AsyncDisplayKit позволяет писать невероятно быстрый, декларативный код.
Помимо быстрой настройки, она автоматически адаптируется к устройству, на котором запущено приложение. Допустим, вы пытаетесь создать узел, который можно использовать во view controller приложения или в качестве popover в приложении для iPad. Если его layout будет правильно создан, вы сможете перенести узел в эту новую среду, не беспокоясь об изменении базового кода макета!
В этом туториале AsyncDisplayKit 2.0 вы вернетесь к классу CardNode из первой части и узнаете о спецификациях layout, которые были использованы для его создания. Вы увидите, как легко составлять спецификации макета, чтобы получить желаемый результат.
Проблема с Auto Layout
Я слышу, как вы кричите: «Что не так с Auto Layout ?!» В Auto Layout каждый создаваемый вами коснтрейнт представляется в виде уравнения в системе уравнений. Это означает, что каждый добавленный констрейнт увеличивает время расчета констрейнтов экспоненциально. Такой расчет всегда выполняется в основном потоке.
Одна из целей дизайна ASDK – как можно лучше придерживаться API UIKit. К сожалению, Auto Layout – непрозрачная система, в которой нет способа выполнять расчет констрейнтов в другом потоке.
Давайте начнем
Для начала загрузите стартовый проект отсюда. Поскольку вы будете изучать часть спецификаций макетов, стоит начать с измененной версии готового продукта из первого туториала по AsyncDisplayKit 2.0.
Примечание: Прежде чем приступить к работе с туториалом по AsyncDisplayKit 2.0, ознакомьтесь с первой частью.
Знакомство с ASLayoutSpec
Прежде чем начать, расскажу вам немного предыстории.
Спецификации макета – это обобщение системы макетов, о которой вкратце рассказано в Building Paper Event. Идея состоит в том, чтобы унифицировать вычисление и применение размеров и положений узла и подузлов, и иметь возможность их переиспользовать.
В ASDK 1.9.X вы могли создавать асинхронные макеты, но код макета был похож на pre-auto layout в UIKit. Размер подузлов узла можно было вычислить в методе -calculateSizeThatFits :. Эти размеры могли быть закэшированны, а затем добавлены позже в -layout. А позиции узлов все еще должны были рассчитываться с использованием старой-доброй математики – никто не любит возиться с математикой.
Спецификация макета
В ASDK 2.0 подклассы ASDisplayNode могут реализовать -layoutSpecThatFits :. Объект класса ASLayoutSpec определяет размер и положение всех подузлов. При этом спецификация макета также определяет размер упомянутого родительского узла.
Узел вернет объект спецификации макета из -layoutSpecThatFits :. Этот объект определит размер узла, а также размеры и позиции всех его подузлов рекурсивно.
Аргумент ThatFits является объектом класса ASSizeRange. Он имеет два свойства типа CGSize (min и max), которые определяют наименьшие и наибольшие размеры узла.
ASDK предоставляет множество различных видов спецификаций. Вот некоторые из них:
- ASStackLayoutSpec: Позволяет определить вертикальный или горизонтальный стек дочерних элементов. Свойство justifyContent определяет расстояние между дочерними элементами в направлении стека, а alignItems определяет их расстояние вдоль противоположной оси. Эта спецификация настроена аналогично UIKit UIStackView.
- ASOverlayLayoutSpec: Позволяет растянуть один элемент макета над другим. Объект, который накладывается на него, должен иметь intrinsic content size, чтобы это сработало.
- ASRelativeLayoutSpec: Помещает элемент в относительную позицию внутри его доступного пространства. Подумайте о девяти секциях девяти нарезанных изображений. Вы можете поместить объект в одну из этих секций.
- ASInsetLayoutSpec: Позволяет сделать отступы вокруг существующего объкта. Вы хотите классические «iOS 16 пиксельные отступы» вокруг вашей ячейки? Нет проблем!
ASLayoutElement протокол
Спецификации макета управляют расположением одного или нескольких дочерних элементов. Элемент может быть узлом, таким как ASTextNode или ASImageNode. В дополнение к узлам, дочерний шаблон спецификации макета также может быть другой спецификацией макета.
Эй, как это возможно?
Дочерние элементы спецификации макета должны соответствовать протоколу ASLayoutElement. ASLayoutSpec и ASDisplayNode соответствуют ASLayoutElement, поэтому оба типа и их подклассы могут быть дочерними элементами.
Эта простая концепция оказывается невероятно мощной. Одной из наиболее важных характеристик макета является ASStackLayoutSpec. Возможность разместить изображение и текст – это одно, а вот разместить изображение и другой стек – совсем другое!
Вы совершенно правы. Пришло время дуэли! Я имею в виду, писать код ...
Размещение изображения животного
Итак, вы на работе, и ваш дизайнер отправил вам скриншот того, что он хочет для нового приложения «Энциклопедия для животных», над которым вы работаете.
Первое, что нужно сделать, это разбить экран на соответствующие спецификации макета, чтобы понять общий макет. Иногда это может ввести в ступор, но помните, что мощность макета зависит от того, насколько легко они могут быть скомпонованы. Начните с простого.
Забегая вперед, скажу лишь, что верхняя и нижняя половины будут отлично работать в стеке вместе. Теперь, когда вы это знаете, можете разместить две половинки отдельно и объединить их в конце.
Разархивируйте стартовый проект и откройте RainforestStarter.xcworkspace. Перейдите в CardNode.m к методу -layoutSpecThatFits :. Сейчас он просто возвращает пустой объект ASLayoutSpec.
Если вы скомпилируите и запустите проект, то увидите следующее:
Хорошо, это только начало. Как насчет того, чтобы сперва показать изображение животного?
По умолчанию узел сетевого изображения не имеет содержимого и, следовательно, не имеет собственного размера. Посмотрев на скриншот, можно сказать, что изображение животного должно занимать полную ширину экрана и 2/3 высоты.
Для этого замените существующий оператор return следующим:
//1
CGFloat ratio = constrainedSize.min.height/constrainedSize.min.width;
//2
ASRatioLayoutSpec *imageRatioSpec = [ASRatioLayoutSpec
ratioLayoutSpecWithRatio:ratio
child:self.animalImageNode];
//3
return imageRatioSpec;
Рассмотрим каждый пронумерованный комментарий по очереди:
- Calculate Ratio: Во-первых, вы определяете соотношение, которое хотите применить к своему изображению. Соотношения определяются по высоте / ширине. Здесь вы указываете, что высота этого изображения должна составлять 2/3 минимальной высоты ячейки, что является высотой экрана.
- Create Ratio Layout Spec: Затем вы создаете новый ASRatioLayoutSpec с помощью рассчитанного отношения и дочернего элемента animalImageNode.
- Return a Spec: Возвращенный imageRatioSpec определяет высоту и ширину ячейки.
Скомпилируйте и запустите, чтобы посмотреть, как выглядит ваш макет:
Довольно легко, да? Поскольку изображение – единственный элемент, у которого есть размер, ячейки растянулись, чтобы приспособиться к нему.
Примечание: constrainedSize, переданный в ячейку узла таблицы, состоит из минимального (0, 0) и максимального значения (tableNodeWidth, INF). Поэтому вам необходимо использовать preferredFrameSize для определения высоты изображения. preferredFrameSize был установлен в AnimalPagerController в части 1.
Добавление градиента
Теперь, когда у вас есть изображение животного, следующим логическим шагом является добавление поверх него градиентного узла.
ASOverlayLayoutSpec – это просто спецификация для работы.
Сначала добавьте следующую строчку после инициализации imageRatioSpec:
ASOverlayLayoutSpec *gradientOverlaySpec = [ASOverlayLayoutSpec
overlayLayoutSpecWithChild:imageRatioSpec
overlay:self.gradientNode];
При создании своих спецификаций макета вы всегда получите те, которые содержат остальные. Пришло время для gradientOverlaySpec.
Замените текущий return следующим.
return gradientOverlaySpec;
Скомпилируйте и запустите, чтобы увидеть градиент, растянутый по каждому объекту imageNode.
Градиент для каждой птицы – отлично!
Добавление текста с именем животного
Единственное, что осталось сделать – это добавить название животного.
Хотя задача кажется простой, есть несколько требований, которые необходимо учитывать:
- Название должно располагаться поверх градиента.
- Название должно быть в нижнем левом углу изображения животного.
- Отступ – 16 поинтов слева и 8 снизу.
Вы уже знаете, как разместить текст сверху. Пришло время вырваться из проверенной и истинной спецификации оверлея.
Добавьте следующую строчку сразу после gradientOverlaySpec
ASOverlayLayoutSpec *nameOverlaySpec = [ASOverlayLayoutSpec
overlayLayoutSpecWithChild:gradientOverlaySpec
overlay:self.animalNameTextNode];
Кроме того, вам нужно изменить оператор return на следующий:
return nameOverlaySpec;
Скомпилируйте и запустите приложение, чтобы увидеть текст на экране:
Неплохо. Вам просто нужно переместить этот текст в нижний угол.
Стоит упомянуть об общем случае, с которым вы столкнетесь. У вас есть текст на птице, поэтому естественное стремление – обернуть OverlaySpec в другие спецификации, чтобы поместить его там, где хотите. Как правило, нужно сделать шаг назад и подумать о том, что вы пытаетесь выразить.
В этом случае вы используете nameOverlaySpec, чтобы расширить что-нибудь еще поверх существующего содержимого.
На самом деле вы не хотите расширять название по содержанию. Вы лишь хотите сказать имени, что оно должно быть в нижнем левом углу своего свободного пространства, а затем растянуть эту спецификацию макета по доступному пространству.
Знакомство с ASRelativeLayoutSpec
На самом деле вам нужен ASRelativeLayoutSpec.
ASRelativeLayoutSpec принимает дочерний объект ASLayoutElement, рассматривает пространство, в котором оно доступно, и затем помещает этот дочерний элемент в соответствии с вашими инструкциями.
При определении относительной спецификации, можно установить ее свойства verticalPosition и horizontalPosition.
Эти два свойства могут быть следующими:
- ASRelativeLayoutSpecPositionStart
- ASRelativeLayoutSpecPositionCenter
- ASRelativeLayoutSpecPositionEnd
Комбинация позволяет разместить объект в одном из углов, краев или в центре доступного пространства.
В качестве упражнения: как бы вы поместили эту лягушку на правом краю свободного пространства?
Если вы сказали: «Установите verticalPosition в ASRelativeLayoutSpecPositionCenter и horizontalPosition в ASRelativeLayoutSpecPositionEnd», то вы правы!
Теперь, когда вы попрактиковались, следующая строка должна иметь немного больше смысла. Добавьте эту строчку прямо перед nameOverlaySpec:
ASRelativeLayoutSpec *relativeSpec = [ASRelativeLayoutSpec
relativePositionLayoutSpecWithHorizontalPosition:ASRelativeLayoutSpecPositionStart
verticalPosition:ASRelativeLayoutSpecPositionEnd
sizingOption:ASRelativeLayoutSpecSizingOptionDefault
child:self.animalNameTextNode];
Так вы установите horizontalPosition дочернего элемента для старта и verticalPosition для завершения. На лягушачьем языке это выглядело бы примерно так:
Теперь, когда у вас установлена ??относительная спецификация, измените определение nameOverlaySpec на следующее:
ASOverlayLayoutSpec *nameOverlaySpec = [ASOverlayLayoutSpec
overlayLayoutSpecWithChild:gradientOverlaySpec
overlay:relativeSpec];
Скомпилируйте и запустите, чтобы посмотреть, что у вас получилось:
Хорошо! Только есть еще одна вещь, которую нужно сделать на этой половине ячейки.
Знакомство с ASInsetLayoutSpec
Последнее, что вам нужно сделать, это поместить название животного на 16 поинтов слева и на 8 поинтов вниз. Для этого у вас есть ASInsetLayoutSpec.
Чтобы добавить небольшой отступ вокруг любого из ваших объектов, просто оберните объект в спецификацию вставки и предоставьте UIEdgeInsets определить, какой отступ вам нужен.
Добавьте следующую строчку после nameOverlaySpec:
ASInsetLayoutSpec *nameInsetSpec = [ASInsetLayoutSpec
insetLayoutSpecWithInsets:UIEdgeInsetsMake(0, 16.0, 8.0, 0.0)
child:nameOverlaySpec];
Затем еще раз измените оператор return, чтобы вернуть самую внешнюю спецификацию.
return nameInsetSpec;
Скомпилируйте и запустите.
Вы не хотите, чтобы вставка была применена ко всей области, которую охватывает оверлей, так как она включает в себя изображение вашего животного.
То, что вы на самом деле хотите, это применить вставку к пространству relativeSpec. Чтобы исправить это, сначала удалите текущее определение nameInsetSpec.
Затем добавьте следующую новую и улучшенную версию прямо перед определением nameOverlaySpec:
ASInsetLayoutSpec *nameInsetSpec = [ASInsetLayoutSpec
insetLayoutSpecWithInsets:UIEdgeInsetsMake(0, 16.0, 8.0, 0.0) child:relativeSpec];
Теперь вам нужен nameOverlaySpec, чтобы наложить новую вставку, не relativeSpec. Замените определение nameOverlaySpec на:
ASOverlayLayoutSpec *nameOverlaySpec = [ASOverlayLayoutSpec
overlayLayoutSpecWithChild:gradientOverlaySpec overlay:nameInsetSpec];
Наконец, вернитесь к:
return nameOverlaySpec;
Теперь скомпилируйте и запустите, чтобы увидеть что получилось:
Половина работы выполнена!
Нижняя половина
Вторая половина немного легче. Это просто описание животного со вставкой вокруг него… и вы уже знаете, как это сделать.
Добавьте следующую строчку перед оператором return, чтобы создать вставку с текстом описания.
ASInsetLayoutSpec *descriptionTextInsetSpec = [ASInsetLayoutSpec
insetLayoutSpecWithInsets:UIEdgeInsetsMake(16.0, 28.0, 12.0, 28.0)
child:self.animalDescriptionTextNode];
Если бы вы вернули эту вставку, а затем скомпилировали и запустили приложение, то увидели бы следующее:
Это именно то, что вы хотели. Теперь, когда мы разобрались с обеими половинками, объединить их вместе совсем несложно.
Внутренние размеры контента
Вам не нужно беспокоиться о том, чтобы у текста был размер контента для заполнения пространства. Это потому, что ASTextNode имеет свой внутренний размер содержимого, основанный на его тексте и атрибутах.
Следующие узлы не имеют дефолтного размера:
- Подклассы ASDisplayNode
- ASNetworkImageNode и ASMultiplexImageNode
- ASVideoNode и ASVideoPlayerNode
Общность заключается в том, что у этих узлов нет первоначального контента и, следовательно, нет способа определить их собственный размер. Они должны либо иметь preferredFrameSize, либо быть помещены в спецификацию макета, прежде, чем у них будет конкретный размер для работы.
Знакомство с ASStackLayoutSpec
Это идеальное время для использования спецификации макета стека. Вы можете воспринимать это как спецификацию макета, эквивалентную UIStackView, исключая ее автоматическую обратную совместимость, которая довольно изящна.
Стеки могут быть определены как вертикальные, так и горизонтальные. И как все, спецификации макета могут принимать либо узлы, либо другие разметки в качестве дочерних элементов.
Чтобы настроить этот стек, добавьте эти три строки после определения вставки описания:
ASStackLayoutSpec *verticalStackSpec = [[ASStackLayoutSpec alloc] init];
verticalStackSpec.direction = ASStackLayoutDirectionVertical;
verticalStackSpec.children = @[nameOverlaySpec, descriptionTextInsetSpec];
Здесь вы создаете стек, устанавливая его направление вертикальным и добавляя верхнюю половину и нижнюю половину в качестве дочерних элементов.
И снова верните новую спецификацию макета.
return verticalStackSpec;
Скомпилируйте и запустите. Почти готово!
Примечание: Как упоминалось ранее, стеки являются одними из наиболее важных спецификаций макета. Большинство макетов могут быть выражены как стеки или серии вложенных стеков.
Вложенность стеков, каждый из которых имеет свои собственные настройки justifyContent и alignItems, означает, что они могут быть невероятно выразительными, а также невероятно разочаровывающими. Не забудьте посмотреть flex box froggy game и Async Display Kit docs для более глубокого изучения.
Знакомство с ASBackgroundLayoutSpec
Помните спецификацию оверлея? Одно из правил заключается в том, что предмет, который накладывается на нее, должен иметь свой собственный размер.
Элемент сзади определяет размер, а элемент спереди просто растягивается над ним.
Фоновая спецификация в точности противоположна. Если у вас есть один элемент, который может определить свой собственный размер, а другой вы хотите растянуть за ним, понадобится спецификация фона.
В этом случае вам нужно использовать спецификацию макета фона, чтобы растянуть изображение размытого животного.
Для этого добавьте следующую строчку:
ASBackgroundLayoutSpec *backgroundLayoutSpec = [ASBackgroundLayoutSpec
backgroundLayoutSpecWithChild:verticalStackSpec
background:self.backgroundImageNode];
И замените оператор return
return backgroundLayoutSpec;
Скомпилируйте и запустите, чтобы посмотреть, что получилось:
Завершенный проект доступен по ссылке. Повторюсь, что также есть версия для Swift.
Как только вы разберетесь с этими концепциями, начните изучать документацию. Это был просто маленький обзор того, на что способна система компоновки.
Мы надеемся, что вам понравился этот туториал AsyncDisplayKit 2.0, и если у вас есть вопросы – не стесняйтесь оставлять их в комментариях!
P.S. Отдельное спасибо BeataSunshine и evgen за помощь в переводе статьи.