Макеты в представлениях

Попробуйте Compose
Jetpack Compose – это рекомендуемый набор инструментов для создания пользовательского интерфейса в Android. Узнайте, как работать с макетами в Compose.

Макет определяет структуру пользовательского интерфейса в приложении, например в действии. Все элементы макета создаются с помощью иерархии объектов View и ViewGroup. View обычно рисует что-то, что пользователь может увидеть и с чем может взаимодействовать. ViewGroup – это невидимый контейнер, который определяет структуру макета для объектов View и других объектов ViewGroup, как показано на рисунке 1.

Рисунок 1. Иллюстрация иерархии View, которая определяет макет пользовательского интерфейса.

Объекты View часто называют виджетами. Они могут относиться к одному из множества подклассов, например Button или TextView. Объекты ViewGroup обычно называются макетами и могут быть одного из многих типов, которые обеспечивают различную структуру макета, например LinearLayout или ConstraintLayout.

Объявить макет можно двумя способами:

  • Объявление элементов интерфейса в XML. В Android используется простой словарь XML, который соответствует классам и подклассам View, например для виджетов и макетов. Вы также можете использовать редактор макетов Android Studio, чтобы создать макет XML с помощью интерфейса перетаскивания.

  • Создавайте экземпляры элементов макета во время выполнения. Ваше приложение может создавать объекты View и ViewGroup и программно управлять их свойствами.

Объявление пользовательского интерфейса в XML позволяет отделить представление приложения от кода, который управляет его поведением. Использование XML-файлов также упрощает предоставление различных макетов для разных размеров и ориентаций экрана. Подробнее об этом рассказывается в статье Поддержка экранов разных размеров.

Фреймворк Android позволяет использовать любой из этих методов или оба сразу. Например, вы можете объявить макеты приложения по умолчанию в XML, а затем изменить их во время выполнения.

Создайте XML-файл

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

Каждый файл макета должен содержать ровно один корневой элемент, который должен быть объектом View или ViewGroup. После определения корневого элемента вы можете добавить дополнительные объекты макета или виджеты в качестве дочерних элементов, чтобы постепенно создать иерархию View, определяющую ваш макет. Например, в следующем макете XML используется вертикальный элемент LinearLayout, содержащий элементы TextView и Button:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical" >
    <TextView android:id="@+id/text"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="Hello, I am a TextView" />
    <Button android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello, I am a Button" />
</LinearLayout>

После того как вы объявите макет в XML, сохраните файл с расширением .xml в каталоге res/layout/ проекта для Android, чтобы он правильно компилировался.

Подробные сведения о синтаксисе XML-файла макета можно найти в описании ресурса макета.

Загрузка XML-ресурса

При компиляции приложения каждый XML-файл макета компилируется в ресурс View. Загрузите ресурс макета в реализации обратного вызова Activity.onCreate() приложения. Для этого вызовите метод setContentView(), передав ему ссылку на ресурс разметки в следующем формате: R.layout.layout_file_name. Например, если макет XML сохранен как main_layout.xml, загрузите его для Activity следующим образом:

Kotlin

fun onCreate(savedInstanceState: Bundle) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main_layout)
}

Java

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main_layout);
}

При запуске Activity фреймворк Android вызывает метод обратного вызова onCreate() в Activity. Дополнительную информацию о жизненных циклах объектов activity вы можете найти в статье Введение в объекты activity.

Атрибуты

Каждый объект View и ViewGroup поддерживает свой набор атрибутов XML. Некоторые атрибуты относятся к объекту View. Например, TextView поддерживает атрибут textSize. Однако эти атрибуты также наследуются любыми объектами View, которые расширяют этот класс. Некоторые из них являются общими для всех объектов View, поскольку они унаследованы от корневого класса View, например атрибут id. Другие атрибуты считаются параметрами макета, которые описывают определенные ориентации макета объекта View, как определено родительским объектом ViewGroup этого объекта.

ID

С каждым объектом View можно связать целочисленный идентификатор, который будет уникальным View в пределах дерева. При компиляции приложения этот идентификатор указывается как целое число, но обычно он назначается в XML-файле макета как строка в атрибуте id. Это атрибут XML, общий для всех объектов View и определенный классом View. Вы используете его очень часто. Синтаксис для идентификатора внутри XML-тега выглядит следующим образом:

android:id="@+id/my_button"

Символ @ в начале строки указывает, что синтаксический анализатор XML должен обработать и расширить остальную часть строки идентификатора и распознать ее как ресурс идентификатора. Символ плюса (+) означает, что это новое название ресурса, которое нужно создать и добавить в ресурсы в файле R.java.

Фреймворк Android предлагает множество других ресурсов идентификаторов. При ссылке на идентификатор ресурса Android символ плюс не нужен, но необходимо добавить пространство имен пакета android, как показано ниже:

android:id="@android:id/empty"

Пространство имен пакета android указывает, что вы ссылаетесь на идентификатор из класса ресурсов android.R, а не из класса локальных ресурсов.

Чтобы создать представления и ссылаться на них из приложения, можно использовать следующий шаблон:

  1. Определите представление в файле макета и назначьте ему уникальный идентификатор, как в следующем примере:
    <Button android:id="@+id/my_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/my_button_text"/>
  2. Создайте экземпляр объекта представления и получите его из макета, как правило, в методе onCreate(), как показано в следующем примере:

    Kotlin

    val myButton: Button = findViewById(R.id.my_button)

    Java

    Button myButton = (Button) findViewById(R.id.my_button);

При создании RelativeLayout важно определить идентификаторы для объектов представления. В относительном макете дочерние представления могут определять свой макет относительно другого дочернего представления, на которое ссылается уникальный идентификатор.

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

Параметры макета

Атрибуты макета XML с именем layout_something определяют параметры макета для View, которые подходят для ViewGroup, в котором он находится.

Каждый класс ViewGroup реализует вложенный класс, который расширяет ViewGroup.LayoutParams. Этот подкласс содержит типы свойств, которые определяют размер и положение каждого дочернего представления, подходящего для группы представлений. Как показано на рисунке 2, родительская группа представлений определяет параметры макета для каждой дочерней группы представлений, включая дочернюю группу представлений.

Рисунок 2. Визуализация иерархии View с параметрами макета, связанными с каждым View.

У каждого подкласса LayoutParams есть собственный синтаксис для настройки значений. Каждый дочерний элемент должен определять LayoutParams, подходящий для его родительского элемента, хотя он также может определять другой LayoutParams для своих дочерних элементов.

Все группы представлений включают ширину и высоту, используя layout_width и layout_height, и каждое представление должно их определять. Для многих LayoutParams можно задать поля и границы.

Вы можете указать точные размеры, но это не всегда удобно. Чаще всего для установки ширины или высоты используется одна из следующих констант:

  • wrap_content – указывает, что размер представления должен соответствовать размеру контента.
  • match_parent – представление должно быть максимально большим, насколько позволяет родительская группа представлений.

В общем случае мы не рекомендуем указывать ширину и высоту макета в абсолютных единицах, например в пикселях. Лучше использовать относительные единицы измерения, например пиксели, не зависящие от плотности экрана (dp), wrap_content или match_parent. Это поможет обеспечить правильное отображение приложения на экранах разных размеров. Допустимые типы измерений определены в ресурсе разметки.

Позиция макета

Область просмотра имеет прямоугольную форму. У него есть местоположение, выраженное в виде пары координат left и top, и два размера, выраженные в виде ширины и высоты. Единица измерения для местоположения и размеров – пиксель.

Вы можете получить местоположение представления, вызвав методы getLeft() и getTop(). Первый метод возвращает левую координату (x) прямоугольника, представляющего область просмотра. Последний возвращает верхнюю координату (y) прямоугольника, представляющего представление. Эти методы возвращают местоположение представления относительно его родительского элемента. Например, если getLeft() возвращает 20, это означает, что представление находится на 20 пикселей правее левого края своего непосредственного родительского элемента.

Кроме того, есть удобные методы, позволяющие избежать ненужных вычислений: getRight() и getBottom(). Эти методы возвращают координаты правого и нижнего краев прямоугольника, представляющего представление. Например, вызов getRight() аналогичен следующему вычислению: getLeft() + getWidth().

Размер, поля и отступы

Размер представления определяется его шириной и высотой. У представления есть две пары значений ширины и высоты.

Первая пара – это измеренная ширина и измеренная высота. Эти размеры определяют, насколько большим должно быть представление в родительском элементе. Получить измеренные размеры можно с помощью методов getMeasuredWidth() и getMeasuredHeight().

Вторая пара – это ширина и высота (иногда ширина отрисовки и высота отрисовки). Эти размеры определяют фактический размер представления на экране во время рисования и после макета. Эти значения могут отличаться от измеренных ширины и высоты. Ширину и высоту можно получить, вызвав методы getWidth() и getHeight().

Чтобы измерить размеры, представление учитывает его отступы. Отступы указываются в пикселях для левой, верхней, правой и нижней частей представления. Вы можете использовать отступы, чтобы сместить контент на определенное количество пикселей. Например, если задать отступ слева, равный двум, то контент будет сдвинут на два пикселя вправо от левого края. Вы можете задать отступы с помощью метода setPadding(int, int, int, int) и запросить их, вызвав getPaddingLeft(), getPaddingTop(), getPaddingRight() и getPaddingBottom().

Хотя представление может определять отступы, оно не поддерживает поля. Однако группы представлений поддерживают поля. Дополнительную информацию можно найти в статьях ViewGroup и ViewGroup.MarginLayoutParams.

Подробнее о параметрах…

Поля и отступы можно задавать не только программно, но и в макетах XML, как показано в следующем примере:

  <?xml version="1.0" encoding="utf-8"?>
  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical" >
      <TextView android:id="@+id/text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="16dp"
                android:padding="8dp"
                android:text="Hello, I am a TextView" />
      <Button android:id="@+id/button"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_marginTop="16dp"
              android:paddingBottom="4dp"
              android:paddingEnd="8dp"
              android:paddingStart="8dp"
              android:paddingTop="4dp"
              android:text="Hello, I am a Button" />
  </LinearLayout>
  

В примере выше показано, как применяются поля и отступы. В TextView поля и отступы одинаковы со всех сторон, а в Button показано, как можно задать их по отдельности для разных сторон.

Общие макеты

Каждый подкласс класса ViewGroup предоставляет уникальный способ отображения вложенных в него представлений. Самый гибкий тип макета, который предоставляет лучшие инструменты для поддержания неглубокой иерархии макета, – это ConstraintLayout.

Ниже приведены некоторые распространенные типы макетов, встроенные в платформу Android.

Создайте линейный макет

Организует дочерние элементы в одну горизонтальную или вертикальную строку и создает полосу прокрутки, если длина окна превышает длину экрана.

Создание веб-приложений в WebView

Показывает веб-страницы.

Создание динамических списков

Если контент для макета динамический или не определен заранее, можно использовать RecyclerView или подкласс AdapterView. RecyclerView обычно является лучшим вариантом, поскольку он использует память более эффективно, чем AdapterView.

Ниже приведены примеры макетов, которые можно создать с помощью RecyclerView и AdapterView.

List

Показывает прокручиваемый список в один столбец.

Сетка

Показывает прокручиваемую сетку со столбцами и строками.

RecyclerView предлагает больше возможностей и позволяет создать собственный менеджер макетов.

Заполнение представления адаптера данными

Вы можете заполнить AdapterView, например ListView или GridView, связав экземпляр AdapterView с Adapter, который получает данные из внешнего источника и создает View, представляющий каждую запись данных.

В Android есть несколько подклассов Adapter, которые позволяют получать разные типы данных и создавать представления для AdapterView. Вот два самых распространенных адаптера:

ArrayAdapter
Используйте этот адаптер, если источником данных является массив. По умолчанию ArrayAdapter создает представление для каждого элемента массива, вызывая toString() для каждого элемента и помещая содержимое в TextView.

Например, если у вас есть массив строк, которые вы хотите показать в ListView, инициализируйте новый ArrayAdapter с помощью конструктора, чтобы указать макет для каждой строки и массива строк:

Kotlin

    val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, myStringArray)
    

Java

    ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
            android.R.layout.simple_list_item_1, myStringArray);
    

Аргументы для этого конструктора:

  • Ваше приложение Context
  • Макет, содержащий TextView для каждой строки в массиве.
  • Массив строк

Затем вызовите setAdapter() на устройстве ListView:

Kotlin

    val listView: ListView = findViewById(R.id.listview)
    listView.adapter = adapter
    

Java

    ListView listView = (ListView) findViewById(R.id.listview);
    listView.setAdapter(adapter);
    

Чтобы настроить внешний вид каждого элемента, можно переопределить метод toString() для объектов в массиве. Если вы хотите создать представление для каждого элемента, отличного от TextView (например, ImageView для каждого элемента массива), расширьте класс ArrayAdapter и переопределите getView(), чтобы возвращать тип представления, который вы хотите использовать для каждого элемента.

SimpleCursorAdapter
Используйте этот адаптер, если данные поступают из Cursor. При использовании SimpleCursorAdapter укажите макет для каждой строки в Cursor и столбцы в Cursor, которые нужно вставить в представления макета. Например, если вы хотите создать список имен и номеров телефонов, вы можете выполнить запрос, который возвращает Cursor, содержащий строку для каждого человека и столбцы для имен и номеров. Затем создайте массив строк, указывающий, какие столбцы из Cursor вы хотите включить в макет для каждого результата, и целочисленный массив, указывающий соответствующие представления, в которых должен быть размещен каждый столбец:

Kotlin

    val fromColumns = arrayOf(ContactsContract.Data.DISPLAY_NAME,
                              ContactsContract.CommonDataKinds.Phone.NUMBER)
    val toViews = intArrayOf(R.id.display_name, R.id.phone_number)
    

Java

    String[] fromColumns = {ContactsContract.Data.DISPLAY_NAME,
                            ContactsContract.CommonDataKinds.Phone.NUMBER};
    int[] toViews = {R.id.display_name, R.id.phone_number};
    

При создании экземпляра SimpleCursorAdapter передайте макет, который будет использоваться для каждого результата, Cursor с результатами и два следующих массива:

Kotlin

    val adapter = SimpleCursorAdapter(this,
            R.layout.person_name_and_number, cursor, fromColumns, toViews, 0)
    val listView = getListView()
    listView.adapter = adapter
    

Java

    SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
            R.layout.person_name_and_number, cursor, fromColumns, toViews, 0);
    ListView listView = getListView();
    listView.setAdapter(adapter);
    

Затем SimpleCursorAdapter создает представление для каждой строки в Cursor, используя предоставленный макет и вставляя каждый элемент fromColumns в соответствующее представление toViews.

Если в течение жизненного цикла приложения вы измените базовые данные, которые считываются адаптером, вызовите метод notifyDataSetChanged(). Это уведомляет прикрепленное представление о том, что данные были изменены, и оно обновляется.

Обработка событий клика

Вы можете обрабатывать события кликов для каждого элемента в AdapterView, реализовав интерфейс AdapterView.OnItemClickListener. Пример:

Kotlin

listView.onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id ->
    // Do something in response to the click.
}

Java

// Create a message handling object as an anonymous class.
private OnItemClickListener messageClickedHandler = new OnItemClickListener() {
    public void onItemClick(AdapterView parent, View v, int position, long id) {
        // Do something in response to the click.
    }
};

listView.setOnItemClickListener(messageClickedHandler);

Дополнительные ресурсы

Посмотрите, как макеты используются в демоприложении Sunflower на GitHub.