Разработка
Кастомные макеты в Compose — стопка карточек с кошками
В приложении, которое я создала, есть кошки — много кошек — и вы можете получить еще больше. Фотографии кошек представлены в виде карточек. Я хотела складывать карточки в стопку, просто потому что думала, что смогу это сделать — и я смогла!
Я писала совершенно другую статью в блоге о том, как работать с Glance и виджетами приложений, и мне нужен был пример. Ни одно из существующих приложений мне не подошло, поэтому нужно было создать новое. И я перестаралась — мне нужно было просто что-то очень простое, но в итоге я создала более сложное приложение с несколькими новыми концепциями.
В приложении, которое я создала, есть кошки — много кошек — и вы можете получить еще больше. Фотографии кошек представлены в виде карточек. Я хотела складывать карточки в стопку, просто потому что думала, что смогу это сделать — и я смогла! Поэтому в этом блоге я расскажу о том, как создать макет стопки карточек. И немного о кошках.
Если вам когда-нибудь понадобятся кошки, есть потрясающий API — Cats as a service. Я считаю его одним из самых важных. Просто упоминаю. Возможно, в будущем я стану кошатницей.
Но ладно, вернемся к кодингу. В следующих разделах я сначала более подробно расскажу о макете об идее, затем обсужу кастомные макеты в Compose и, наконец, расскажу о коде моей версии стопки с карточками.
Стопка карточек с кошками
Идея приложения заключается в том, чтобы найти картинку с кошкой, а затем получить еще больше картинок с кошками, как только вы увидите одну. Ничего сложного, просто кошки. Приложение получает JSON-данные случайной картинки из Cataas-API и сохраняет идентификатор картинки в Data Store. Почему именно хранилище данных, а не, например, Room? Ну, это простое приложение, не предназначенное для использования в продакшене, и ему нужны очень простые данные — набор строк.
Кроме того, оно рендерит картинки с помощью SubcomposeAsyncImage
от Coil на основе URL, поэтому не загружает изображения на устройство. Опять же, это не производственное приложение, и здесь нет необходимости в поддержке оффлайна или подобных вещах. В приложении для пользователей многие вещи должны быть улучшены. Я старалась сделать код (ссылка в конце поста!) как можно более простым, поэтому я срезала некоторые углы.
Поэтому, если вы нанимаете Android-разработчиков, пожалуйста, не смотрите на этот код как на мой шедевр и доказательство того, что я все умею. Поверьте, в продакшене я напишу более надежный код. Обещаю!
Пользовательский интерфейс приложения показывает последнюю фотографию поверх всего, а затем остальные, сложенные в стопку, внизу. Пользователи могут удалить самую верхнюю картинку из стопки, нажав на кнопку X, которая есть у каждой карточки в правом верхнем углу.
Ну а теперь, после стольких намеков, давайте, наконец, посмотрим на картинки. Точнее, фотографии. Вот как выглядят стопка, когда в ней много карт:
Как я создала этот макет? Я использовала кастомные макеты, которые предоставляет Compose. Давайте поговорим об этом дальше.
Кастомные макеты
В Compose есть несколько встроенных макетов, таких как Columns и Rows, но часто требуются собственные макеты. Подробнее о пользовательских макетах в Compose вы можете узнать из документации по Android.
Существует два способа создания кастомных макетов с помощью composable компонентов: с помощью модификатора layout- или с помощью composable макета Layout. Модификатор layout- изменяет только тот компонент, для которого он вызван, поэтому в нашем случае это не вариант.
Поскольку этот макет включает в себя более одного компонента, мы хотим построить макет с помощью композита Layout. Он позволяет нам использовать сразу несколько composable. Процесс создания макета состоит из трех этапов (цитата из документации Custom Layouts):
Каждый узел должен:
- Измерить все дочерние элементы
- Определить свой размер
- Разместить свои дочерние элементы
Мы рассмотрим конкретный пример всех этих этапов немного позже, но это процесс для большинства кастомных макетов.
Покажите мне код
Итак, чтобы создать макет из сложенных карт, нам понадобится кастомный компонент, который принимает в качестве параметров модификатор и содержимое. Конечно, этот компонент может принимать и другие параметры, но для данного примера нужны только эти два.
// CardStack.kt @Composable fun CardStack( modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { ... }
Затем внутри компонента CardStack
мы определим макет, который принимает те же параметры содержимого и модификатора. Третий параметр — это лямбда MeasurePolicy
, отвечающая за все действия и создание макета. У этой лямбды есть два параметра: measurables
— список Measurables
, которые соответствуют дочерним элементам макета. Другой параметр — constraints
, ограничения для макета. Следующий код показывает все это более конкретно:
// CardStack.kt Layout( content, modifier, ) { measurables, constraints -> ... }
Внутри Layout нам нужно будет выполнить три действия, упомянутые в предыдущем разделе. Сначала мы измеряем дочерние элементы, затем определяем их размер, а затем размещаем их.
Первым шагом будет измерение дочерних элементов. Для этого мы воспользуемся параметром measurables
, сверимся с ним и для каждого элемента Measurable
вызовем функцию measure
. Затем мы сохраним все эти данные в переменной под названием placeables
:
// CardStack.kt val placeables = measurables.map { measurable -> measurable.measure(constraints) }
Следующий шаг — выбор размера. Поскольку карточки укладываются друг на друга, площадь экрана, которая нам нужна для карт, практически равна размеру одной карты с некоторым дополнительным наполнением. Конечно, мы не хотим, чтобы карты лежали точно друг на друге, чтобы создать эффект сложенных карт, поэтому количество дополнительной подложки должно зависеть от количества карт.
Для высоты раскладки мы хотим проверить высоту одной карты, поэтому берем первую карту в дочерней measurables переменной (placeables
) и ее высоту. В этом примере размер карт всегда одинаков, поэтому установка высоты не представляет сложности. Для карточек разного размера потребуются дополнительные расчеты высоты.
Затем мы добавляем дополнительную прокладку (в данном случае 10 пикселей) и умножаем ее на размер дочерних карт. Поскольку макет может иметь ноль или более дочерних элементов, мы хотим учесть случай, когда их нет — поэтому, если список placeables
пуст, мы устанавливаем высоту равной 0.
Для ширины макета мы используем ширину первого дочернего элемента, если дети есть, и 0, если их нет. Затем мы определяем макет с этими высотой и шириной. В коде все это выглядит следующим образом:
// CardStack.kt val height = if (placeables.isNotEmpty()) placeables.first().height + (CardStack.EXTRA_PADDING * placeables.size) else 0 val width = if (placeables.isNotEmpty()) placeables.first().width else 0 layout(width = width, height = height) { ... }
Последний шаг — размещение дочерних элементов. Это происходит внутри макета, который мы определили. Мы хотим провести карту через дочерние элементы (placeables
), а затем вызвать метод place
. Он принимает координаты x и y, и мы хотим использовать эти координаты для небольшого смещения, чтобы создать более реалистичный вид.
Так, для значения x мы либо поместим его в координату 0 в родительской системе координат, либо сместим его по x на 5 (определено вне фрагмента кода). Решение зависит от того, является ли значение четным или нечетным — если оно четное, то мы используем 0. В противном случае мы используем 5. Функция isEven
— это функция расширения, которую я определил, чтобы уменьшить количество повторений при проверке четности целого числа.
Для позиции y мы хотим умножить позицию y (на 5, определена за пределами фрагмента кода, полный код см. ниже) на индекс текущего элемента, чтобы создать эффект стопки, показывающий карты под самой верхней картой. Все это переводится в код следующим образом:
// CardStack.kt layout(width = width, height = height) { placeables.mapIndexed { index, placeable -> placeable.place( x = if (index.isEven()) 0 else CardStack.X_POSITION, y = CardStack.Y_POSITION * index, ) } }
Итоговый код для кастомного макета выглядит следующим образом:
// CardStack.kt @Composable fun CardStack( modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { Layout( content, modifier, ) { measurables, constraints -> val placeables = measurables.map { measurable -> measurable.measure(constraints) } val height = if (placeables.isNotEmpty()) placeables.first().height + (CardStack.EXTRA_PADDING * placeables.size) else 0 val width = if (placeables.isNotEmpty()) placeables.first().width else 0 layout(width = width, height = height) { placeables.mapIndexed { index, placeable -> placeable.place( x = if (index.isEven()) 0 else CardStack.X_POSITION, y = CardStack.Y_POSITION * index, ) } } } } object CardStack { const val EXTRA_PADDING = 10 const val Y_POSITION = 5 const val X_POSITION = 5 }
Но нам все еще нужно сделать одну вещь, чтобы добиться того пользовательского интерфейса, который мы видели. В данный момент макет в виде стопки выглядит следующим образом:
Карты лежат друг на друге и довольно хорошо выравниваются. Однако мы хотим, чтобы карты лежали немного случайным образом и вращались, как настоящие, физические карты, когда их складывают в стопку. Мы добавим этот эффект к самим картам с помощью модификатора.
В MainScreen
мы перебираем идентификаторы картинок с кошками, а затем показываем компонент CatCard
с соответствующим идентификатором. Здесь мы добавим модификатор rotate
со случайным количеством градусов для вращения.
Внутри отображения мы определяем переменную degrees
, запоминаем ее между рекомпозициями, а затем передаем это значение модификатору rotate
. Для значения градусов мы хотим получить число в диапазоне от -2 до 2. Поскольку Random.nextFloat()
не позволяет нам определить диапазон, мы используем Random.nextInt()
, а затем преобразуем значение в float
.
Теперь вы можете спросить, зачем нам нужно запоминать переменную. В противном случае это случайное число генерировалось бы при каждой перекомпоновке (когда элемент добавляется или удаляется из списка), заставляя карточки менять свои позиции при каждой перекомпоновке.
// MainScreen.kt CardStack { catIds.value.mapIndexed { index, id -> val degrees = remember { Random.nextInt(-2, 2).toFloat() } AnimatedCatCard( modifier = Modifier.rotate(degrees), ... ) } }
После этих изменений у нас получился макет стопки карточек.
Подведение итогов
В этой статье мы рассмотрели создание кастомных макетов в Compose, создав макет стопки открыток с фотографиями кошек. Для этого мы использовали composable Layout
и некоторые расчеты. Чтобы завершить макет, мы добавили немного случайного вращения с помощью модификатора rotate
.
Полный код этого приложения можно найти в репозитории Cats-repository.
А вы создавали собственные макеты? Есть что-нибудь интересное, чем можно поделиться? Или какие-нибудь наработки?