Читаем макет: dp, sp и три контейнера
Зачем это нужно
На чемпионате «Профессионалы» задание почти никогда не звучит как «сделай красивый экран, на своё усмотрение». Судьи выдают макет — картинку или ссылку на Figma с готовым дизайном — и критерии, по которым экран сверяют с макетом буквально попиксельно: не тот отступ, не тот размер шрифта, не тот цвет — минус баллы, даже если на глаз всё красиво.
Прошлые главы дали строительные блоки — Column, Row, Modifier, Text, Button, .dp — но использовали их для простых, придуманных с нуля экранов. Эта глава переворачивает задачу: не «придумай экран», а «вот готовый макет с конкретными числами — прочитай его правильно».
Здесь разберём первую половину работы: как читать числа и цвета на макете, чем dp отличается от sp, как выбрать между Box, Column и Row и как понять, какое именно свойство Arrangement или Alignment нужно. Вторая половина — сборка настоящей карточки по шагам — в следующей главе.
Сам Jetpack Compose в браузере не запускается — нужен Android-проект, поэтому Compose-код здесь даётся статичными блоками с построчным разбором. Но статичный не значит непроверенный: весь код этой главы собран настоящим компилятором в библиотечном модуле Android, и все сообщения об ошибках ниже — его подлинный вывод. А там, где под капотом обычный Kotlin — числа, строки, функции, объекты, — глава даёт исполняемые примеры, которые можно запустить прямо здесь.
- Зачем это нужно
- Как читать макет: отступы, размеры, цвета
- dp и sp: чем отличаются единицы измерения
- Box, Column, Row: три контейнера для макета
- Arrangement и Alignment: где именно окажется каждый элемент
- Типичные ошибки чтения макета
- Слова главы
- Потренируйся печатать
- Символы и приёмы главы
- Куда дальше: проверенные ресурсы
- Проверка по главе
- Что должен уметь
Как читать макет: отступы, размеры, цвета
Независимо от инструмента, которым пользовался дизайнер (Figma, Sketch, Zeplin, Avocode — на разных чемпионатах встречаются разные), готовый макет почти всегда сопровождают одни и те же три вида пометок поверх картинки:
- Стрелки с числом — расстояние в dp: от края экрана до элемента (отступ, padding), между двумя элементами (промежуток, spacing) или размер самого элемента (ширина/высота).
- Подпись у текста вида
16sp / Medium— размер шрифта в sp и его насыщенность (Regular, Medium, Bold). - Цветной прямоугольник с подписанным HEX-кодом вроде
#E53935или#FFFFFF— точный цвет заливки, обводки или текста.
Дальше в этой главе разберём карточку товара с такими же пометками — только текстом, в виде таблицы, раз картинку в MDX-файл не вставить. Вот сама спецификация — она будет цитироваться до конца главы:
| Элемент | Что показано в макете | Значение | Во что превратится в коде |
|---|---|---|---|
| Карточка целиком | стрелки от края до содержимого со всех сторон, подпись «12»; сама карточка залита цветом с подписью #FFFFFF и растянута от края до края экрана | отступ 12 (в dp), фон #FFFFFF, ширина — на весь доступный экран | Modifier.fillMaxWidth().background(Color(0xFFFFFFFF)).padding(Dimens.CardPadding), где CardPadding = 12.dp |
| Изображение товара | прямоугольник высотой «120», залитый цветом с подписью #EEEEEE | высота 120dp, фон #EEEEEE | Modifier.height(Dimens.ImageHeight).background(Color(0xFFEEEEEE)) |
| Бейдж скидки | маленький прямоугольник в правом верхнем углу картинки, заливка #E53935, вокруг текста отступ «4» | фон #E53935, отступ 4dp | Modifier.align(Alignment.TopEnd).background(Color(0xFFE53935)).padding(Dimens.BadgePadding) |
| Заголовок товара | подпись рядом с текстом «16sp / Medium» | размер шрифта 16sp | fontSize = Dimens.TitleFontSize, где TitleFontSize = 16.sp |
| Цена | подпись «18sp / Bold» | размер шрифта 18sp | fontSize = Dimens.PriceFontSize |
| Ряд «цена + кнопка» | стрелка от левого края до правого с засечками на обоих концах, подпись «space-between» | распределить по краям, ничего не оставляя посередине | horizontalArrangement = Arrangement.SpaceBetween |
А теперь потренируй сам навык чтения чисел с макета — на глаз, как это приходится делать, когда стрелки с подписями на картинке видны, а точное значение нужно назвать самому.
Цвет как число: hex-запись
HEX-код вроде #E53935 — это то же самое, что число 0xE53935, только записанное в шестнадцатеричной системе счисления (hexadecimal — «по основанию 16», отсюда и hex): каждая пара символов — это один канал цвета от 00 до FF (0–255 в десятичной системе). У Color в Compose есть конструктор, принимающий такое число целиком, только с ЧЕТЫРЬМЯ парами вместо трёх — в начало добавляется канал прозрачности (alpha):
Порядок каналов — alpha, red, green, blue — часто сокращают до аббревиатуры ARGB. Именно в этом порядке дизайнерские инструменты обычно и подписывают цвет под прямоугольником на макете, только без alpha-канала, если фон непрозрачный: #E53935 без пары FF в начале — то же самое, что 0xFFE53935 с добавленной непрозрачностью по умолчанию.
Прежде чем видеть цвет как единое число, полезно самому разложить его руками — ровно так, как это делает конструктор Color внутри себя: побитовым сдвигом (shr — shift right, «сдвинуть вправо») и маской and 0xFF (оставить только младшие 8 бит, то есть одну пару шестнадцатеричных цифр).
fun main() {
val badgeColor = 0xFFE53935
val alpha = (badgeColor shr 24) and 0xFF
val red = (badgeColor shr 16) and 0xFF
val green = (badgeColor shr 8) and 0xFF
val blue = badgeColor and 0xFF
println("alpha=$alpha red=$red green=$green blue=$blue")
}
Разбор по строкам
- Строка 2:
val badgeColor = 0xFFE53935— то же самое число, что стоит внутриColor(...)на макете; тип у него —Long, а неInt: значение0xFFE53935— это 4 293 212 469 в десятичной записи, вInt(максимум 2 147 483 647) оно не помещается, и компилятор сам берёт для такого литералаLong. Именно поэтому и настоящий конструкторColor(...)принимаетLong. - Строка 3:
val alpha = (badgeColor shr 24) and 0xFF—shr 24сдвигает все биты числа на 24 позиции вправо, из-за чего самая первая пара цифр (FF) оказывается там, где раньше были младшие 8 бит;and 0xFFпосле сдвига «обрезает» всё, кроме этих восьми бит, оставляя чистое значение канала. - Строки 4–5:
red/green— та же операция, но со сдвигом на 16 и на 8: чем ближе канал к началу записи числа, тем на большее число бит его приходится сдвинуть, чтобы «дотянуть» до самого конца. - Строка 6:
val blue = badgeColor and 0xFF— синий канал уже стоит в самом конце числа, поэтому сдвиг вообще не нужен — сразу маскаand 0xFF.
Что выведет: alpha=255 red=229 green=57 blue=53 — те же самые числа, что дают шестнадцатеричные пары FF, E5, 39, 35 из макета, только в привычной десятичной записи. Настоящий класс Color внутри себя делает ровно эти же четыре строки, просто один раз, при создании объекта — а не при каждом обращении к цвету.
dp и sp: чем отличаются единицы измерения
В прошлых главах .dp уже применялся для отступов и размеров — но макет требует ещё одну единицу, sp, специально для текста, и путать их — одна из самых частых ошибок при переносе макета в код.
dp (density-independent pixels, «пиксели, независимые от плотности экрана») — единица для всего, что НЕ текст: отступы, ширина, высота, размер иконки. Официальная документация Android определяет dp как виртуальный пиксель, примерно равный одному физическому пикселю на экране с базовой плотностью 160 dpi (точек на дюйм); на экране с другой плотностью Android сам пересчитывает dp в нужное количество настоящих пикселей, поэтому один и тот же 16.dp выглядит одинакового физического размера что на дешёвом, что на дорогом телефоне.
sp (scale-independent pixels, «пиксели, независимые от масштаба») — единица только для размера текста. По умолчанию sp равен dp один в один, но с одним важным отличием: sp дополнительно учитывает системную настройку размера шрифта, которую пользователь выставляет в настройках телефона для себя (например, человеку с плохим зрением). Если для текста использовать dp вместо sp, эта настройка перестанет работать именно для этого текста — он останется мелким, даже если пользователь специально увеличил шрифт во всей системе.
Правило простое: если число со макета относится к тексту (размер шрифта, межстрочный интервал) — .sp. Если к чему угодно ещё (отступ, ширина, высота, радиус скругления) — .dp. Перепутать их — не опечатка, а ошибка типа, которую компилятор поймает сразу; разберём её точно в разделе «Типичные ошибки».
Прежде чем смотреть на настоящие Dp и TextUnit (которым Compose оборачивает .sp), стоит увидеть сам принцип «два разных типа для двух разных единиц» на самодельных классах — они устроены так же, как настоящие: тонкая обёртка вокруг числа, которая не даёт компилятору перепутать одну единицу измерения с другой.
@JvmInline
value class Dp(val value: Float)
@JvmInline
value class Sp(val value: Float)
val Int.dp: Dp get() = Dp(this.toFloat())
val Int.sp: Sp get() = Sp(this.toFloat())
fun cardPadding(padding: Dp) {
println("Отступ карточки: ${padding.value}dp")
}
fun titleFontSize(fontSize: Sp) {
println("Размер шрифта заголовка: ${fontSize.value}sp")
}
fun main() {
cardPadding(12.dp)
titleFontSize(16.sp)
}
Разбор по строкам
- Строки 1–2 и 3–4:
@JvmInline value class Dp(...)/value class Sp(...)— два маленьких класса-обёртки вокругFloat.value class(value-класс) — та же тема из главы про Compose-экран: во время выполнения программы это просто число, без накладных расходов на создание отдельного объекта, но во время КОМПИЛЯЦИИ это два разных, несовместимых типа. - Строки 6–7:
val Int.dp: Dp get() = Dp(this.toFloat())— то же свойство-расширение.dp, что и в прошлой главе, только теперь рядом появляется симметричный.sp, оборачивающий число вSp, а не вDp. - Строка 9:
fun cardPadding(padding: Dp)— функция принимает СТРОГОDp, ничего другого. - Строка 13:
fun titleFontSize(fontSize: Sp)— а эта — строгоSp. - Строки 17–18:
cardPadding(12.dp)иtitleFontSize(16.sp)— каждое число передано в СВОЮ функцию, с правильным суффиксом.
Что выведет: Отступ карточки: 12.0dp, затем Размер шрифта заголовка: 16.0sp. Пока суффикс соответствует ожидаемому параметру — компилятор доволен. Что случится, если их перепутать местами — cardPadding(16.sp) вместо titleFontSize(16.sp), — разобрано в разделе «Типичные ошибки»: настоящий Compose требует именно Dp для padding/height/width и именно TextUnit-значение вроде .sp для fontSize, и точно так же ловит несовпадение на этапе компиляции.
Box, Column, Row: три контейнера для макета
Прошлая глава уже разобрала Column (сверху вниз) и Row (слева направо). Макет карточки товара не обходится без третьего контейнера — Box, который до сих пор в курсе не встречался.
Box кладёт все свои дочерние элементы ДРУГ НА ДРУГА, а не рядом: первый вызванный внутри Box элемент рисуется первым, а каждый следующий — ПОВЕРХ уже нарисованных. Официальная документация Compose именно так и объясняет назначение Box: «put elements on top of another» («расположить элементы друг на друге»). Это ровно то, что нужно для бейджа скидки на макете: серый прямоугольник-заглушка изображения и красный бейдж с текстом занимают одно и то же место на экране, один поверх другого — а не выстроены в ряд или в столбец.
Прежде чем смотреть на настоящий Box, стоит увидеть сам принцип «порядок вызова определяет порядок наложения» на чистом Kotlin — в противовес Column, где каждый вызов получает своё, отдельное место.
fun myBox(content: () -> Unit) {
println("--- Box: каждый следующий вызов рисует СВЕРХУ предыдущего ---")
content()
}
fun myColumn(content: () -> Unit) {
println("--- Column: каждый следующий вызов получает своё место НИЖЕ предыдущего ---")
content()
}
fun main() {
myBox {
println("Слой 1: фон-заглушка изображения")
println("Слой 2: бейдж скидки (лёг поверх слоя 1)")
}
myColumn {
println("Блок 1: заголовок товара")
println("Блок 2: цена (встал ПОД блоком 1, а не поверх)")
}
}
Разбор по строкам
- Строки 1–4 и 6–9:
myBox/myColumn— обе функции принимают трейлинг-лямбдуcontentи просто вызывают её — сама разница междуBoxиColumnздесь не в коде функций (он одинаковый), а в том, ЧТО именно с элементами внутри делает настоящий Compose:Boxкладёт их друг на друга,Column— друг под другом. Демо специально показывает это словами в подписи, раз нарисовать наложение печатью в консоль нельзя. - Строки 12–15: вызов
myBox { ... }— дваprintlnвнутри условно изображают два элемента, которые в настоящемBoxзаняли бы одно и то же место, один поверх другого, в порядке вызова. - Строки 16–19: вызов
myColumn { ... }— те же по смыслу два элемента, но в настоящемColumnони получили бы РАЗНЫЕ места, одно под другим, а не одно поверх другого.
Что выведет: обе шапки с чёрточкой ---, и под каждой — свои две строки, в порядке вызова. Печать одинакова для обеих функций — но за этим одинаковым выводом в консоль стоит совершенно разное поведение настоящих Box и Column на экране: Compose помнит порядок вызова и либо накладывает элементы друг на друга (Box), либо раскладывает их по отдельным местам (Column) — то, что в тексте демо просто проговорено словами.
contentAlignment и Modifier.align: где именно окажется элемент внутри Box
Внутри Box элементы накладываются друг на друга, но по умолчанию — все они прижаты к верхнему левому углу. Чтобы бейдж скидки оказался именно в правом верхнем углу картинки, как на макете, нужно явно задать его позицию — двумя разными способами.
Способ 1 — contentAlignment у самого Box: задаёт положение СРАЗУ ДЛЯ ВСЕХ детей Box, если у них самих нет собственной настройки.
Способ 2 — Modifier.align(...) у КОНКРЕТНОГО ребёнка: переопределяет contentAlignment только для одного элемента, не трогая остальные. Именно этот способ нужен для бейджа: заглушка изображения занимает весь Box целиком (ей выравнивание не важно), а вот бейдж должен оказаться именно в углу.
Почему align существует только внутри Box
Заметь формулировку в разборе .align выше: «доступна только внутри Box». Это не случайность и не искусственное ограничение — align в настоящем Compose объявлена как функция-расширение (extension function) специально для типа BoxScope, а BoxScope существует только внутри трейлинг-лямбды Box { ... }, куда его подставляет сам Compose. Вне этого места у компилятора просто нет объекта подходящего типа, у которого можно было бы вызвать align. Смоделируем этот же механизм на чистом Kotlin, без единой строчки Compose.
interface BoxScope
class Position(val x: Int, val y: Int)
fun BoxScope.place(x: Int, y: Int): Position = Position(x, y)
fun myBox(content: BoxScope.() -> Unit) {
val scope = object : BoxScope {}
scope.content()
}
fun main() {
myBox {
val badgePosition = place(x = 100, y = 0)
println("Бейдж внутри Box: x=${badgePosition.x}, y=${badgePosition.y}")
}
}
Разбор по строкам
- Строка 1:
interface BoxScope— пустой интерфейс-маркер, точная модель настоящегоBoxScope: сам по себе он ничего не содержит, а служит лишь «пропуском», подтверждающим, что код выполняется именно внутриBox. - Строка 4:
fun BoxScope.place(x: Int, y: Int): Position— функция-расширение (extension function) типаBoxScope, ровно как.dpбыло функцией-расширениемIntв прошлой главе — только здесь получателем (receiver) выступает не число, а тип-маркерBoxScope. Вызватьplace(...)можно только там, где ЕСТЬ объект типаBoxScope, у которого её вызвать. - Строка 6:
fun myBox(content: BoxScope.() -> Unit)— новый вид типа функции:BoxScope.() -> Unitвместо знакомого() -> Unitозначает «лямбда с получателем» — внутри её тела уже ЕСТЬ неявныйthisтипаBoxScope, и все функции-расширенияBoxScope, включаяplace, доступны без явного указания объекта. Это тот же механизм, которым в настоящем Compose объявленыcontentуBox,Row,Column. - Строки 7–8:
val scope = object : BoxScope {}иscope.content()—myBoxсоздаёт конкретный объект нужного типа и вызывает переданную лямбду именно НА НЁМ — отсюда внутри лямбдыplace(...)внезапно становится доступен, хотя нигде явно не импортировался и не создавался читателем кода. - Строки 12–13:
myBox { val badgePosition = place(x = 100, y = 0); ... }— внутри трейлинг-лямбдыplaceвызывается как будто это обычная функция без получателя — а фактически это вызовscope.place(...), просто неявный.
Что выведет: Бейдж внутри Box: x=100, y=0. Ключевой вывод не в числах, а в том, ЧТО это демо доказывает: place (аналог настоящего align) технически недоступен НИГДЕ, кроме как внутри content: BoxScope.() -> Unit — то есть внутри myBox { ... }. Попытка вызвать его в main() напрямую, вне myBox { ... }, дала бы ту же самую ошибку компилятора, что и вызов weight() вне Row/Column — «Unresolved reference» — потому что вне подходящего receiver-объекта функция-расширение попросту не находится. Эта же ошибка разобрана дальше, в разделе «Типичные ошибки».
Arrangement и Alignment: где именно окажется каждый элемент
У Row и Column — по два параметра позиционирования каждый, а не один общий, как contentAlignment у Box. Причина в том, что у Row и Column есть две принципиально разные оси: главная (main axis) — та, вдоль которой контейнер выстраивает элементы, и поперечная (cross axis) — перпендикулярная ей.
- Arrangement отвечает за главную ось — КАК распределить элементы вдоль направления самого контейнера: у
Rowэто горизонталь (horizontalArrangement), уColumn— вертикаль (verticalArrangement). - Alignment отвечает за поперечную ось — КАК выровнять элементы относительно направления, перпендикулярного контейнеру: у
Rowэто вертикаль (verticalAlignment), уColumn— горизонталь (horizontalAlignment).
Официальная документация Compose формулирует это правило именно так: чтобы задать позицию детей внутри Row, указывают horizontalArrangement и verticalAlignment; для Column — наоборот, verticalArrangement и horizontalAlignment. Имя параметра всегда называет ТУ ось, за которую он отвечает, а не контейнер, в котором он стоит — отсюда и кажущаяся на первый взгляд путаница у Row, где verticalAlignment пишут чаще, чем horizontalArrangement.
| Контейнер | Главная ось | Параметр главной оси | Поперечная ось | Параметр поперечной оси |
|---|---|---|---|---|
Row | горизонталь | horizontalArrangement | вертикаль | verticalAlignment |
Column | вертикаль | verticalArrangement | горизонталь | horizontalAlignment |
Box | нет главной оси — только наложение | — | обе сразу | contentAlignment (и Modifier.align для отдельного ребёнка) |
Значения Arrangement (одинаковый набор что для horizontalArrangement, что для verticalArrangement — меняется только направление):
| Значение | Что делает |
|---|---|
Arrangement.Start / Arrangement.Top | все элементы прижаты к началу контейнера, свободное место — в конце |
Arrangement.End / Arrangement.Bottom | все элементы прижаты к концу, свободное место — в начале |
Arrangement.Center | все элементы сгруппированы в середине, свободное место поровну по обоим краям |
Arrangement.SpaceBetween | свободное место — только МЕЖДУ элементами, по краям — впритык, без зазора |
Arrangement.SpaceAround | каждый элемент получает свободное место с обеих сторон, но по краям контейнера места вдвое меньше, чем между элементами |
Arrangement.SpaceEvenly | все промежутки — включая края контейнера — абсолютно одинаковые |
Arrangement.spacedBy(8.dp) | фиксированный зазор между элементами, без привязки к свободному месту — противоположность предыдущим четырём вариантам |
Разница между SpaceBetween, SpaceAround и SpaceEvenly на словах звучит похоже, но числа расставляют всё по местам. Посчитаем зазоры вручную для ряда шириной 300, где три элемента по 60 занимают в сумме 180, а 120 остаётся свободными — ровно та же арифметика, которую Compose делает сам внутри Row/Column.
fun main() {
val containerWidth = 300
val itemWidth = 60
val itemCount = 3
val freeSpace = containerWidth - itemWidth * itemCount
val gapsBetween = itemCount - 1
println("SpaceBetween: зазор между элементами=${freeSpace / gapsBetween}, по краям=0")
val aroundPerItem = freeSpace / itemCount
println("SpaceAround: по краям=${aroundPerItem / 2}, между элементами=$aroundPerItem")
val evenlyGaps = itemCount + 1
println("SpaceEvenly: каждый промежуток=${freeSpace / evenlyGaps}")
}
Что выведет: SpaceBetween: зазор между элементами=60, по краям=0, SpaceAround: по краям=20, между элементами=40, SpaceEvenly: каждый промежуток=30. Три стратегии делят одни и те же свободные 120 единиц по-разному: SpaceBetween делит их на gapsBetween = itemCount - 1 = 2 промежутка МЕЖДУ элементами и ничего не оставляет по краям; SpaceAround даёт каждому элементу свою «личную» долю свободного места с двух сторон, но соседние элементы делят пространство пополам между собой (отсюда между элементами = 40, вдвое больше, чем по одному краю — 20); SpaceEvenly делит свободное место на itemCount + 1 = 4 РАВНЫХ промежутка, включая оба края.
Значения Alignment для поперечной оси Row/Column — набор всего из трёх штук на каждую, потому что у поперечной оси нет середины между несколькими элементами, только положение самого блока:
| Где используется | Значения |
|---|---|
horizontalAlignment у Column | Alignment.Start, Alignment.CenterHorizontally, Alignment.End |
verticalAlignment у Row | Alignment.Top, Alignment.CenterVertically, Alignment.Bottom |
contentAlignment у Box | все 9 сочетаний: TopStart, TopCenter, TopEnd, CenterStart, Center, CenterEnd, BottomStart, BottomCenter, BottomEnd — потому что у Box, в отличие от Row и Column, обе оси равноправны |
Девять сочетаний — это на самом деле просто две независимые координаты (по горизонтали и по вертикали), перемноженные друг на друга: 3 варианта по одной оси × 3 по другой. Посчитаем реальные координаты (x — слева, y — сверху) для трёх из девяти сочетаний, если сам Box — 100×100, а ребёнок внутри — 24×24:
fun main() {
val boxWidth = 100
val boxHeight = 100
val childWidth = 24
val childHeight = 24
println("TopEnd: x=${boxWidth - childWidth}, y=0")
println("Center: x=${(boxWidth - childWidth) / 2}, y=${(boxHeight - childHeight) / 2}")
println("BottomStart: x=0, y=${boxHeight - childHeight}")
}
Что выведет: TopEnd: x=76, y=0, Center: x=38, y=38, BottomStart: x=0, y=76. TopEnd — «верх» значит y=0 (самый верх), «конец» значит ребёнок прижат к правому краю (x = ширина Box − ширина ребёнка); Center делит остаток пополам по обеим осям сразу; BottomStart — зеркальное отражение TopEnd, прижато к левому краю и низу. Ровно эту же арифметику — «где взять свободное место и как его распределить по краю или пополам» — Compose делает сам для contentAlignment, только на настоящих размерах экрана, а не на придуманных 100×100.
Типичные ошибки чтения макета
Все сообщения ниже сняты с настоящей сборки, а не пересказаны: библиотечный модуль Android, Kotlin 2.0.21, Compose BOM 2024.10.01, плагин сборки 8.7.3, compileSdk 34. Формулировки компилятора меняются от версии к версии — на Kotlin 1.x та же ошибка про weight звучала как Unresolved reference: weight, без кавычек и точки. Если у вас текст отличается, сверьте версии, прежде чем искать другую причину.
Ошибка 1. dp вместо sp для текста.
Text(text = "Цена", fontSize = 16.dp)
e: Probe.kt:5:23 None of the following candidates is applicable:
fun Text(text: AnnotatedString, modifier: Modifier = ..., color: Color = ..., fontSize: TextUnit = ..., …): Unit
fun Text(text: String, modifier: Modifier = ..., color: Color = ..., fontSize: TextUnit = ..., …): Unit
Сообщение выглядит пугающе и как будто не по делу: компилятор не называет виноватый аргумент, а вываливает список всех подходящих по имени функций. Читать его надо так — раз ни одна не подошла, ищите в сигнатуре свой параметр: fontSize: TextUnit. Вот и ответ, .dp даёт Dp, а нужен TextUnit, то есть .sp.
Хуже другой случай — когда размер задан через TextStyle с fontSize в sp, а отступы вокруг текста посчитаны «на глаз» в тех же числах. Тогда при увеличенном системном шрифте текст вырастет, а отступы — нет, и строка вылезет за карточку.
Ошибка 2. Позиционный аргумент Box.
Box(Alignment.Center) { Text("Привет") }
e: Probe.kt:6:27 Argument type mismatch: actual type is 'androidx.compose.ui.Alignment',
but 'androidx.compose.ui.Modifier' was expected.
Первый безымянный параметр Box — это modifier, а не contentAlignment. Писать надо с именем: Box(contentAlignment = Alignment.Center).
Ошибка 3. weight вне Row и Column.
Box {
Text("Слева", modifier = Modifier.weight(1f))
}
e: Probe.kt:6:59 Unresolved reference 'weight'.
weight — функция-расширение для RowScope и ColumnScope. Внутри Box такого получателя нет, и имя просто не находится. То же правило работает в обратную сторону: Modifier.align(Alignment.TopEnd) с двумя координатами существует только внутри Box.
Ошибка 4. Опечатка в имени именованного аргумента.
Row(horizontalArrangment = Arrangement.SpaceBetween) { }
e: Probe.kt:5:27 No parameter with name 'horizontalArrangment' found.
Сообщение точное, но взгляд по нему скользит: Arrangment без e выглядит почти как правильное слово. Если параметр «не находится», а имя выглядит верным — читайте его по буквам.
Слова главы
Потренируйся печатать
Цель: скорость ≥ 110 зн/мин, точность ≥ 90%
Box(contentAlignment = Alignment.TopEnd) {
Строка позиционирования Row — то, что придётся набирать при каждом разведении элементов по краям:
Цель: скорость ≥ 120 зн/мин, точность ≥ 90%
horizontalArrangement = Arrangement.SpaceBetween
Символы и приёмы главы
| Символ / приём | Как называется | Что делает | Пример |
|---|---|---|---|
Box { } | контейнер наложения | кладёт детей друг на друга по порядку вызова, каждый следующий — поверх предыдущего | Box { Image(...); Icon(...) } |
contentAlignment | параметр Box | задаёт позицию по умолчанию для всех детей Box сразу по обеим осям | Box(contentAlignment = Alignment.Center) |
Modifier.align(...) | модификатор внутри Box | переопределяет позицию одного конкретного ребёнка, доступен только внутри Box | Modifier.align(Alignment.TopEnd) |
horizontalArrangement / verticalArrangement | параметр главной оси | распределяет элементы вдоль направления самого Row/Column | Row(horizontalArrangement = Arrangement.SpaceBetween) |
horizontalAlignment / verticalAlignment | параметр поперечной оси | выравнивает элементы перпендикулярно направлению Row/Column | Column(horizontalAlignment = Alignment.CenterHorizontally) |
16.dp | единица размера/отступа | density-independent pixels — для всего, кроме размера текста | Modifier.padding(12.dp) |
16.sp | единица размера текста | scale-independent pixels — учитывает системную настройку размера шрифта | Text(fontSize = 16.sp) |
Color(0xAARRGGBB) | конструктор цвета из числа | четыре пары hex-цифр: прозрачность, красный, зелёный, синий каналы | Color(0xFFE53935) |
Углубиться
Как Compose на самом деле решает размер Box, Column и Row. Всё, что описано в этой главе, — снаружи: КАК элементы позиционируются друг относительно друга, если их размер уже известен. Но откуда сам Box или Column вообще узнаёт, какого размера ему быть? Ответ — трёхшаговый процесс измерения: контейнер сначала измеряет каждого ребёнка (спрашивает, каким тот хочет быть при доступных ограничениях по ширине и высоте), затем решает собственный размер на основе размеров детей и родительских ограничений, и только потом расставляет детей по вычисленным позициям. Row и Column — это готовые, уже написанные за тебя реализации этого процесса под конкретную стратегию; когда их не хватает — например, нужен ряд, сам переносящий лишние элементы на новую строку, — Compose позволяет написать процесс измерения вручную через низкоуровневый Layout и MeasurePolicy.
Почему на макете чемпионата важна каждая единица измерения. Критерии оценки часто автоматизированы: скрипт сравнивает скриншот собранного экрана с эталонным макетом попиксельно, с небольшим допуском. Отступ в 16.dp вместо 12.dp или текст в dp вместо sp — это не стилистическая мелочь, а формальное несовпадение, которое такой скрипт обнаружит и оценит как ошибку, даже если экран визуально выглядит «почти так же».
Дальше по теме:
- Jetpack Compose Layouts — разбор своего
Row, который сам переносит элементы на новую строку. - Custom layouts in Compose — официальное объяснение трёхшагового процесса измерения.
Куда дальше: проверенные ресурсы
Русские материалы
- Единицы измерения. Чем отличается dp (dip) от px. Screen Density — открой за более подробным объяснением плотности экрана и того, как именно Android пересчитывает dp в реальные пиксели на разных устройствах.
Официальная документация (на английском)
- Compose layout basics — открой за первоисточником примеров про Row, Column, Box, Arrangement и Alignment из этой главы.
- Support different pixel densities — открой за официальным определением dp, sp и таблицей плотностей экрана.
- Guide to Dev Mode — Figma Help Center — открой, чтобы увидеть, как выглядит инструмент, которым дизайнеры готовят те самые макеты.
- Basic Layouts in Compose (codelab) — пошаговый практикум на 15 шагов.
Проверка по главе
Сводная проверка по всей главе: 7 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Что должен уметь
- Читать в макете отступы и размеры (числа в dp) и текстовые размеры (числа в sp), не путая единицы между собой.
- Раскладывать hex-цвет вроде
#E53935на каналы ARGB и объяснять, что означает каждая пара цифр. - Выбирать между
Box(наложение),Column(сверху вниз) иRow(слева направо) по тому, что требует макет. - Использовать
contentAlignmentуBoxдля общей позиции содержимого иModifier.align(...)— для позиции одного элемента. - По памяти сопоставлять
Row/Columnс параметрами главной и поперечной оси. - Узнавать по сообщению компилятора четыре типичных ошибки этой темы и чинить каждую.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.