Перейти к основному содержимому

Компоненты пользовательского интерфейса на Android

Разработчику Архитектору Инженеру

Элементы пользовательского интерфейса на Android​

Android UI - View и ViewGroup в legacy​

Глубокий разбор View / ViewGroup и XML-макетов — для legacy и понимания того, что под капотом у Jetpack Compose. Новым проектам чаще сразу Compose.

Пользовательский интерфейс (user interface, UI) в Android — это совокупность визуальных компонентов и интерактивных элементов, через которые пользователь взаимодействует с приложением. Он формируется динамически во время выполнения, в отличие от веб-интерфейсов, где разметка часто отделяется от логики на этапе исполнения. В Android UI строится на основе иерархической структуры объектов, каждый из которых отвечает за отображение части экрана и обработку пользовательского ввода. Эта структура реализована в виде древовидной компоновки, корень которой — объект View, а узлы — производные классы View и ViewGroup.

Построение интерфейса в Android тесно связано с особенностями операционной системы — поддержкой различных плотностей пикселей, ориентаций экрана, размеров устройств, режимов доступности, языковых и региональных настроек. По этой причине концепция UI в Android включает в себя систему ресурсов, механизмы привязки макетов, управление состоянием, а также абстракции, позволяющие адаптировать внешний вид под контекст выполнения.


Архитектурная база​

Основой графического интерфейса в Android является класс android.view.View. Это абстрактный базовый класс, экземпляры которого представляют собой прямоугольные области экрана, способные выполнять три ключевые функции:

  1. Отрисовка — преобразование внутреннего состояния объекта в пиксели на экране.
  2. Размер и расположение — измерение собственных размеров и позиционирование в рамках родительского контейнера.
  3. Обработка событий — реакция на касания, нажатия, прокрутку, фокус и другие действия пользователя.

У всех визуальных элементов — будь то кнопка, текстовое поле или изображение — есть общий предок View. Однако сам по себе экземпляр View не содержит визуального содержимого; он определяет поведение. Визуальное наполнение предоставляют его подклассы — например, TextView, ImageView, Button.

Для группировки и компоновки элементов используется класс ViewGroup — наследник View, расширяющий его возможностями управления дочерними элементами. ViewGroup не отрисовывает собственного содержимого (его onDraw() по умолчанию пуст), но реализует логику измерения (onMeasure) и размещения (onLayout) дочерних View. Именно контейнеры типа LinearLayout, RelativeLayout, ConstraintLayout являются подклассами ViewGroup. Они определяют правила компоновки — линейное выравнивание, привязку к границам, ограничения относительно других элементов и так далее.

Архитектура отрисовки в Android строится на паттерне измерение → размещение → отрисовка (measure → layout → draw), инициируемом системой при изменениях в иерархии (например, при изменении размера окна, добавлении элемента, изменении текста). Процесс начинается от корневого View, обычно — DecorView, который представляет собой корневой контейнер окна приложения. Затем рекурсивно обрабатываются все дочерние элементы.

Важно — несмотря на то, что иерархия View создаётся в основном потоке (main thread, также называемом UI-потоком), сама отрисовка выполняется отдельным системным компонентом — RenderThread, начиная с Android 5.0 (API 21). Это позволяет разгрузить Java-поток от низкоуровневых операций растеризации и повысить плавность анимаций. Однако логика измерения и размещения остаётся в UI-потоке, и её блокировка приводит к замедлению отклика интерфейса.


Жизненный цикл View​

У элемента интерфейса есть собственный жизненный цикл, хотя он и менее формализован, чем жизненный цикл Activity или Fragment. Ключевые этапы:

  • Создание — происходит при инфляции макета (LayoutInflater.inflate()) или при программном создании (new Button(context)). В этот момент вызываются конструкторы View, в которых устанавливаются начальные параметры, обрабатываются атрибуты из XML (если применимо), инициализируются внутренние поля.
  • Присоединение к окну (attach) — при добавлении View в иерархию, которая уже связана с окном (Window), вызывается onAttachedToWindow(). Здесь может производиться регистрация слушателей, инициализация анимаций, получение ссылок на системные сервисы (например, WindowManager).
  • Измерение (onMeasure) — система запрашивает у View его желаемые размеры с учётом ограничений родителя (например, "максимум 300dp по ширине") и собственных параметров (layout_width, layout_height, внутренние отступы, содержимое). Результатом является фиксация размеров через setMeasuredDimension().
  • Размещение (onLayout) — только для ViewGroup. Здесь вызывается layout() для каждого дочернего элемента с указанием его координат (left, top, right, bottom) внутри родителя.
  • Отрисовка (onDraw) — вызывается после завершения измерения и размещения. Здесь View получает Canvas, на котором может рисовать своё содержимое с помощью Paint, Path, Bitmap и других примитивов. Эта фаза может вызываться многократно — при скролле, анимации, изменении состояния (например, нажатие кнопки).
  • Изменение состояния — View может переходить между различными состояниями — enabled/disabled, pressed, focused, selected, hovered. Эти состояния влияют на внешний вид (через state list drawables) и поведение (например, отключение обработки касаний).
  • Отсоединение от окна (detach) — при уничтожении фрагмента, смене макета или закрытии активити вызывается onDetachedFromWindow(). Здесь следует освобождать ресурсы — отменять анимации, удалять callback’и, освобождать ссылки на контекст (во избежание утечек памяти).

Особое внимание уделяется управлению памятью и ссылками на Context. Поскольку View хранит ссылку на контекст (обычно Activity), удерживание ссылки на View после уничтожения активити ведёт к утечкам. Поэтому при использовании View вне стандартного жизненного цикла (например, в кэше, фоновом потоке, глобальном объекте) требуется аккуратная очистка.


Атомарные элементы интерфейса​

TextView​

TextView — базовый класс для отображения текстового содержимого. Несмотря на простое название, он реализует значительную часть функциональности, связанной с отображением, форматированием и взаимодействием с текстом. Сам по себе он не предназначен для редактирования (в отличие от EditText, его подкласса), но поддерживает богатое оформление — жирный, курсив, подчёркивание, цвета, размеры, шрифты, а также встроенную поддержку SpannableString, позволяющую применять разные стили к отдельным частям текста.

TextView отвечает за обработку текстовых атрибутов из XML (android:text, android:textSize, android:textColor, android:fontFamily, android:maxLines, android:ellipsize и др.). Важно понимать, что многие из этих атрибутов транслируются в вызовы setTypeface(), setTextColor(), setLines() и т.д. на уровне Java/Kotlin после инфляции.

Особое значение имеет атрибут android:textAppearance — механизм, позволяющий выносить набор текстовых стилей в тему (<style>), обеспечивая единообразие и упрощая изменения стиля глобально. Это часть более широкой системы стилей и тем в Android, включая Material Theming.

TextView также поддерживает автоматическую локализацию направления текста (left-to-right / right-to-left) в зависимости от содержимого и языковой настройки устройства, начиная с Android 4.2 (API 17). Это реализовано через android:autoMirrored и android:layoutDirection.


EditText​

EditText — расширение TextView, добавляющее возможность редактирования текста. Он интегрируется с системной Input Method Editor (IME) — клавиатурой — и обеспечивает двустороннюю связь — отображает курсор, подсвечивает выделение, передаёт события ввода в IME, принимает введённые символы.

Ключевые аспекты EditText:

  • Тип ввода (inputType) — определяет, какая клавиатура будет показана (текстовая, числовая, телефон, email, пароль, дата и т.д.), а также какие символы допускаются. Задаётся через android:inputType, например — textPersonName, numberDecimal, textPassword, textEmailAddress. Это не только визуальная подсказка — IME и система фильтруют ввод на уровне InputFilter.
  • Фильтрация ввода — через setFilters() можно задать цепочку InputFilter, ограничивающих длину, набор символов, формат (например, маска телефона). Например, InputFilter.LengthFilter(10) ограничит ввод 10 символами.
  • Слушатели текста — TextWatcher позволяет отслеживать изменения до, в процессе и после ввода. Это критично для валидации в реальном времени, автозаполнения, динамического форматирования (например, добавление пробелов в номер карты).
  • Автозаполнение (Autofill) — начиная с Android 8.0 (API 26), EditText поддерживает фреймворк Autofill, позволяющий системе или сторонним менеджерам паролей предлагать значения (логин, пароль, адрес). Для корректной работы требуется указание android:autofillHints (например, "username", "password"), а также соблюдение политик безопасности (например, запрет автозаполнения для полей подтверждения пароля через android:importantForAutofill="no").

EditText часто используется в связке с TextInputLayout (из библиотеки Material Components), который добавляет плавающую подпись (floating label), иконки, валидационные сообщения и поддержку состояний (ошибка, предупреждение, успех).


Button и его производные​

Button — стандартный элемент для инициации действия. Он наследуется от TextView, поэтому поддерживает весь текстовый функционал, включая Spannable, выравнивание, иконки через drawableStart, drawableEnd и т.д.

Существует несколько специализированных подклассов:

  • AppCompatButton — совместимая версия Button из библиотеки AppCompat, обеспечивающая единообразный внешний вид на старых версиях Android (до 5.0). Рекомендуется использовать всегда, даже при minSdkVersion ≥ 21, для согласованности тем.
  • MaterialButton — более современная реализация от Google, входящая в Material Components for Android. Поддерживает:
    • Стилизацию через backgroundTint, strokeColor, strokeWidth, cornerRadius — без необходимости создания ShapeDrawable вручную.
    • Режимы — filled, outlined, text (аналог flat button).
    • Иконки с контролем отступов (iconGravity, iconPadding).
    • Состояния (enabled, checked, hovered) через StateListAnimator и RippleDrawable.
  • ImageButton — кнопка без текста, отображающая только изображение (android:src). Часто используется в панелях инструментов или для иконок действий.
  • FloatingActionButton (FAB) — круглая кнопка с тенью, придерживающаяся Material Проектирование принципа "primary action". Поддерживает анимацию, расширение (speed dial), иконки, и может быть привязана к BottomAppBar.

Важно — все кнопки наследуют поведение View#performClick(), которое гарантирует вызов OnClickListener, а также генерацию событий Accessibility (например, для TalkBack). Прямой перехват onTouchEvent без вызова super может нарушить доступность.


Контролы выбора и переключения состояний​

Эти элементы позволяют пользователю управлять бинарными или множественными состояниями.


CompoundButton и его реализации​

Базовый абстрактный класс CompoundButton инкапсулирует логику двойного состояния (checked/unchecked). От него наследуются:

  • CheckBox — независимый переключатель. Используется, когда выбор одного варианта не исключает другие (например, "получать уведомления", "запоминать пароль").
  • RadioButton — элемент группы взаимоисключающих вариантов. Важно — для корректной работы радиокнопки должны быть помещены в общий RadioGroup, который обеспечивает синхронизацию состояний (отключение всех, кроме выбранной).
  • Switch — ползунок-переключатель (toggle switch), визуально имитирующий физический выключатель. Является более современной заменой ToggleButton в большинстве сценариев.
  • ToggleButton — кнопка с двумя текстовыми состояниями ("ON"/"OFF" по умолчанию). Устарела в пользу Switch, но сохраняется для обратной совместимости.

Все CompoundButton генерируют событие OnCheckedChangeListener, передающее новое состояние (isChecked). Они также поддерживают android:button для замены стандартного индикатора (галочка, кружок) на кастомный Drawable, например, иконку.

Особенность — состояние checked сохраняется при пересоздании активити (например, при повороте экрана), если у элемента задан android:id, и не отключена автоматическая сохранность (setSaveEnabled(false)).


Spinner​

Spinner — выпадающий список выбора одного значения из множества. Внутри использует Adapter (например, ArrayAdapter, CursorAdapter) для привязки данных. Имеет два режима отображения:

  • Dropdown — по умолчанию: при нажатии открывается список в виде модального окна (на старых API) или встроенного списка (на новых).
  • Dialog — устаревший режим, открывающий полноэкранный диалог выбора.

Современная альтернатива — MaterialAutoCompleteTextView или ExposedDropdownMenu, которые обеспечивают более плавную интеграцию с Material Проектирование и поддержку поиска внутри списка.


Вспомогательные элементы интерфейса​

Прогресс-индикаторы​

  • ProgressBar — универсальный индикатор, поддерживает два режима:
    • Determinate (style="?android:attr/progressBarStyleHorizontal") — отображает процент завершения (setMax(), setProgress()).
    • Indeterminate (по умолчанию) — анимированная "крутилка", сигнализирующая о длительной операции без известного времени завершения.
  • CircularProgressIndicator и LinearProgressIndicator — из Material Components. Более гибкие, поддерживают цвета, анимации, отображение значения, прерывистые сегменты.

Разделители и невидимые элементы​

  • View с android:background и фиксированными размерами — классический способ создать горизонтальную или вертикальную линию-разделитель. Например:
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider" />

Использование системного атрибута listDivider гарантирует соответствие текущей теме.

  • Space — специализированный View, который ничего не отрисовывает, но занимает место в компоновке. Оптимизирован для случаев, где нужен только отступ (вместо View с android:background="@null").

Подсказки и тултипы​

  • Tooltip — всплывающая подсказка при долгом нажатии. Устанавливается программно через TooltipCompat.setTooltipText(view, "Описание"). Автоматически учитывает доступность и не перекрывает важные элементы.
  • Snackbar — временный баннер внизу экрана для не критичных уведомлений ("Сообщение отправлено", "Отменить"). Часть Material Components, интегрируется с CoordinatorLayout для правильного поведения при наличии FAB или BottomAppBar.

Работа с текстом — Spannable, MovementMethod, TextWatcher​

SpannableString и SpannableStringBuilder​

Позволяют применять разные стили внутри одного TextView — цвет, шрифт, кликабельность, изображения, подчёркивание и др. Каждый стиль представлен span — объектом, реализующим интерфейсы типа CharacterStyle, UpdateLayout, ParcelableSpan.

Примеры:

  • ForegroundColorSpan — изменение цвета текста.
  • StyleSpan(Typeface.BOLD) — жирный.
  • ClickableSpan — участок текста становится кликабельным (требует setMovementMethod(LinkMovementMethod.getInstance())).
  • ImageSpan — вставка изображения в текст (например, смайлы).
  • RelativeSizeSpan, ScaleXSpan — изменение размера.

SpannableStringBuilder предпочтительнее SpannableString при динамическом изменении текста, так как позволяет модифицировать содержимое и спаны без полного пересоздания.


MovementMethod​

Определяет, как TextView реагирует на навигацию (клавиши, джойстик) и касания. Ключевые реализации:

  • ArrowKeyMovementMethod — для навигации по тексту стрелками (редко используется в мобильных приложениях).
  • LinkMovementMethod — обязателен для работы ClickableSpan и гиперссылок (android:autoLink="web"). Обеспечивает выделение, переход по ссылке, копирование URL.

TextWatcher​

Интерфейс с тремя методами:

  • beforeTextChanged() — вызывается до изменения текста. Используется редко, например, для сохранения предыдущего состояния.
  • onTextChanged() — вызывается в процессе изменения (например, при вводе символа). Аргументы — последовательность символов, позиция, число удалённых и добавленных символов.
  • afterTextChanged() — вызывается после завершения изменения. Здесь чаще всего проводится валидация, форматирование, обновление зависимых полей.

Важно: TextWatcher вызывается в UI-потоке и не должен содержать тяжёлых операций (например, сетевых вызовов без debounce).


Контейнеры компоновки — управление пространством и иерархией​

Контейнеры в Android отвечают за распределение пространства между дочерними элементами в соответствии с заданными правилами. Все они являются подклассами ViewGroup, но реализуют собственные алгоритмы onMeasure() и onLayout(), что определяет их поведение, гибкость и производительность.


FrameLayout​

Простейший контейнер. Позиционирует всех дочерних элементов относительно собственного верхнего левого угла. По умолчанию каждый новый элемент накладывается поверх предыдущего — отсюда название frame (кадр). Часто используется как корневой контейнер Fragment, или для реализации простых наложений — например, изображение + иконка-метка поверх него, или фон + центрированная кнопка.

FrameLayout поддерживает android:layout_gravity у дочерних элементов для выравнивания (top, bottom, center, start и т.д.), а также android:layout_margin. Так как он не проводит сложных расчётов, его измерение выполняется за O(n), где n — число дочерних элементов, и является одним из самых быстрых.


LinearLayout​

Располагает элементы в одном направлении: горизонтально (android:orientation="horizontal") или вертикально (vertical). Поддерживает:

  • android:layout_weight — механизм распределения остаточного пространства. Элементы с ненулевым весом делят свободное место пропорционально своим значениям. Например, два View с layout_weight="1" займут по 50 % свободной ширины в горизонтальном LinearLayout.
  • android:baselineAligned — выравнивание по базовой линии текста (полезно для строк с TextView и EditText на одном уровне).

Недостаток LinearLayout: вложенность нескольких LinearLayout с layout_weight приводит к двукратному проходу измерения (onMeasure() вызывается дважды для некоторых элементов), что снижает производительность. Это происходит потому, что при наличии веса первый проход определяет минимальные размеры, а второй — распределяет оставшееся пространство.


RelativeLayout​

Позиционирует элементы относительно друг друга или относительно границ контейнера. Правила задаются через атрибуты вида android:layout_toRightOf, android:layout_below, android:layout_alignParentStart, android:layout_centerInParent и др.

RelativeLayout решает систему зависимостей между элементами — каждое правило создаёт направленное отношение ("A должен быть справа от B"). Для корректной отрисовки требуется топологическая сортировка. При наличии циклических зависимостей (например, A справа от B, а B — справа от A) поведение неопределено, и система может проигнорировать часть правил.

Производительность RelativeLayout — O(n²) в худшем случае, так как каждый элемент может зависеть от любого другого. Это делает его непригодным для сложных или динамических макетов.


ConstraintLayout​

Современный контейнер, разработанный Google для замены вложенных LinearLayout и RelativeLayout. Основан на системе ограничений (constraints) — каждая граница элемента (start, end, top, bottom, baseline) может быть привязана к границе другого элемента, к направляющей (guideline), к барьеру (barrier), или к родителю.

Ключевые преимущества:

  • Плоская иерархия — ConstraintLayout позволяет реализовать сложные компоновки в одном уровне, без вложенности, что уменьшает глубину дерева View и ускоряет измерение.
  • Гибкие соотношения — поддержка layout_constraintHorizontal_bias (смещение вдоль оси), layout_constraintDimensionRatio (фиксированное соотношение сторон), layout_constraintWidth_percent (ширина как процент от родителя).
  • Барьеры и цепочки — Barrier группирует элементы и создаёт динамическую границу по их краям (например, "правая граница — по самому правому из A, B, C"); chains (цепи) позволяют распределять элементы вдоль оси с заданным поведением (spread, spread_inside, packed).
  • Оптимизированный движок измерения — начиная с версии 2.0, ConstraintLayout использует компилятор ограничений (Constraint Solver), работающий в O(n) для большинства практических случаев.

ConstraintLayout не входит в системный Android и подключается через отдельную библиотеку (androidx.constraintlayout:constraintlayout). Он является рекомендуемым контейнером для большинства сценариев, особенно при адаптации под разные размеры экрана.


ScrollView и NestedScrollView​

ScrollView — контейнер, позволяющий прокручивать один дочерний элемент (если дочерних больше — поведение неопределено). Он добавляет прокрутку поверх содержимого. Основное ограничение — нельзя помещать в ScrollView другие прокручиваемые контейнеры (RecyclerView, ListView, WebView), так как возникает конфликт жестов (gestures collision).

Для вложенной прокрутки (например, RecyclerView внутри вертикального скролла) используется NestedScrollView — специализированная версия, поддерживающая вложенную прокрутку (nested scrolling) через NestedScrollingChild и NestedScrollingParent интерфейсы. Однако даже NestedScrollView не рекомендуется использовать с RecyclerView напрямую — предпочтительнее объединять элементы в одну RecyclerView с разными типамиViewHolder.


Производительность и глубина иерархии​

Система измерения в Android требует рекурсивного обхода дерева View. Чем глубже иерархия, тем больше вызовов onMeasure() и onLayout(), тем выше вероятность пропуска кадров (jank). Поэтому:

  • Избегайте вложенности более 4–5 уровней.
  • Предпочитайте ConstraintLayout комбинациям LinearLayout + RelativeLayout.
  • Используйте <merge> в XML-макетах для устранения избыточных промежуточных ViewGroup.
  • Для повторяющихся элементов — только RecyclerView, никогда ScrollView + LinearLayout.

Списки и повторяющиеся элементы — от ListView к RecyclerView​

ListView (устаревший)​

ListView — первый стандартный компонент для отображения вертикальных списков. Основан на паттерне Adapter:

  • Adapter (BaseAdapter, ArrayAdapter) предоставляет данные и создаёт/обновляет View для каждой позиции.
  • convertView — механизм переиспользования: при прокрутке ListView передаёт уходящие из экрана View в Adapter.getView(), позволяя переиспользовать их вместо создания новых.

Недостатки:

  • Отсутствие встроенной поддержки анимаций изменений.
  • Сложность реализации разделителей, header/footer, разных типов строк.
  • Нет контроля над кэшированием.
  • Плохая интеграция с Material Проектирование (например, отсутствие ItemTouchHelper для свайпов).

RecyclerView — современный стандарт​

RecyclerView декомпозирует функциональность ListView на независимые компоненты:

  1. LayoutManager — отвечает за расположение элементов. Стандартные реализации:

    • LinearLayoutManager — вертикальный/горизонтальный список.
    • GridLayoutManager — сетка с фиксированным числом столбцов.
    • StaggeredGridLayoutManager — "водопад" (Masonry layout), где высота элементов может различаться.
  2. Adapter — связывает данные с ViewHolder. Обязательно переопределяет:

    • onCreateViewHolder() — создаёт ViewHolder (инициализирует View из макета).
    • onBindViewHolder() — заполняет ViewHolder данными для позиции n.
    • getItemCount() — возвращает общее число элементов.
  3. ViewHolder — хранит ссылки на View внутри элемента списка, предотвращая лишние вызовы findViewById(). Рекомендуется делать его статическим вложенным классом.

  4. ItemAnimator — управляет анимациями при добавлении, удалении, перемещении, изменении элементов. По умолчанию — DefaultItemAnimator.

  5. ItemDecoration — добавляет разделители, отступы, фоновые элементы (например, разделительные линии).

  6. ItemTouchHelper — поддержка жестов: свайп для удаления, перетаскивание для сортировки.


DiffUtil и производительность обновлений​

При изменении данных RecyclerView не позволяет просто заменить список — это приведёт к полному пересозданию всех View. Вместо этого рекомендуется использовать DiffUtil — утилиту, вычисляющую минимальный набор операций (insert, remove, change, move) для перехода от старого списка к новому, используя алгоритм Myers’ diff.

DiffUtil.Callback требует реализации:

  • areItemsTheSame() — сравнение по идентификатору (например, id).
  • areContentsTheSame() — сравнение по содержимому (например, все поля объекта).

Расчёт различий может быть ресурсоёмким, поэтому его следует выполнять в фоновом потоке (например, через AsyncListDiffer или ListAdapter, который инкапсулирует DiffUtil и AsyncTask).


Кэширование и пул ViewHolder​

RecyclerView поддерживает два уровня кэширования:

  • Scrap — временно отсоединённые ViewHolder, ожидающие переиспользования в текущем кадре (например, при прокрутке).
  • Recycler — пул ViewHolder, готовых к переиспользованию. Размер пула можно настроить через setItemViewCacheSize().

При корректной реализации onBindViewHolder() (без утечек, без тяжёлых операций) RecyclerView обеспечивает плавную прокрутку даже при сотнях элементов.


Инструменты связывания — ViewBinding и DataBinding​

ViewBinding​

Механизм генерации тизейф-классов, предоставляющих прямые ссылки на View внутри XML-макета. Для activity_main.xml генерируется класс ActivityMainBinding, содержащий поля вида textView, button, и метод getRoot().

Преимущества:

  • Исключает findViewById() и связанные с ним NullPointerException.
  • Типобезопасен: ошибка в имени View обнаруживается на этапе компиляции.
  • Не требует аннотаций или специального синтаксиса в XML.

ViewBinding не вмешивается в логику приложения и не добавляет накладных расходов во время выполнения — привязка происходит через кэшированные View внутри сгенерированного класса.


DataBinding​

Более сложный фреймворк, позволяющий напрямую связывать данные с UI в XML:

<TextView
android:text="@{user.name}"
android:visibility="@{user.isAdmin ? View.VISIBLE : View.GONE}" />

Требует:

  • Оборачивания макета в <layout>.
  • Описания <Данные> с переменными.
  • Генерации Binding-класса, включающего ссылки на View и методы setUser(), invalidateAll(), executePendingBindings().

Ограничения:

  • Сложность отладки (ошибки в выражениях обнаруживаются только в runtime).
  • Риск утечек памяти при связывании с Context или View.
  • Замедление компиляции из-за генерации кода.
  • Конфликты с ViewBinding (нельзя использовать одновременно в одном модуле без явного отключения).

DataBinding целесообразен только при строгой MVVM-архитектуре с ViewModel и LiveData/StateFlow, где требуется двусторонняя привязка. В большинстве случаев предпочтительнее комбинация ViewBinding + явное обновление состояния в коде.


Анимации и переходы на уровне View​

Анимации в Android делятся на две основные категории: View Animation (устаревшая) и Property Animation (современная, рекомендуемая). Также существует отдельный уровень — Transitions, управляющий анимированным изменением целых фрагментов интерфейса.


Property Animation — ObjectAnimator, ValueAnimator, ViewPropertyAnimator​

Property Animation, введённая в Android 3.0 (API 11), изменяет фактические значения свойств объекта, а не только его отображение. Это позволяет анимировать любые числовые (и некоторые нечисловые) свойства — translationX, alpha, rotation, scaleX, backgroundColor, и даже кастомные поля.

  • ValueAnimator — базовый класс, генерирующий последовательность значений в заданном диапазоне. Не привязан к конкретному объекту; требует AnimatorUpdateListener для ручного применения значений.
  • ObjectAnimator — подкласс ValueAnimator, автоматически обновляющий указанное свойство целевого объекта (например, ObjectAnimator.ofFloat(view, "alpha", 0f, 1f)).
  • ViewPropertyAnimator — упрощённый интерфейс, доступный через view.animate(). Поддерживает цепочки вызовов и внутреннюю оптимизацию (например, объединение нескольких анимаций в одну транзакцию).

Ключевые особенности:

  • Анимации выполняются в рендер-потоке, не блокируя UI-поток после запуска.
  • Поддержка interpolator (AccelerateDecelerateInterpolator, LinearInterpolator, PathInterpolator) для контроля временной кривой.
  • Возможность отмены (cancel()) и приостановки (pause()/resume()) начиная с API 19.
  • Анимации не влияют на фактическую позицию View в иерархии — для ViewGroup остаются исходные координаты (измерение и размещение не пересчитываются). Чтобы сделать анимированный элемент кликабельным в новом положении, требуется использовать View.setPivotX/Y() и View.setTranslationX/Y() в связке с setClickable(true) и корректной обработкой onTouchEvent.

Системные переходы (Transitions)​

Transitions — фреймворк для анимированного переключения между состояниями интерфейса, представленными как Scene. В отличие от Property Animation, Transitions анализируют различия между двумя иерархиями View и автоматически генерируют анимации для изменений — появление, исчезновение, перемещение, изменение размера.

Основные компоненты:

  • Scene — захватывает состояние иерархии View в определённый момент (или строится из XML с помощью <scene>).
  • Transition — определяет, как анимировать изменения. Стандартные реализации:
    • ChangeBounds — анимирует изменение границ (left, top, right, bottom).
    • Fade — появление/исчезновение с прозрачностью.
    • Slide — сдвиг с края экрана.
    • ChangeTransform, ChangeImageTransform — анимация scale, rotation, pivot.
  • TransitionManager — инициирует переход: beginDelayedTransition(container) перед изменением макета, или go(scene) для перехода к сохранённому состоянию.

Transitions особенно полезны при:

  • Показе/скрытии панелей (например, расширение карточки).
  • Переключении между вкладками с сохранением позиций элементов.
  • Реализации "плавающей" иконки (FAB → панель инструментов).

Shared Element Transitions​

Механизм для анимированного перехода между экранами (Activity или Fragment) с плавным преобразованием общего элемента (например, миниатюра из списка → полноразмерное изображение).

Требует:

  1. У обоих элементов (startView, endView) задать одинаковый android:transitionName.
  2. При запуске Activity: ActivityOptions.makeSceneTransitionAnimation(activity, startView, transitionName).
  3. В целевой Activity разрешить window content transitions через requestWindowFeature(Window.FEATURE_ACTIVITY_TRANSITIONS) или тему (<item name="android:windowContentTransitions">true</item>).

Система автоматически рассчитывает трансформацию между начальным и конечным положениями, масштабом и формой элемента. Для сложных преобразований (например, изменение формы иконки) можно задать кастомный Transition через setSharedElementEnterTransition().

Важно: shared transitions работают только при hardware-accelerated rendering и могут вызывать мерцание при несовпадении View по типу (например, ImageView → TextView).


Доступность (Accessibility)​

Доступность — фундаментальное требование к пользовательскому интерфейсу. Android предоставляет богатый фреймворк для поддержки пользователей с ограниченными возможностями, в первую очередь — пользователей скринридеров (например, TalkBack).


Основные принципы​

  1. Семантическая насыщенность — каждый интерактивный элемент должен иметь однозначное назначение.
  2. Навигация без касания — поддержка фокуса (focusable, focusableInTouchMode), клавиатурного ввода.
  3. Контраст и масштабируемость — соответствие требованиям WCAG (например, контраст текста ≥ 4.5:1).
  4. Отказ от визуальных подсказок как единственного канала — например, цвета ошибок должны сопровождаться иконками или текстом.

Технические механизмы​

  • ContentDescription (android:contentDescription) — текстовое описание элемента для TalkBack. Должно быть кратким, ёмким, без избыточности ("Кнопка" — избыточно; "Сохранить черновик" — корректно). Для неинтерактивных элементов (например, декоративных изображений) значение должно быть @null или "", чтобы избежать "мусорного" чтения.

  • importantForAccessibility — управляет включением элемента в accessibility tree:

    • auto (по умолчанию) — система решает на основе типа View.
    • yes — всегда включать.
    • no — исключать, но сохранять дочерние элементы.
    • noHideDescendants — исключать всю ветвь.
  • AccessibilityNodeInfo — объект, который View предоставляет через onInitializeAccessibilityNodeInfo(). Позволяет задавать:

    • setClassName() — семантический тип (например, "android.widget.Button", даже если это кастомный View).
    • setAction() — поддерживаемые действия (ACTION_CLICK, ACTION_LONG_CLICK, ACTION_SCROLL_FORWARD).
    • setCollectionInfo() / setCollectionItemInfo() — для списков и таблиц.
  • AccessibilityDelegate — механизм переопределения поведения доступности без наследования View. Полезен при добавлении доступности в сторонние библиотеки.


Инструменты проверки​

  • TalkBack — встроенный скринридер (настраивается в Специальные возможности).
  • Accessibility Scanner — приложение от Google, анализирующее скриншоты и выдающее рекомендации (контраст, размер касаемых областей, описание).
  • adb shell dumpsys accessibility — выводит состояние accessibility service, включённые утилиты, активные узлы.
  • View.setAccessibilityLiveRegion() — для динамических обновлений (например, уведомлений), чтобы TalkBack автоматически зачитывал изменения.

Минимальный размер касаемой области — 48×48 dp (рекомендовано Google), что соответствует ~9 мм на большинстве устройств.


Адаптация под разные устройства и конфигурации​

Android-устройства различаются по плотности пикселей, размеру экрана, форме (вырез, отверстие), наличию клавиатуры, ориентации. UI должен корректно адаптироваться без горизонтального скролла, обрезки контента или перекрытия важных элементов.


Квалификаторы ресурсов​

Система ресурсов Android позволяет задавать альтернативные файлы в зависимости от конфигурации устройства. Каталоги ресурсов могут содержать суффиксы, например:

  • layout-sw600dp/ — макеты для экранов с наименьшей шириной ≥ 600 dp (планшеты).
  • values-land/ — ресурсы в альбомной ориентации.
  • drawable-xxhdpi/ — изображения для экранов с плотностью ~480 dpi.
  • layout-v21/ — макеты, использующие API 21+ (например, Elevation).

Принцип: система выбирает наиболее специфичный совпадающий каталог. При несовпадении — берётся базовый (layout/, values/).


Обработка изменений конфигурации​

При повороте экрана, изменении языка, подключении клавиатуры система по умолчанию пересоздаёт Activity. Это приводит к потере состояния, если оно не сохранено явно.

Стратегии:

  • Сохранение состояния через onSaveInstanceState() — для временных данных (текст в EditText, позиция скролла). Не подходит для объёмных данных.
  • Использование ViewModel — сохраняет данные при пересоздании Activity/Fragment, но не при уничтожении процесса.
  • Явное указание android:configChanges — отменяет пересоздание, но требует ручной обработки изменений в onConfigurationChanged(). Рекомендуется только для специфичных случаев (например, полный экран в видео).

WindowInsets и безопасные зоны​

Современные устройства имеют вырезы (notch), каплевидные отверстия, изогнутые края, скрытые навигационные панели. Отображение контента под этими областями может сделать его нечитаемым или некликабельным.

  • WindowInsets — объект, содержащий отступы, накладываемые системой — statusBars, navigationBars, displayCutout, ime (клавиатура).
  • View.setOnApplyWindowInsetsListener() — позволяет получать WindowInsets и корректировать padding/margin динамически.
  • fitsSystemWindows="true" — устаревший механизм, автоматически добавляющий padding. Часто конфликтует с ConstraintLayout; предпочтителен программный контроль.
  • WindowInsetsController (начиная с API 30) — управление видимостью системных панелей (setSystemBarsBehavior(), hide(), show()).

DisplayCutout предоставляет информацию о форме и расположении выреза: safeInsetTop, boundingRects. Для полноэкранного режима (LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES) содержимое может заходить в область выреза, но текст и кнопки должны оставаться вне safeInset.


Поддержка разных плотностей​

Изображения должны предоставляться в нескольких разрешениях:

  • mdpi (1x), hdpi (1.5x), xhdpi (2x), xxhdpi (3x), xxxhdpi (4x).
    Рекомендуется использовать векторную графику (VectorDrawable, AnimatedVectorDrawable), которая масштабируется без потерь и не требует множества файлов.

Для размеров и отступов — всегда использовать dp (density-independent pixels), для текста — sp (scale-independent pixels, учитывают настройки размера шрифта пользователя).


Современные тенденции — переход на Jetpack Compose​

Хотя текущая глава посвящена View-системе, важно обозначить контекст эволюции: Google активно развивает Jetpack Compose как декларативную замену View/ViewGroup.


Почему Compose?​

  • Декларативность — UI описывается как функция состояния (@Composable fun Greeting(name: String) { Text("Hello $name") }), а не как императивное изменение иерархии.
  • Инкрементальная перерисовка — только изменённые @Composable пересчитываются; используется система slot table и skippable композиций.
  • Единая кодовая база — отсутствие разделения на XML и код; стили, анимации, логика — в одном месте.
  • Глубокая интеграция с Kotlin — корутины, StateFlow, extension-функции, DSL для компоновки.

Гибридные сценарии​

Миграция происходит постепенно. Поддерживается:

  • ComposeView — View, встраивающий Compose-иерархию в существующее View-дерево.
  • AndroidView — Compose-обёртка для интеграции legacy View (например, MapView, WebView) в Compose.
  • Совместное использование ViewModel, Repository, сетевых слоёв — без изменений.

Jetpack Compose не отменяет знания View-системы — понимание measure/layout/draw, WindowInsets, accessibility, жизненного цикла остаётся критически важным — эти концепции переносятся в Compose на более абстрактном уровне.

Material Проектирование 3 реализован в первую очередь для Compose (androidx.compose.material3), хотя библиотека com.google.android.material:material также обновляется.


Тестирование пользовательского интерфейса​

Тестирование UI в Android делится на три уровня:

  1. Unit-тесты для логики UI — например, проверка TextWatcher, Adapter, DiffUtil.Callback. Выполняются на JVM (с Robolectric или без), быстро, изолированно.
  2. Интеграционные UI-тесты — проверка взаимодействия компонентов внутри одного экрана или между Activity/Fragment. Основной инструмент — Espresso.
  3. End-to-end (E2E) тесты — сквозные сценарии ("регистрация → вход → оформление заказа"), часто с участием backend-моков. Могут использовать Espresso в связке с MockWebServer, или инструменты вроде UI Automator для межприкладного взаимодействия.

Espresso — архитектура и ключевые компоненты​

Espresso — официальный фреймворк UI-тестирования от Google, интегрированный в AndroidX Test. Основан на трёх китах:

  • ViewMatcher — условия поиска элемента:
    withId(R.id.button), withText("Сохранить"), isDisplayed(), isEnabled(), hasFocus().
    Поддерживаются композиции — allOf(), anyOf(), not().

  • ViewAction — действия над элементом:
    click(), typeText("text"), clearText(), scrollTo(), closeSoftKeyboard().

  • ViewAssertion — проверки состояния:
    matches(isDisplayed()), matches(withText("Успешно")), doesNotExist().

Пример:

onView(withId(R.id.email_edit_text))
.perform(typeText("user@example.com"), closeSoftKeyboard())

onView(withId(R.id.login_button))
.perform(click())

onView(withId(R.id.welcome_text))
.check(matches(withText("Добро пожаловать, user@example.com")))

Работа с асинхронностью — IdlingResource​

Espresso автоматически синхронизируется с UI-потоком и message queue, но не с фоновыми операциями (сетевые вызовы, Coroutine, Handler с задержкой). Для корректного ожидания завершения таких операций используется IdlingResource.

Реализация требует:

  • isIdleNow: Boolean — текущее состояние (занят/свободен).
  • registerIdleTransitionCallback() — уведомление Espresso при переходе в idle.
  • Регистрация через Espresso.registerIdlingResources() перед тестом и отмена после.

Современные альтернативы:

  • CountingIdlingResource — упрощённая реализация для счётчика активных задач.
  • EspressoIdlingResource из androidx.test.espresso.idling.concurrent — интеграция с ExecutorService.
  • ComposeTestRule — в Compose тестирование асинхронности встроено (через awaitIdle()).

Скриншот-тесты и визуальная регрессия​

Для контроля внешнего вида используются:

  • Screenshot (из androidx.test.screenshot) — делает снимок экрана и сохраняет в sdcard/. Требует adb shell для извлечения.
  • Third-party решения — Paparazzi (JVM-скриншоты без эмулятора), Shot, Screengrab (Fastlane).
    Paparazzi генерирует изображения на основе View-иерархии в тестах, используя LayoutInflater и Canvas в памяти — без запуска эмулятора, что ускоряет проверку.

Важно — скриншот-тесты чувствительны к незначительным изменениям (шрифты, плотность), поэтому применяются выборочно — для критичных экранов (онбординг, платёжная форма).


Ограничения Espresso​

  • Не работает с WebView без дополнительного Web-API (onWebView()).
  • Сложности с кастомными View, не предоставляющими AccessibilityNodeInfo.
  • Тесты медленные (1–5 секунд на сценарий), требуют CI-инфраструктуры с эмуляторами.

Отладка и профилирование производительности UI​

Производительность UI оценивается по двум ключевым метрикам:

  • Frame time — время генерации одного кадра. Цель — ≤ 16.6 мс (60 FPS) или ≤ 8.3 мс (120 FPS).
  • Input latency — задержка между касанием и визуальной реакцией. Цель — ≤ 100 мс.

Инструменты Android Studio​

  • Layout Inspector
    Позволяет в реальном времени исследовать иерархию View, их границы, свойства (measuredWidth, translationX, visibility). Поддерживает захват с эмулятора и физического устройства. Особенно полезен для выявления:

    • Избыточных вложений (LinearLayout внутри ConstraintLayout без необходимости).
    • Прозрачных View с clickable=true, перехватывающих касания.
    • Невидимых, но измеряемых элементов (View.GONE vs View.INVISIBLE).
  • Система Trace (ранее Systrace)
    Записывает события ядра и фреймворка:

    • measure/layout/draw фазы,
    • работа RenderThread,
    • Choreographer#doFrame,
    • блокировки UI-потока (длительные Binder-вызовы, синхронные сетевые запросы).
      Визуализируется как хронограмма с цветовой кодировкой: зелёный — кадр уложился в лимит, красный — пропуск кадра.
  • GPU rendering profile
    Включается в Настройки → Для разработчиков → Профиль обработки GPU. Отображает столбчатую диаграмму по кадрам:

    • Draw — подготовка команд (CPU),
    • Prepare — синхронизация с GPU,
    • Process — выполнение команд (GPU),
    • Execute — отправка буфера на дисплей.
      Высокий Draw указывает на тяжёлые onDraw(), Prepare — на overdraw или сложные шейдеры.

Overdraw и оптимизация отрисовки​

Overdraw — многократная отрисовка одних и тех же пикселей разными View. Измеряется в слоях (1x — идеально, 4x — критично).

Методы снижения:

  • Удаление избыточных фонов (android:background="@null" у View, если родитель уже задаёт фон).
  • Использование clipToPadding="false" и clipChildren="false" только при необходимости.
  • Замена alpha < 1.0 на Color.TRANSPARENT в невидимых состояниях.
  • Применение setLayerType(LAYER_TYPE_HARDWARE, null) для анимаций (ускорение через GPU), но с осторожностью — потребляет видеопамять.

Анализ утечек памяти, связанных с UI​

Типичные сценарии:

  • Удержание ссылки на Activity в View (например, через анонимный OnClickListener).
  • Незакрытые Animator, зарегистрированные в View.
  • Кэширование Bitmap без LruCache.

Инструменты:

  • Android Profiler → Memory — захват heap dump, поиск retained instances.
  • LeakCanary — автоматическое обнаружение утечек, интеграция в debug-сборку.

Проверка: после уничтожения Activity все её View должны быть garbage-collected в течение одного GC-цикла.


Лучшие практики проектирования и сопровождения UI​

Именование ресурсов​

Система именования должна обеспечивать:

  • Однозначность назначения,
  • Локализуемость,
  • Поиск по префиксам.

Рекомендации:

  • id — fragment_login_email_edit_text, activity_main_fab — префикс по контексту (fragment_, activity_, item_, dialog_).
  • string — screen_login_title, error_network_unavailable — домен (screen_, error_, hint_, button_).
  • style: TextAppearance.Body1, Widget.Button.Primary — по иерархии Material (TextAppearance, Widget, ThemeOverlay).
  • dimen — spacing_small (4dp), spacing_medium (8dp), spacing_large (16dp) — семантические имена вместо margin_16dp.

Запрещено:

  • Использовать R.id.button1, R.string.text2.
  • Дублировать значения в dimen.xml (например, 16dp в трёх местах).

Изоляция стилей и тем​

Стили должны быть:

  • Наследуемыми — через parent,
  • Параметризованными — через ?attr/ (например, ?attr/colorPrimary),
  • Группируемыми — общие черты выносятся в базовый стиль (Widget.Base.Button).

Темы определяют глобальные атрибуты (colorPrimary, textAppearanceBody1, elevation), а стили — конкретное применение (style="@style/Widget.Button.Primary").

Для Material Проектирование 3 используется Theme.Material3.*, поддерживающий динамические цвета (?attr/colorPrimaryContainer), контрастные темы (light/dark), и компактные/плотные режимы.


Документирование UI-компонентов​

В проектах с длительным жизненным циклом (как "Вселенная IT") критически важно документировать:

  • Назначение кастомного View (в Javadoc класса),
  • Поддерживаемые атрибуты (в attrs.xml + Javadoc для declare-styleable),
  • Состояния и их визуальное отображение (например, таблица: enabled + pressed + focused → background = ripple(#42000000)),
  • Ограничения использования ("не использовать внутри ScrollView", "требует TextInputLayout").

Для открытых библиотек — обязательны sample в @sample-блоках и README.md с визуальными примерами.


Контроль глубины иерархии​

Целевые показатели:

  • Максимальная глубина: ≤ 6 уровней (от DecorView до листового View).
  • Число View на экране: ≤ 100 (для сложных экранов — ≤ 150).
  • Время onMeasure() + onLayout(): ≤ 8 мс.

Контрольные точки:

  • Регулярный запуск Layout Inspector,
  • Использование Hierarchy Viewer (устаревший, но полезный для анализа),
  • Статический анализ через Lint (Overdraw, NestedWeights, UselessParent).

Кросс-платформенные согласованности​

Если проект включает веб- и iOS-версии, важно согласовать:

  • Терминологию ("сохранить" vs "применить"),
  • Размеры касаемых областей (48 dp Android ↔ 44 pt iOS),
  • Порядок элементов (FAB внизу справа → iOS — плавающая кнопка в правом нижнем углу),
  • Цветовые токены (Material Проектирование ↔ Apple Human Interface Guidelines).

Это снижает когнитивную нагрузку при использовании нескольких платформ.


Частые ошибки​

СимптомПричина
OverdrawЛишние непрозрачные фоны
InflateExceptionОшибка XML или несовместимый атрибут
Кнопка не кликаетсяПерекрытие View с clickable=true
RTL "ломает" layoutНе start/end, а left/right

Что попробовать​

  1. Layout Inspector в Android Studio на одном экране.
  2. Тот же экран на Compose — сравните строк кода.
  3. Kotlin в мобильных — следующий шаг после XML.

В подборках​

Статья входит в тематические подборки и блок "С чего начать?" на главной. Соседние шаги того же маршрута:

Мобильная разработка — Мобильные приложения, Сборка и развёртывание мобильных приложений, Мобильные приложения, Супераппы, Android, Беспроводные технологии - Bluetooth, Zigbee, NFC.