Работа с ConstraintLayout через XML-разметку

  • Tutorial

ConstraintLayout


Привет! Меня зовут Гавриил, я Android-лид Touch Instinct.


В марте Google выкатил релизное обновление ConstraintLayout. Презентовали его еще на прошлом Google I/O. С того момента прошел почти год, ConstraintLayout стал лучше, быстрее и оброс новыми возможностями. Например, приятно, что появилась возможность объединения элементов в цепи — это позволяет использовать ConstraintLayout вместо LinearLayout.


О всех новых и старых возможностях ConstraintLayout я и постараюсь рассказать в этой статье. Предупрежу сразу — статья будет длинная и других семи частей не будет. Про визуальный редактор ConstraintLayout в статье не будет ни слова — только XML-разметка (по старинке) и немного кода.



1. Добавляем в проект
2. Contsraints. Привязываем элементы друг к другу
3. Задаем размеры View
4. Задаем размеры View на основе соотношения сторон
5. Выставляем относительное расположение View внутри ConstraintLayout
6. Особенности привязки линий
7. Особенности привязки по Baseline
8. Создаем цепи (chains)
8.1. Стиль spread
8.2. Стиль spread_inside
8.3. Стиль packed
8.4. Стиль weighted
9. Указываем отступы с учетом скрытых элементов
10. Разбираемся с Guidelines
11. Настраиваем параметры ConstraintLayout из кода
12. Коротко об анимации
13. Стоит ли пользоваться ConstraintLayout?

Добавляем в проект


  1. Обновляем версию Android Studio до 2.3;
  2. Проверяем, что установлена последняя версия ConstraintLayout — это можно посмотреть в Android Studio → Settings(Preferences) → Appearance & Behavior → System Settings → Android SDK → SDK Tools → Support Repository;
  3. Добавляем dependency в build.gradle модуля проекта:
    dependencies {
    ...
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    ...
    }
  4. Теперь можно использовать ConstraintLayout у себя в проекте:


    <android.support.constraint.ConstraintLayout
        android:id="@+id/my_first_constraint_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <TextView
            android:id="@+id/some_constraint_layout_element"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello world!"/>
    
    </android.support.constraint.ConstraintLayout>


Constraints. Привязываем элементы друг к другу


Constraints — это линии, на основе которых располагается view внутри ConstraintLayout. Constraints могут быть привязаны к сторонам самого ConstraintLayout или к сторонам других view внутри ConstraintLayout. Constraints можно разделить на вертикальные и горизонтальные.


Горизонтальные constraints:


  • правой стороны (Right), левой стороны (Left);
  • начальной стороны (Start), конечной стороны (End).

Вертикальные constraints:


  • верхней стороны (Top), нижней стороны (Bottom);
  • базовой линии (Baseline).

Вертикальное и горизонтальное constraints друг с другом не связаны.


Напомню, что Baseline — это линия выравнивания контента элемента. Пример — для TextView это линия строки, на которой пишется текст. Если у view выставлен Baseline сonstraint, то базовая линия элемента будет находиться на уровне базовой линии view, к которой привязан сonstraint.


Для начала, проще всего рассматривать сonstraints, как стороны view. То есть можно, например, привязать левую сторону view B к правой стороне view A — тогда view B будет располагаться справа от view A.


Общий формат атрибутов для привязки сonstraint выглядит следующим образом:


  app:layout_constraint{X}_to{Y}Of="{Z}"

Где:


  • X — сonstraint привязываемой view;
  • Yсторона view, к которой привязываются;
  • Z — id view, к которой привязываются, или parent, если привязать нужно к стороне ContraintLayout.

Пример привязок:


    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <!-- Левый constraint view_1 привязан к левой стороне ConstraintLayout.
               Верхний constraint view_1 — к верхней стороне ConstraintLayout.
               Правый и нижний constraint не привязаны -->
        <TextView
            android:id="@+id/view_1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="view_1"
            android:textSize="24sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

        <!-- Левый constraint у view_2 привязан к правой стороне view_1.
               Constraint базовой линии view_2 — к базовой линии view_1.
               Нижний и верхний constraint привязывать нельзя, так как есть привязка по базовой линии.
               Правый constraint не привязан. -->
        <TextView
            android:id="@+id/view_2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="view_2"
            android:textSize="14sp"
            app:layout_constraintLeft_toRightOf="@id/view_1"
            app:layout_constraintBaseline_toBaselineOf="@id/view_1"/>

        <!-- Левый constraint view_3 привязан к правой стороне view_1.
               Верхний constraint view_3 — к нижней стороне view_1.
               Правый и нижний constraint не привязаны. -->
        <TextView
            android:id="@+id/view_3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="view_3"
            android:textSize="14sp"
            app:layout_constraintTop_toBottomOf="@id/view_1"
            app:layout_constraintLeft_toRightOf="@id/view_1"/>

        <!-- Левый constraint view_4 привязан к правой стороне view_2.
               Нижний constraint view_4 — к нижней стороне view_2.
               Правый и верхний constraint не привязаны. -->
        <TextView
            android:id="@+id/view_4"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="view_4"
            android:textSize="18sp"
            app:layout_constraintBottom_toBottomOf="@id/view_1"
            app:layout_constraintLeft_toRightOf="@id/view_2"/>

    </android.support.constraint.ConstraintLayout>

Пример


Основные правила привязки сторон:


  • привязывать между собой можно только Start и End, Left и Right, Top и Bottom. То есть, нельзя, например, привязать Left к Start или Baseline к Top;
  • Baseline можно привязать только к Baseline другой view;
  • при привязке Start/End игнорируются привязки Left/Right;
  • при привязке Baseline игнорируются привязки Top/Bottom;
  • не привязывайте view с внешней стороны ConstraintLayout, например, layout_constraintRight_toLeftOf="parent". ConstraintLayout обработает такую привязку, но как себя при этом поведет, сложно предсказать.

Задаем размеры View


Чтобы задать размеры view, используются обязательные атрибуты layout_width и layout_height, и необязательные атрибуты layout_constraintWidth_default и layout_constraintHeight_default.


Значение атрибутов layout_constraintWidth_default и layout_constraintHeight_default по умолчанию равно spread.


Размер view могут быть указан, как (на примере высоты):


  • layout_height="100dp" — обозначим, как fixed_size. View будет указанного размера;
  • layout_height="wrap_content" — обозначим, как any_size. Размер вычисляется самой view, может быть любым;
  • layout_height="0dp" + layout_constraintHeight_default="spread" — обозначим, как match_constraint_spread. Размер view будет равен расстоянию между constraints. Для высоты, например, это расстояние между верхним constraint и нижним constraint;
  • layout_height="0dp" + layout_constraintHeight_default="wrap" — обозначим, как match_constraint_wrap. Размер вычисляется самой view, но не может выйти за рамки constraints.

Важно! Указывать размер match_parent или fill_parent запрещено. Чтобы размер view совпадал с размерами ConstraintLayout, достаточно просто привязать constraints к сторонам ConstraintLayot и использовать размер match_constraint_spread.


Если указан размер match_constraint_wrap или match_constraint_spread, стоит учесть, что:


  • чтобы такой тип размера работал корректно, у view должны быть привязаны два constraint: для ширины это Left и Right или Start и End. Для высоты — Top и Bottom;
  • размер view не может выйти за рамки constraints;
  • можно выставить минимальный и максимальный размер view в рамках constraints. Для этого используются атрибуты layout_constraintWidth_min, layout_constraintHeight_min, layout_constraintWidth_max, layout_constraintHeight_max;
  • не стоит выставлять такой тип размера для высоты, если у view привязан Baseline constraint — вероятно, высота элемента будет рассчитываться неверно.

Для других типов размеров стоит учитывать, что:


  • если указан any_size или fixed_size, то элемент может выходить за рамки constraints. Например, в примере из раздела "Constraints", если в текстовых view задать длинный текст, то он будет выходить за рамки ConstraintLayout;
  • на размеры ConstraintLayout влияют view с fixed_size, any_size и match_constraint_wrap. Если покажется, что размер ConstraintLayout рассчитан неверно, скорее всего, виновата одна из view с такими размерами;
    Размеры

Задаем размеры View на основе соотношения сторон


ConstraintLayout позволяет рассчитывать высоту или ширину view на основе заданного соотношения сторон. То есть, например, при соотношении сторон 16:9, если высота будет 900dp, то ширина рассчитается, как 1600dp.


За это отвечает атрибут layout_constraintDimensionRatio. Задать соотношение сторон можно в двух форматах: текстовом 16:9 или числовом 1.8. При этом перед значением можно указать символ стороны, которая находится в числителе соотношения. Например, H,16:9 будет означать, что 16 — это значение, соотвествующее высоте (H), а 9 — ширине (W).


Значение в layout_constraintDimensionRatio учитывается при расчете размеров view, только если хотя бы одна из сторон выставлена в match_constraint_wrap или match_constraint_spread.


Пример:


    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- Соотношение сторон 16:9
               Все constraints у view привязаны к сторонам ConstraintLayout.
               Ширина view будет равна ширине ConstraintLayout,
                так как layout_width — match_constraint_spread.
               Высота view будет рассчитана на основе соотношения,
                так как layout_height — any_size. -->
        <ImageView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:scaleType="centerCrop"
            app:layout_constraintDimensionRatio="16:9"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            android:background="@drawable/big_widescreen_image"/>

    </android.support.constraint.ConstraintLayout>

Соотношение сторон


Выставляем относительное расположение View внутри ConstraintLayout


Если у view привязать два горизонтальных constraints, то ей можно выставить горизонтальное относительное расположение. То же применимо и для вертикальных constraints.


За горизонтальное расположение отвечает атрибут layout_constraintHorizontal_bias, за вертикальное — layout_constraintVertical_bias. Указывается относительное расположение значением от 0 до 1.


По сути, это более гибкая замена атрибута layout_gravity. Например, для горизонтального расположения 0 будет означать расположение крайне слева, 0.5по центру, 1крайне справа. По умолчанию — 0.5.


Теперь, например, выставим значение 0.3. Это будет означать, что 30% не заполненного view места будет слева от view, а 70% — справа. Если же размер view больше размера расстояния между constraints, то 30% выходящего за constraints размера будет слева от ограничений, а 70% — справа.


Относительное расположение


Небольшое важное замечание: если в манифесте выставлена поддержка RTL языков, то layout_constraintHorizontal_bias вместо "слева" будет располагать элементы "от начала", а вместо "справа" — "от конца". То есть тем, кто поддерживает RTL языки стоит учитывать, что явно выставить расположение "слева" и "справа" не выйдет. По крайней мере, я такой возможности не нашел.


Пример:


    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- Горизонтальный bias выставлен в 0.3 (30% отступ слева/от начала).
               Вертикальный bias — в 1 (снизу).
               Constraints привязаны к сторонам контейнера.
               Ширина — match_constraint_wrap, высота — match_constraint_wrap. -->
        <TextView
            android:id="@+id/view_1"
            android:text="view_1"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintWidth_default="wrap"
            app:layout_constraintHeight_default="wrap"
            android:scaleType="centerCrop"
            app:layout_constraintHorizontal_bias="0.3"
            app:layout_constraintVertical_bias="1"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"/>

    </android.support.constraint.ConstraintLayout>

Особенности привязки линий


После презентации ConstraintLayout его часто сравнивали с RelativeLayout. Но, на самом деле, у них принципиально разные расчеты расположения элементов. В RelativeLayout у view просто указывается, с какой стороны другой view ей нужно находиться — "слева от", "справа от" и т.д. В ConstraintLayout constraints привязываются к сторонам других views и расположение view зависит от того, как ее constraints будут рассчитаны.


Для расположения constraint сперва рассчитывается расположение view, к которой этот constraint привязан. А для расположения view сперва рассчитываются все указанные для нее constraints. Циклические зависимости view и constraints при этом запрещены, так что, фактически, внутри ConstraintLayout строится направленный ациклический граф зависимостей constraints от view и view от constraints. Все расчеты производятся последовательно, начиная от не зависимых элементов графа.


Для простоты советую разделять вертикальные и горизонтальные зависимости на две независимые группы, как-будто внутри ConstraintLayout строится отдельно граф вертикальных зависимостей и отдельно — горизонтальных.


Но, вообще говоря, стоит понимать, что вертикальные параметры view, косвенно, могут зависеть от горизонтальных параметров другой view. Пример — расчет размера на основе соотношения сторон: при изменении ширины view меняется и ее высота. То есть, если у view изменится ширина из-за изменений ее горизонтальных constraints, то высота view тоже изменится.


Теперь рассмотрим любопытный пример расчета вертикальных constraints:


<android.support.constraint.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="wrap_content">

    <TextView
        android:id="@+id/view_A"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="View A"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <TextView
        android:id="@+id/view_B"
        android:text="View B"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintTop_toBottomOf="@id/view_A"
        app:layout_constraintBottom_toTopOf="@id/view_A"/>

    <TextView
        android:id="@+id/view_C"
        android:text="View C"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toRightOf="@id/view_A"
        app:layout_constraintTop_toBottomOf="@id/view_B"/>

</android.support.constraint.ConstraintLayout>

Результат:
Пример !


View A просто привязан к левой и верхней сторонам ConstraintLayout, то есть находится слева сверху.


View B привязан странным образом — Top(B)->bottom(A) и Bottom(B)->top(A), — расстояние между его вертикальными constraints, фактически, отрицательное. Сама высота B выставлена в match_constraint_spread.


View C находится справа от ALeft(C)toRight(A) — и (вроде как) снизу от BTop(C)toBottom(B).


По горизонтальному расположению вопросов возникнуть не должно. Теперь объясню вертикальное расположение.


Последовательность вертикальных расчетов:


  1. Для расчета C необходимо рассчитать ее нижний constraint;
  2. Для расчета нижнего constraint C необходимо рассчитать верхнюю сторону B;
  3. Для расчета верхней стороны B необходимо рассчитать ее нижний и верхний constraints;
  4. Для расчета нижнего и верхнего constraints B необходимо рассчитать верхнюю и нижнюю стороны A;
  5. A просто располагается слева-сверху, размеры рассчитывает сам.

Результаты вертикальных расчетов:


  1. Верхняя сторона A находится на уровне верхней стороны ConstraintLayout, нижняя рассчитывается по размеру текста A, так как у A высота wrap_content;
  2. Верхний constraint B на уровне нижней стороны A, нижний привязан к верхней стороне A, то есть он на уровне верхней стороны ConstraintLayout;
  3. Так как высота B — match_constraint_spread, то верхняя сторона B — на уровне нижней стороны A, а нижняя — на уровне верхней стороны ConstraintLayout. Это странно, но, фактически, высота B — отрицательная.
  4. Верхний constraint C привязан к нижней стороне B, то есть он на уровне верхней стороны ConstraintLayout;
  5. В итоге, верхняя сторона C на уровне верхней стороны ConstraintLayout, нижняя рассчитывается по размеру текста C, так как у A высота wrap_content.

В общем, на мой взгляд, такой алгоритм расчета стоит учитывать, чтобы понимать, где будет располагаться view в конечном счете.


Особенности привязки по Baseline


View, привязанная по Baseline, не может быть ограничена сверху и снизу, то есть Top и Bottom constraints будут игнорироваться. Это значит, что для такой view нельзя выставить размер match_constraint_spread или match_constraint_wrap.


Из этого не совсем очевидно следует, что по Baseline стоит привязывать невысокие view к высоким. Иначе есть шанс, что высокая view выйдет за рамки ConstraintLayout или размер ConstraintLayout будет рассчитан неверно.


Пример некорректной Baseline-привязки:


    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <!-- Размер текста 12sp -->
        <TextView
            android:id="@+id/left_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Left view"
            android:textSize="12sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

        <!-- Размер текста 20sp,
               то есть больший по размерам элемент привязан по базовой линии к меньшему -->
        <TextView
            android:id="@+id/right_view"
            android:text="Right view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20sp"
            app:layout_constraintBaseline_toBaselineOf="@id/left_view"
            app:layout_constraintLeft_toRightOf="@id/left_view"/>

    </android.support.constraint.ConstraintLayout>

Результат:
Некорректное отображение Baseline


Высота ConstraintLayout (в черной рамке) равна высоте большой TextView (в красной рамке), так как высота TextView выставлена, как wrap_content.


Базовая линия большой TextView привязана к базовой линии малой TextView (в зеленой рамке), так что текст находится на одной линии.


При этом большая TextView выходит за рамки ConstraintLayout.


Создаем цепи (chains)


При привязке сторон есть одно интересное правило — если привязать две стороны двух элементов друг к другу Left(B)toRight(A) и Right(A)toLeft(B), то элементы будут выделены в цепь и к ним будут применяться особые правила расположения.


Цепью считается набор элементов, стороны которых привязаны друг к другу. Цепи определяются автоматически на основе привязок элементов внутри ConstraintLayout. Цепь располагается на основе привязок ее крайних элементов, а элементы внутри цепи располагаются по правилам определенного стиля цепи. Стиль цепи задается атрибутом layout_constraint{X}_chainStyle, где XHorizontal для горизонтальных цепей или Vertical для вертикальных.


Пример цепи: правило Right(A)toLeft(B) + Left(B)toRight(A) свяжет элементы A и B в цепь, а Left(A)toLeft(parent) + Right(B)toRight(parent) привяжет всю цепь элементов к внешним сторонам ConstraintLayout.


Стиль цепи и его параметры берутся из атрибутов головного элемента цепи — самого левого, начального или самого верхнего.


Стиль spread


Элементы цепи распределяются равномерно, то есть отступы между элементами и от элементов до границ цепи будут одинаковые. Используется по умолчанию;


    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- По умолчанию chainStyle — spread, так что значения атрибута можно не указывать -->
        <TextView
            android:id="@+id/view_1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="view_1"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/view_2"/>

        <TextView
            android:id="@+id/view_2"
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:text="view_2"
            app:layout_constraintLeft_toRightOf="@id/view_1"
            app:layout_constraintRight_toLeftOf="@+id/view_3"/>

        <TextView
            android:id="@+id/view_3"
            android:layout_width="200dp"
            android:layout_height="wrap_content"
            android:text="view_3"
            app:layout_constraintLeft_toRightOf="@id/view_2"
            app:layout_constraintRight_toRightOf="parent"/>

    </android.support.constraint.ConstraintLayout>

Spread


Стиль spread_inside


Элементы цепи распределяются так же, как и при стиле spread, но отступы от границ цепи всегда равны нулю;


    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- ChainStyle головного элемента — spread_inside -->
        <TextView
            android:id="@+id/view_1"
            app:layout_constraintHorizontal_chainStyle="spread_inside"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="view_1"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/view_2"/>

        <TextView
            android:id="@+id/view_2"
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:text="view_2"
            app:layout_constraintLeft_toRightOf="@id/view_1"
            app:layout_constraintRight_toLeftOf="@+id/view_3"/>

        <TextView
            android:id="@+id/view_3"
            android:layout_width="200dp"
            android:layout_height="wrap_content"
            android:text="view_3"
            app:layout_constraintLeft_toRightOf="@id/view_2"
            app:layout_constraintRight_toRightOf="parent"/>

    </android.support.constraint.ConstraintLayout>

Spread inside


Стиль packed


Элементы располагаются группой друг за другом. Такой стиль позволяет устанавливать относительную позицию группы элементов в доступном цепи пространстве через атрибут layout_constraint{*}_bias. Bias атрибут нужно указывать у головного элемента цепи;


    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- ChainStyle головного элемента — packed, bias — 0.3 (30% отступ слева/от начала) -->
        <TextView
            android:id="@+id/view_1"
            app:layout_constraintHorizontal_chainStyle="packed"
            app:layout_constraintHorizontal_bias="0.3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="view_1"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/view_2"/>

        <TextView
            android:id="@+id/view_2"
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:text="view_2"
            app:layout_constraintLeft_toRightOf="@id/view_1"
            app:layout_constraintRight_toLeftOf="@+id/view_3"/>

        <TextView
            android:id="@+id/view_3"
            android:layout_width="200dp"
            android:layout_height="wrap_content"
            android:text="view_3"
            app:layout_constraintLeft_toRightOf="@id/view_2"
            app:layout_constraintRight_toRightOf="parent"/>

    </android.support.constraint.ConstraintLayout>

Packed
Packed bias


Стиль weighted


Элементы располагаются в соответствии с их весом по аналогии с тем, как работает LinearLayout. Чтобы такой стиль заработал, одна из view цепи должна иметь размер match_constraint_spread. Для указания веса элемента используются атрибуты layout_constraintHorizontal_weight и layout_constraintVertical_weight.


    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- Ширина  match_constraint_spread, weight=3,
               по ширине займет 3/4 свободного пространства -->
        <TextView
            android:id="@+id/view_1"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintHorizontal_weight="3"
            android:text="view_1"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/view_2"/>

        <!-- Ширина — match_constraint_spread, weight=1,
               по ширине займет 1/4 свободного пространства -->
        <TextView
            android:id="@+id/view_2"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintHorizontal_weight="1"
            android:text="view_2"
            app:layout_constraintLeft_toRightOf="@id/view_1"
            app:layout_constraintRight_toLeftOf="@+id/view_3"/>

        <!-- Ширина — any_size, займет столько пространства, сколько ей нужно для отрисовки -->
        <TextView
            android:id="@+id/view_3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="view_3"
            app:layout_constraintLeft_toRightOf="@id/view_2"
            app:layout_constraintRight_toRightOf="parent"/>

    </android.support.constraint.ConstraintLayout>

Weighted


Указываем отступы с учетом скрытых элементов


Отступы указываются стандартными атрибутами layout_margin{X}, где X — сторона отступа (Left/Right/Top/Bottom/Start/End). Эти отступы применяются к линиям привязки элемента, а не к сторонам элемента. Это значит, что линия привязки будет с отступом от стороны, к которой она привязана.


Отдельные правила были введены для скрытых элементов, то есть элементов, у которых Visibility выставлено в значение GONE. Когда элемент скрыт, обычные отступы от его сторон игнорируются, но используются специальные gone-отступы. Его размеры при этом при расчетах считаются равными нулю. Gone-отступы представлены атрибутами: layout_goneMargin{X}, где X — сторона отступа.


Другими словами, допустим, у элемента A выставлен отступ слева от элемента B равный 10dp, а gone-отступ слева выставлен 50dp. Если элемент B скрыт (GONE), то отступ элемента A слева будет 50dp, если элемент BVISIBLE или INVISIBLE, то отступ элемента A слева будет 10dp.


Пример:


    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/view_1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="gone"
            android:text="view_1"
            app:layout_constraintLeft_toLeftOf="parent"/>

        <!-- Левый constraint view_2 привязан к правой стороне view_1.
               Так как view_1 скрыта (gone), то используется отступ goneMarginLeft (50dp). -->
        <TextView
            android:id="@+id/view_2"
            android:layout_marginLeft="10dp"
            app:layout_goneMarginLeft="50dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="view_2"
            app:layout_constraintHorizontal_bias="0"
            app:layout_constraintLeft_toRightOf="@id/view_1"
            app:layout_constraintRight_toRightOf="parent"/>

    </android.support.constraint.ConstraintLayout>

Отступы и учет скрытых элементов


Разбираемся с Guidelines


Guideline — это аналог линии, устанавливаемой на макетах в визуальных редакторах, по которой дизайнеры выравнивают элементы. Такую линию представляет view класса android.support.constraint.Guideline. Guideline может быть горизонтальным или вертикальным — это указывается атрибутом android:orientation. Сам guideline нулевого размера, не занимает места в контейнере и всегда привязан только к сторонам ConstraintLayout.


Guideline используется, чтобы привязывать к нему стороны view выравнивая их тем самым по одной линии.


Расположение guideline контролируется тремя атрибутами:


  • layout_constraintGuide_begin — отступ от левой (Left, а не Start) стороны ConstraintLayout для вертикальных gudeline и от верхней стороны — для горизонтальных;
  • layout_constraintGuide_end — отступ от правой (Right, а не End) стороны ConstraintLayout для вертикальных guideline и от нижней стороны — для горизонтальных;
  • layout_constraintGuide_percent — относительный отступ guideline в процентах от левой стороны ConstraintLayout для вертикальных gudeline и от верхней стороны — для горизонтальных. Указывается числом от 0 до 1.

    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- Вертикальный guideline с относительным отступом 25% -->
        <android.support.constraint.Guideline
            android:id="@+id/line_1"
            android:orientation="vertical"
            app:layout_constraintGuide_percent="0.25"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <!-- Вертикальный guideline с отступом 100dp от левой стороны ConstraintLayout -->
        <android.support.constraint.Guideline
            android:id="@+id/line_2"
            android:orientation="vertical"
            app:layout_constraintGuide_begin="100dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <!-- Вертикальный guideline с отступом 50dp от правой стороны ConstraintLayout -->
        <android.support.constraint.Guideline
            android:id="@+id/line_3"
            android:orientation="vertical"
            app:layout_constraintGuide_end="50dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:id="@+id/view_1"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="view_1"
            app:layout_constraintLeft_toLeftOf="@id/line_1"
            app:layout_constraintRight_toRightOf="parent"/>

        <TextView
            android:id="@+id/view_2"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="view_2"
            app:layout_constraintTop_toBottomOf="@id/view_1"
            app:layout_constraintLeft_toLeftOf="@id/line_1"
            app:layout_constraintRight_toRightOf="@id/line_3"/>

        <TextView
            android:id="@+id/view_3"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="view_3"
            app:layout_constraintTop_toBottomOf="@id/view_2"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="@id/line_2"/>

    </android.support.constraint.ConstraintLayout>

Guidelines


Настраиваем параметры ConstraintLayout из кода


За это отвечает отдельный класс — android.support.constraint.ConstraintSet.


Создать этот ConstraintSet можно тремя способами:


  • скопировать параметры из существующего ConstraintLayout;
        ConstraintLayout constraintLayout = LayoutInflater.from(context).inflate(R.layout.my_constraint_layout, null);
        ConstraintSet constraintSet = new ConstraintSet();
        constraintSet.clone(constraintLayout);
  • скопировать параметры из *.xml файла, на основе которого создается ConstraintLayout;
        ConstraintSet constraintSet = new ConstraintSet();
        constraintSet.clone(context, R.layout.my_constraint_layout);
  • скопировать другой ConstraintSet.
        ConstraintSet constraintSet = new ConstraintSet();
        constraintSet.clone(constraintSet2);

Описание методов для изменения ConstraintSet можно посмотреть в документации (их очень много).


Чтобы применить ConstraintSet к ConstraintLayout используйте метод applyTo.


        final ConstraintLayout constraintLayout = (ConstraintLayout) findViewById(R.id.constr);
        ConstraintSet constraintSet = new ConstraintSet();
        constraintSet.clone(constraintLayout);
        constraintSet.setHorizontalBias(R.id.sample_view, 0.5f);
        constraintSet.applyTo(constraintLayout);

Естественно, также можно напрямую изменять LayoutParams конкретного элемента и затем явно вызывать requestLayout. Класс, представляющий все параметры расположения элемента в ConstraintLayout — это android.support.constraint.ConstraintLayout.LayoutParams.


Коротко об анимации


Для анимаций в ConstraintLayout не предусмотрено специальных методов. ConstraintLayout наследуется от ViewGroup, так что для анимации расположения элементов используются те же инструменты, что и для обычных контейнеров: ValueAnimator, TransitionManager и так далее.


Стоит ли пользоваться ConstraintLayout?


Основные преимущества:


  • обладает широким спектром возможностей, что позволяет реализовывать сложные взаиморасположения элементов на экране;
  • в Android Studio для этого контейнера имеется удобный визуальный редактор;
  • в большинстве случаев можно избежать излишней вложенности контейнеров друг в друга, что положительно влияет на производительность и читаемость кода;
  • заменяет стандартные контейнеры, то есть достаточно выучить один раз ConstraintLayout и не пользоваться в дальнейшем такими контейнерами, как: FrameLayout, LinearLayout, RelativeLayout, GridLayout, PercentRelativeLayout;
  • содержится в Support Library, так что баги будут правиться и, возможно, будут появляться новые функции.

Основные недостатки:


  • в силу большого количества возможностей уходит больше времени на понимание, как он работает;
  • при большом количестве элементов и взаимосвязей между ними сложно проводить код-ревью разметки — иерархическую вложенность контейнеров воспринимать проще, чем большое количество взаимосвязанных элементов в одном контейнере;
  • в определенных случаях по производительности может быть хуже иерархии стандартных контейнеров, так как дополнительно требуется время на построение дерева зависимостей;
  • несмотря на релизную версию, все еще можно наткнуться на неприятные баги;
  • атрибутов у views будет гораздо больше, чем при использовании обычных layouts.

В компании Touch Instinct мы собираемся, как минимум, попробовать этот компонент. Особенно интересно посмотреть, как он поведет себя при использовании в ячейках RecyclerView — там часто изменяются значения элементов, что обычно приводит к пересчету расположения элементов в ячейке и иногда пересчету ее размера.


Разработчики Google проделали большую работу и у ConstraintLayout есть все шансы стать стандартным при разработке элементом UI, какими стали те же RecyclerView и CoordinatorLayout.

Метки:
  • +11
  • 11,6k
  • 5
Touch Instinct 46,72
Разрабатываем мобильные приложения
Поделиться публикацией
Похожие публикации
Комментарии 5
  • 0
    Теперь я уже не имею причин не использовать (пробовать) ConstraintLayout. До этого момента отпугивали не только возможные баги, но и малое количество обзорных материалов «где все по полочкам». Большое спасибо за проделанный анализ!

    Столкнулись ли вы с багами, с какими? Пытались ли вы объективно сравнить время отрисовки классической верстки на Relative/Frame/Linear-layouts и с ConstraintLayout. Посоветуете инструмент?
    • 0
      Очевидные баги самого ConstraintLayout большинство пофикшены, я еще в октябре с ним разбирался — было очень много багов, сейчас явных не замечал. Во всяких странных кейсах типа «привязать view с внешней стороны контейнера, а к ней привязать другую view» скорее всего будут проблемы)

      При работе с ConstraintSet точно есть баги еще. Например, изменить веса у меня нормально не получилось в цепи через него.

      Насчет производительности — это отдельная тема, тут надо исходники смотреть подробно.
      К примеру, я точно знаю, что если view сама вызвала requestLayout (например, у нее стояла ширина any_size и был изменен текст), то ConstraintLayout полностью перестроит граф зависимостей, а это может занимать много времени. Это из кейсов изменения графа в рантайме.
      Другой тип кейсов — если сам граф зависимостей будет сложный, то расчет размеров может занимать много времени. Я подозреваю, что такое часто получается, если связывать горизонтальные и вертикальные зависимости (например, через dimensionRation).

      В общем, я бы не стал просто брать рядом LinearLayout и ConstraintLayout, смотреть, сколько занимает onMeasure+onLayout и делать из этого какие-то выводы)
    • 0
      А есть возможность выровнять элемент по центру высоты соседнего элемента без использования дополнительного внешнего контейнера?

      В левом верхнем углу есть ImageView, справа от него привязал вертикальный LinearLayoyt с 2 TextView.

      Разметка
      <android.support.constraint.ConstraintLayout
      xmlns:android=«http://schemas.android.com/apk/res/android»
      android:layout_width=«match_parent»
      android:layout_height=«match_parent»
      xmlns:app=«http://schemas.android.com/apk/res-auto»
      android:id="@+id/root">

      <ImageView
      android:id="@+id/image"
      app:layout_constraintLeft_toLeftOf="@id/root"
      app:layout_constraintRight_toLeftOf="@id/text_container"
      android:layout_width=«wrap_content»
      android:layout_height=«wrap_content»
      android:src="@drawable/logo"
      />

      <LinearLayout
      android:id="@+id/text_container"
      app:layout_constraintLeft_toRightOf="@id/image"
      android:orientation=«vertical»
      android:layout_width=«wrap_content»
      android:layout_height=«wrap_content»>

      <TextView
      android:id="@+id/count"
      android:layout_width=«wrap_content»
      android:layout_height=«wrap_content»
      android:text=«Products count» />

      <TextView
      android:id="@+id/price"
      android:layout_width=«wrap_content»
      android:layout_height=«wrap_content»
      android:text=«Price»/>


      </android.support.constraint.ConstraintLayout>



      Сборка в spread chain (spread) не помогает. По старому я бы просто поместил и ImageView и LinearLayout в relative-контейнер (wrap-content по высоте), разместил бы их относительно друг друга как надо ( android:layout_height=«wrap_content» не забыл бы) и каждому бы ещё android:layout_centerVertical=«true» добавил.

      А тут как-то с baseline можно попробовать или всё те же контейнеры?
      • 0
        Разметка
        <android.support.constraint.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">
        
        <!-- тут еще можно добавить bias=0, если элементы по левому краю должны быть выровнены -->
            <ImageView
                app:layout_constraintHorizontal_chainStyle="packed"
                android:id="@+id/logo"
                android:layout_width="wrap_content"
                android:layout_height="0dp"
                app:layout_constraintHeight_default="wrap"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toLeftOf="@+id/texts"
                app:layout_constraintTop_toTopOf="parent"
                android:src="@drawable/logo"/>
        
            <LinearLayout
                android:id="@+id/texts"
                android:layout_width="wrap_content"
                android:layout_height="0dp"
                android:orientation="vertical"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintHeight_default="wrap"
                app:layout_constraintLeft_toRightOf="@+id/logo"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent">
        
                <TextView
                    android:id="@+id/count"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="COUNT"/>
        
                <TextView
                    android:id="@+id/price"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="PRICE"/>
        
            </LinearLayout>
        
        </android.support.constraint.ConstraintLayout>

        Это ваш модифицированный вариант — обе вьюхи по вертикали привязываются к верхним сторонам констрейнта и им ставится высота match_constraint_wrap. По умолчанию bias=0.5, так что они отцентрируются по вертикали.


        Если попробовать избавиться от LinearLayout'a, то, к сожалению, можно напороться на багу

        • 0

          Хотя про багу это я погорячился — это просто очень крутое не очевидное поведение)

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое