Введение
«Почему ConstraintLayout?» — вопрос, который превращает «я умею верстать» в «я понимаю, как Android считает разметку». Плоская иерархия, связи вместо вложенности, 0dp вместо match_parent.
ConstraintLayout — ViewGroup из Jetpack, который позиционирует детей через связи (constraints) между ними, родителем и невидимыми направляющими. Его главное преимущество — большие и сложные макеты с плоской иерархией: без вложенных ViewGroup, которые замедляют измерение и отрисовку.
На собеседованиях ConstraintLayout всплывает в трёх ипостасях: вопрос «что это и зачем», задача «спроектируйте экран» и разговор про производительность. Разберём основы, которых достаточно для уверенных ответов.
Constraints: как задаётся позиция
Каждый View в ConstraintLayout должен иметь минимум одну горизонтальную и одну вертикальную связь. Связь — это соединение края или центра с другим view, родителем или guideline. Официальная документация прямо говорит: без связей по обеим осям позиция не определена.
Классика, на которой ловят: если у view нет связей, в Layout Editor он лежит там, куда вы его перетащили, — но на устройстве отрисуется в координатах [0, 0], то есть в левом верхнем углу. Отсутствие связи — не ошибка компиляции, но редактор подсветит её как предупреждение.
Библиотека подключается одной зависимостью:
dependencies {
implementation("androidx.constraintlayout:constraintlayout:2.2.2")
}Пример простейшего экрана с заголовком:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>Атрибуты связей читаются как «чему равен мой край»: layout_constraintStart_toStartOf="parent" — «мой start равен start родителя». Отсюда два следствия:
- Связь не означает выравнивание. B связан «справа от A» — но по вертикали может быть где угодно, пока нет вертикальной связи.
- Две противоположные связи центрируют. Если view привязан и к start, и к end при фиксированном размере — он встаёт по центру. Положение регулируется bias:
layout_constraintHorizontal_bias="0.3"смещает к левому краю. По умолчанию bias — 0.5.
Есть и baseline-связь — выравнивание по базовой линии текста. Она пригодится, когда TextView и EditText должны стоять на одной строке независимо от размеров шрифта: обычная привязка края к краю тут не сработает, а baseline — сработает. При создании связи в редакторе появляется отступ по умолчанию — кратный 8dp, чтобы views выравнивались по сетке Material Design.
Ошибка новичков — тянуть view за четыре угла, «чтобы стоял». Правильный рефлекс: сначала связи, потом bias.
Типичная ошибка на собеседовании: кандидат не может объяснить, что будет с view без связей. Ответ: на устройстве он отрисуется в левом верхнем углу. В редакторе это незаметно — связей там просто нет, а «не упало» не значит «работает».
Размеры: wrap_content, 0dp и ratio
В ConstraintLayout три режима размера:
- wrap_content — размер по содержимому.
- Фиксированный — размер в dp.
- 0dp (match constraints) — view растягивается, чтобы удовлетворить связи с учётом отступов.
Типичная ошибка: использовать match_parent внутри ConstraintLayout. Он там не работает — вместо него нужен 0dp. В ConstraintLayout размер задаётся связями, а не родителем: layout_width="0dp" + связи start и end = ширина во весь доступный диапазон.
Из 0dp вырастают две мощные фичи. Первая — веса в цепочках, о них дальше. Вторая — пропорции:
<ImageView
android:id="@+id/poster"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />layout_constraintDimensionRatio="16:9" при одном размере 0dp задаёт пропорцию: ширина посчитается из высоты или наоборот. Так делают карточки с превью, которые обязаны сохранять пропорции на любом экране.
match constraints
0dp в ConstraintLayout — не «ноль пикселей», а «растянись до связей». Вместе с ratio и весами это заменяет и match_parent, и layout_weight из LinearLayout.
Chains, guidelines и barriers
Chains — это группа views, связанных двусторонними связями (start каждого → end предыдущего и обратно). Цепочка распределяет views по оси. Стили из документации:
- Spread — равномерное распределение с учётом отступов, стиль по умолчанию.
- Spread inside — крайние views прижаты к концам, остальные распределены между ними.
- Packed — views прижаты друг к другу, смещение цепочки задаётся bias головного view.
- Weighted — в spread или spread inside views с размером 0dp делят оставшееся место; пропорции — через
layout_constraintHorizontal_weight.
Стиль цепочки в XML задаётся на головном view — крайнем слева в горизонтальной цепочке: app:layout_constraintHorizontal_chainStyle="spread". Обязательное условие: оба конца цепочки должны быть связаны с другими объектами на той же оси, иначе цепочка не распределит views.
Типичная задача — две кнопки внизу экрана, растянутые на всю ширину. Это weighted-цепочка: обе кнопки с шириной 0dp, связи концов с parent:
<Button
android:id="@+id/button_cancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/cancel"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/button_ok"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_weight="1" />
<Button
android:id="@+id/button_ok"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/ok"
app:layout_constraintStart_toEndOf="@id/button_cancel"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_weight="1" />Кнопки делят ширину поровну, и никаких вложенных контейнеров: цепочка заменила LinearLayout с layout_weight.
Guidelines — невидимые направляющие, к которым можно привязываться. Позиция задаётся в dp (layout_constraintGuide_begin или _end) или в процентах (layout_constraintGuide_percent="0.5"). Отдельно стоит Barrier — направляющая без собственной позиции, которая двигается за самыми длинными или высокими views внутри себя:
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="title,subtitle" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action"
app:layout_constraintStart_toEndOf="@id/barrier_end"
app:layout_constraintTop_toTopOf="parent" />Кнопка всегда справа от самого широкого из title и subtitle — независимо от того, какой текст длиннее. Это классическое решение для карточек с динамическим контентом.
Производительность и когда ConstraintLayout не нужен
Главный аргумент за ConstraintLayout — плоская иерархия. Вложенные LinearLayout создают дерево, которое Android измеряет и рисует уровнем за уровнем: чем глубже, тем дороже. Документация по Layout прямо рекомендует ConstraintLayout для производительности и удобства работы с Layout Editor.
Но честный ответ на собеседовании — где ConstraintLayout избыточен:
- Экран из одного-двух views — LinearLayout или FrameLayout проще и быстрее.
- Простые item-ы списков — RecyclerView item не требует сложных связей.
- Новые проекты на Compose — разметку строят на Compose, ConstraintLayout там отдельная библиотека для частных случаев.
Сильный кандидат говорит так: ConstraintLayout решает проблему вложенности, а не заменяет все layout-ы. Простая структура — простой контейнер, сложная структура — ConstraintLayout с плоской иерархией.
Почему вложенность вообще дорогая? Android проходит по дереву views трижды: measure (измерение), layout (расстановка) и draw (отрисовка). Каждый вложенный ViewGroup — лишний уровень на каждом проходе. На простом экране разница незаметна, а на скроллящихся списках глубокая иерархия напрямую бьёт по частоте кадров: каждый frame пересчитывает одни и те же уровни. Поэтому рекомендация «делайте иерархию плоской» — это не эстетика, а производительность.
Заодно стоит упомянуть ConstraintSet: набор связей как объект, который применяется к layout-у. С его помощью анимируют перемещение и изменение размеров — начало и конец анимации задаются двумя файлами разметки. Это уровень «знаю, как устроено», который редко встречается у кандидатов на junior.
На собеседованиях я часто вижу кандидатов, которые верстают всё на вложенных LinearLayout — и не могут объяснить, чем это дорого. А вопрос про производительность разметки всплывает в каждом втором интервью: measure, layout, draw проходят по всей иерархии, и каждый вложенный контейнер добавляет работы.
Готовитесь к собеседованию на middle?
Вёрстка, архитектура, Kotlin — на мок-собеседованиях прогоняем темы, которые реально спрашивают, и чиним слабые места.
Итоги
ConstraintLayout — это ViewGroup на связях: каждый view позиционируется минимум одной горизонтальной и одной вертикальной связью с другими views, родителем, guideline или barrier.
Три тезиса, которые стоит унести с собой:
- Связи вместо вложенности. Плоская иерархия = меньше работы для measure, layout и draw. Это главный аргумент и главная причина популярности ConstraintLayout.
- 0dp — магия размера. Match constraints, ratio и веса в цепочках строятся на 0dp. match_parent в ConstraintLayout не работает.
- Инструмент под задачу. Простые экраны — простые контейнеры. ConstraintLayout нужен там, где сложность оправдана.
Если вёрстка для вас — «накидал LinearLayout-ов», попробуйте переписать один экран на ConstraintLayout с плоской иерархией, и разница станет очевидной. А если вы готовитесь к собеседованию и не знаете, какие ещё темы закрыть — напишите в Telegram, разберём вашу ситуацию и составим план подготовки до middle.