03-layouts.md 40 KB

Макеты

Оглавление

Введение

Документация на layouts: https://developer.android.com/guide/topics/ui/declaring-layout.

Макет определяет визуальную структуру пользовательского интерфейса, например, пользовательского интерфейса экрана или отдельного виджета. Существует два способа создания макета:

  • Объявление элементов UI в XML. В Android имеется удобный способ описания XML-элементов для классов View и их подклассов.
  • Программное создание экземпляров визуальных элементов путем наследования классов от View и его производных.

В Android все классы визуальных элементов наследованы от View. От него унаследованы такие элементы как:

  • TextView — компонент для отображения текста,
  • ImageView — компонент для отображения изображения,
  • Button — стандартная кнопка,
  • EditText — компонент для ввода текста,
  • и другие компоненты, такие как CheckBox, Slider, Menu и др.

Для размещения нескольких элементов на экране используются линейные макеты (LinearLayout).
Разделяют горизонтальные и вертикальные макеты. Горизонтальные позволяют размещать элементы в ряд по горизонтали. Вертикальные — в столбик по вертикали, соответственно.

Для изучения процесса создания макетов создадим простое приложение "About Me", отображающее информацию о пользователе, а также позволяющее ввести какую-нибудь дополнительную информацию. В Android Studio для редактирования макетов используется редактор дизайнов макетов, он будет далее рассмотрен. Кроме этого, будет рассмотрен способ передачи данных от элементов интерфейса к их программной реализации с помощью Data Binding.

Для дальнейшего изучения необходимо создать новый проект "About Me". Шаги по созданию были описаны в конспекте №2.

Редактор макетов

Build a UI with Layout Editor

В проекте созданном по-умолчанию уже присутствует один файл с макетом. Это activity_main.xml. Здесь содержится компонент LinearLayout с контентом внутри. Файл макета располагается в каталоге res/layout, где располагаются все файлы макетов. Если кликнуть на файл дважды, откроется редактор макетов.

Редактор макетов предоставляет графический интерфейс, упрощающий процесс построения макетов и размещения в них элементов интерфейса. Редактор содержит следующие панели:

  1. Palette — список визуальных, элементов, которые можно добавить.
  2. Component Tree — дерево уже добавленных макетов и компонентов в виде иерархии.
  3. Toolbar — меню кнопок для настройки предварительного просмотра макета.
  4. Design editor — основное окно редактора, отображающее макет с компонентами.
  5. Attributes — панель с доступными свойствами и атрибутами текущего выбранного компонента с возможностью их редактирования.

layout editor image

Для добавления новых компонентов из Palette можно переносить их как на макет в Design editor, так и в определенное место иерархии на Component Tree.

Для переключения редактора к режиму редактирования XML-кода, необходимо выбрать вкладку Text внизу экрана редактора.

Добавление компонентов на макет с помощью редактора

Приложение "About Me" будет отображать имя и небольшое описание человека, а также возможность ввода и отображения никнейма. План:

  1. Добавить текстовые поля для отображения имени и описания.
  2. Добавить возможность скроллинга страницы, если текст не убирается на экран полностью.
  3. Добавить текстовое поле для ввода никнейма.
  4. Добавить кнопку для приема введенного никнейма.

Добавление TextView

Для добавления нового текстового поля необходимо в редакторе макетов взять элемент TextView из панели Palette и перенести на изображение экрана в Design Editor. На панели Attributes установить следующие параметры:

  • id = name_text_view — идентификатор элемента.
  • layout_width = match_parent — ширина элемента по ширине родителя, т.е. макета LinearLayout.
  • layout_height = wrap_content — высота элемента по высоте контента, т.е. отображаемого текста.
  • text = Steve Jobs — отображаемый текст.
  • textSize = 20sp — размер шрифта, указывается в sp (scale-independent pixels) независимых от масштабирования пикселях.
  • textColor = @android:color/black — стандартный черный цвет.
  • textAlignment = center — разместить контент (текст) по центру элемента TextView.

В Android принято хранить строки в отдельных ресурсах strings. Поэтому необходимо добавить текст с именем пользователя в отдельную строку:

<string name="name">Steve Jobs</string>

И указать в параметре text ссылку на добавленный ресурс @strings/name.

Размеры также принято хранить в ресурсах для возможности переиспользования. Файлов ресурсов с размерами имеет имя res/values/dimens.xml. Если его нет в проекте, его необходимо создать и добавить новое значение с размером шрифта текста:

<resources>
    <dimen name="text_size">20sp</dimen>
</resources>

И также указать ссылку на ресурс @dimen/text_size в параметр textSize.

Создание стиля TextView

В ситуации, когда у нас есть множество однотипных текстовых полей или каких-либо других элементов интерфейса, удобным становится создание стилей элементов. Стиль — набор произвольных атрибутов, задающиеся разработчиком. Например, можно добавить для текстового поля шрифт (font family), отступы контента от края элемента (padding) и элемента от края макета (margin), а затем выделить атрибуты в отдельный стиль.

Сперва добавим к TextView следующие атрибуты:

  • fontFamily = sans-serif — имя шрифта.
  • paddingTop = @dimen/small_padding — отступ текста от края верхнего элемента TextView, ссылка на предварительно добавленный ресурс с размером (small_padding=8dp).
  • layout_marginTop = @dimen/layout_margin — отступ TextView от верхнего края его родителя, т.е. элемента LinearLayout, ссылка на предварительно добавленный ресурс с размером layout_margin=16dp.

Существует два способа добавления стиля:

  1. Добавить стиль вручную в файл values/styles.xml.
  2. Использовать редактор макетов для автоматического создания стиля.

Для первого способа необходимо описать в файле values/styles.xml все атрибуты, которые необходимо объединить в стиль:

<style name="NameStyle">
    <item name="android:layout_marginTop">@dimen/layout_margin</item>
    <item name="android:fontFamily">sans-serif</item>
    <item name="android:paddingTop">@dimen/small_padding</item>
    <item name="android:textColor">@android:color/black</item>
    <item name="android:textSize">@dimen/text_size</item>
</style>

К элементу TextView необходимо добавить атрибут style="@style/NameStyle" для применения стиля.

Для использования второго способа необходимо сперва перейти в редактор стилей. Затем нажать на элементе TextView на панели с добавленными элементами Component Tree правой кнопкой мыши, выбрать Refactor -> Extract Style.... Откроется диалоговое окно со списком атрибутов, необходимо проставить галочки у тех, которые необходимо выделить в отдельный стиль и указать его имя, например, "NameStyle". Таким образом стиль будет создан автоматически и добавлен в файл values/styles.xml, а атрибут style будет автоматически установлен в выбранный элемент TextView.

Добавление ScrollView

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

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

<ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/bio_text"
        style="@style/NameStyle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:lineSpacingMultiplier="1.2"
        android:text="@string/bio" />
</ScrollView>

Для нового элемента TextView также указывается созданный стиль "NameStyle", создается строковый ресурс @string/bio с длинным текстом, а также добавляется новый параметр lineSpacingMultiplier — множитель интервала между строками в тексте.

Можно заметить, что добавленный текст располагается ровно от левого до правого края и это выглядит неаккуратно. Чтобы этого избежать Android Guidelines по дизайну предписывают добавление отступов от левого и правого края экрана. Для этого нужно добавить к корневому элементу атрибуты paddingStart и paddingEnd:

android:paddingStart="@dimen/padding"
android:paddingEnd="@dimen/padding"

Ресурс размера @dimen/padding берется равным 16dp в данном примере.

Добавление EditText

Перейдем к добавлению текстового поля для ввода никнейма.

Необходимо добавить элемент EditText на макет сразу после элемента TextView с именем (id=name_text_view). Можно сделать это вручную в XML, можно в редакторе макетов путем переноса элемента из панели Palette на панель Component Tree.

Для элемента EditText необходимо задать атрибуты:

  • id = nickname_edit_text,
  • layout_width = match_parent,
  • layout_height = wrap_content,
  • style = @style/NameStyle — ссылка на стиль "NameStyle",
  • textAlignment = center,
  • hint = @string/what_is_your_nickname — подсказка для ввода текста, отображается в поле полупрозрачным шрифтом, ссылка на строковый ресурс со строкой "What is your nickname?" (строку необходимо предварительно добавить).

Итоговый элемент должен быть следующим:

<EditText
    android:id="@+id/nickname_edit_text"
    style="@style/NameStyle"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="@string/what_is_your_nickname"
    android:textAlignment="center" />

Добавление Button

Ввод никнейма не должен быть бесполезным. Логично было бы его принять и отобразить как текст. Для этого необходимо добавить еще одно текстовое поле TextView для отображения никнейма, а также кнопку Button для принятия введенного в EditText никнейма и отображения его на новом TextView.

Сперва добавим кнопку для принятия введенного никнейма. Кнопку требуется расположить следом за текстовым полем EditText. Кнопке присваиваются следующие атрибуты:

  • id = done_button,
  • layout_width = wrap_content,
  • layout_height = wrap_content,
  • layout_gravity = center_horizontal — расположить кнопку в центре по горизонтали относительно макета,
  • fontFamily = sans-serif,
  • text = @string/done — ссылка на строковый ресурс с текстом "Done",
  • style = @style/Widget.AppCompat.Button.Colored — ссылка на стандартный стиль, позволяющий делать кнопку цветной.

Итоговый элемент:

<Button
    android:id="@+id/done_button"
    style="@style/Widget.AppCompat.Button.Colored"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:layout_marginTop="@dimen/layout_margin"
    android:fontFamily="sans-serif"
    android:text="@string/done" />

Для отображения введенного никнейма потребуется еще один элемент TextView. Добавить его необходимо после кнопки "Done". Новый TextView имеет абсолютно идентичные атрибуты, что и текстовое поле с id name_text_view.
Отличия: id=nickname_text_view и visibility=gone (скрыть текстовое поле по умолчанию).

Далее необходимо добавить обработчик события нажатия на кнопку "Done", который будет текст из EditText помещать в текстовое поле nickname_text_view и скрывать ненужный EditText.

Добавление обработчика нажатия на кнопку:

// внутри MainActivity::onCreate
findViewById<Button>(R.id.done_button).setOnClickListener {
    addNickname(it)
}

// приватный метод класса MainActivity
private fun addNickname(view: View) {
    val nicknameEditText = findViewById<EditText>(R.id.nickname_edit_text)
    val nicknameTextView = findViewById<TextView>(R.id.nickname_text_view)

    nicknameTextView.text = nicknameEditText.text
    nicknameEditText.visibility = View.GONE // скрывает поле для ввода никнейма
    view.visibility = View.GONE // скрываем кнопку "Done"
    nicknameTextView.visibility = View.VISIBLE // отображаем поле с введенным никнеймом

    // Скрыть клавиатуру после нажатия на кнопку
    val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
    imm.hideSoftInputFromWindow(view.windowToken, 0)
}

Data Binding

Можно заметить, что для получения каждого визуального элемента на макете необходим вызов findViewById(), и когда элементов становится очень много, а это неминуемо при разработке большого приложения, код становится многословным и однообразным.

Data Binding поможет организовать работу с View так, чтобы не пришлось писать множество методов findViewById(). По сути Data Binding — это еще один способ доступа к View-элементам наряду с вызовом findViewById().

Сперва необходимо включить возможность использования Data Binding. Добавляем в Gradle-файл модуля app в блок android:

buildFeatures {
    dataBinding true
}

Затем, необходимо добавить тег <layout></layout> в файл activity_main.xml и поместить внутрь него имеющийся макет LinearLayour :

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <!-- LinearLayout code -->
</layout>

Далее переключаемся в MainActivity и создаем поле binding, которое будет объектом доступа к элементам макета activity_main.

private lateinit var binding: ActivityMainBinding

Класс ActivityMainBinding сгенерирован автоматически. Имя класса берется из имени layout-файла (т.е. activity_main) с добавлением слова "Binding". ActivityMainBinding знает все о содержимом макета: какие View-элементы там есть, каких они типов, и как можно с ними взаимодействовать.

Ключевое слово lateinit помечает поле класса как "будет проинициализировано позже". По-умолчанию Kotlin требует инициализировать поля либо при их объявлении, либо в конструкторе класса. В данном же случае поле binding будет проинициализировано в методе onCreate(). Именно в этом методе в Andorid принято инициализировать поля (либо при их объявлении). Поэтому здесь ключевое слово lateinit необходимо. В противном случае компилятор выдаст ошибку.

Для возможности использования Data Binding на уровне класса MainActivity необходимо проинициализировать поле binding и указать для него макет, данные из которого необходимо использовать:

binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

Описанную выше строку необходимо добавить вместо вызова setContentView(R.layout.activity_main). Метод DataBindingUtil.setContentView() внутри себя выполнит вызов стандартного setContentView() для активности, а также настроит и вернет объект ActivityMainBinding.

Теперь можно заменить вызов findViewById<Button>(R.id.done_button) для получения экземпляра кнопки на binding.doneButton:

binding.doneButton.setOnClickListener {
    addNickname(it)
}

По сути объект Data Binding предоставляет доступ к объектам интерфейса непосредственно по их идентификаторам.
Также можно обновить код метода addNickname() с использованием поля binding:

private fun addNickname(view: View) {
    binding.apply {
        nicknameTextView.text = nicknameEditText.text
        invalidateAll()
        nicknameEditText.visibility = View.GONE
        doneButton.visibility = View.GONE
        nicknameText.visibility = View.VISIBLE
    }
    val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
    imm.hideSoftInputFromWindow(view.windowToken, 0)
}

Вызов метода apply позволяет описать целый блок кода, внутри которого можно не писать каждый раз обращение к полю binding.
Вызов метода binding.invalidateAll() выполняет обновление вида элементов интерфейса на макете в соответствии с измененными данными. Его рекомендуется вызывать после изменения каких-либо данных на макете.

Схема объявления и использования переменной Data Binding в рамках XML-кода:

Кроме доступа к элементам интерфейса внутри классов есть возможность получить доступ к данным на уровне XML-кода. Допустим, приложение хранит данные в отдельном классе UserName:

data class UserName (
    var name: String = "",
    var nickname: String = "")

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

1. Добавить тег <data> с описанием переменной класса в XML-файл макета:

<data>
    <variable
        name="userName"
        type="com.example.android.aboutme.UserName" />
</data>

Данный код объявляет переменную userName типа UserName на уровне макета. Имя класса пишется полностью вместе с именем пакета.

2. Заменить использование ссылки на ресурс @string/name на ссылку на переменную userName:

android:text="@={userName.name}"

Теперь компонент TextView, отображающий имя из строкового ресурса, будет отображать значение поля name объекта userName.

Аналогично нужно сделать и для текстового поля с никнеймом.

3. Создать данные для отображения на макете с помощью Data Binding:

Для того, чтобы переменная userName имела какие-нибудь данные, необходимо, во-первых, создать экземпляр класса UserName.

// Поле класса MainActivity
private val userNameFromActivity: UserName = UserName("Steve Jobs")

Во-вторых, необходимо проинициализировать поле userName, получаемое с помощью Data Binding по его имени.

binding.userName = userNameFromActivity

Таким образом данные, содержащиеся в объекте userNameFromActivity (name="Steve Jobs") будут использоваться на уровне XML-кода через переменную userName.

Для редактирования никнейма можно также использовать возможность доступа к переменной userName:

// nicknameTextView.text = nicknameEditText.text
userName?.nickname = nicknameEditText.text.toString()

Схема инициализации и использования переменной Data Binding в рамках Kotlin-кода:

ConstraintLayout

По-умолчанию Android-проект предлагает использовать компонент ConstraintLayout в качестве основного контейнера для элементов интерфейса вместо LinearLayout, описанного выше. Компонент ConstraintLayout позволяет "привязывать" элементы внутри себя друг к другу. Это позволяет решить проблему сложных интерфейсов, когда приходится размещать множество вложенных линейных макетов LinearLayout. А также проблему связанную с огромным количеством различных размеров экранов, разрешений и отношений сторон экранов. Если привязать элементы друг к другу с помощью ConstraintLayout они всегда на всех экранах будут отображаться корректно, без наползания одних элементов на другие и без больших пробелов между элементами.

Следующий пример продемонстрирует процесс создания и размещения элементов сложным образом внутри ConstraintLayout.

Перед началом дальнейших действий необходимо создать новое приложение "Color Views".

Макет activity_main.xml содержит ConstraintLayout и одно текстовое поле TextView внутри. Если переключиться на редактор макетов и кликнуть на TextView на макете, то можно заметить зигзагообразные линии (пружинки) от элемента к краям экрана — это линии привязки элемента к краям. Они означают, что этот TextView всегда будет располагаться ровно в центре от краев экрана. Элемент можно перемещать по экрану и тогда его координаты будут меняться, это можно наблюдать справа на панели Attributes.

Если удалить одну из привязок к краю, то элемент будет немедленно перемещен к противоположному краю, где привязка осталась. Например, если удалить привязку к верхнему краю, то TextView разместится по нижнему краю.

Чтобы добавить линию привязки, необходимо кликнуть на кружок на элементе TextView и протянуть линию до края макета.

1. Добавление "Box One"

Для реализации приложения "Color Views" сперва подготовим элемент "Box One" — красное текстовое поле TextView сверху экрана. Для этого необходимо:

  1. Удалить нижнюю линию привязки.
  2. Растянуть элемент от левого края макета до правого, установкой атрибута layout_width=match_constraints. Это позволит элементу всегда заполнять все пространство между точками привязки.
  3. Установить отступы от края экрана равными 16dp, чтобы интерфейс выглядел аккуратно.
  4. Создать и установить для элемента стиль "WhiteBox" со следующим содержимым, указанным ниже.
  5. Задать идентификатор для элемента box_one_text.
  6. Задать красный цвет фона: background=@android:color/holo_red_dark.
  7. Задать текст "Box One", выделив его в отдельную строку в ресурсах.

Стиль "WhiteBox":

<style name="WhiteBox">
    <item name="android:background">@android:color/white</item>
    <item name="android:textAlignment">center</item>
    <item name="android:textSize">24sp</item>
    <item name="android:textStyle">bold</item>
    <item name="android:textColor">@android:color/white</item>
    <item name="android:fontFamily">sans-serif</item>
</style>

2. Добавление "Box Two"

Для добавления второго текстового поля необходимо перетащить элемент TextView на макет.
Это поле будет квадратным и со строго заданным размером сторон 130dp.

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

  1. Применить стиль "White Box".
  2. Установить отступы от края макета и от первого поля в 16dp.
  3. Разместить текст по центру поля: gravity=center.
  4. Задать идентификатор для элемента box_two_text.
  5. Задать серый цвет фона: background=@android:color/darker_gray.
  6. Задать текст "Box Two", выделив его в отдельную строку в ресурсах.

3. Добавление "Box Three", "Box Four" и "Box Five"

Для добавления третьего, четвертого и пятого полей необходимо сперва также перенести их из панели Palette на макет.

Далее необходимо задать им идентификаторы: box_three_text, box_four_text и box_five_text. И задать текст для каждого поля: "Box Three", "Box Four" и "Box Five", соответственно. Применить стиль "WhiteBox" и задать синий, зеленый и черный цвета.

Остается задача размещения полей справа от второго поля с заполнением пространства по горизонтали и одинаковыми интервалами между полями.

Для того, чтобы настроить привязки одновременно для группы элементов (трех новых), их необходимо выделить и связать в цепочку (chain), нажав на правую кнопку мыши и выбрав Chains -> Create Vertical Chain. Таким образом три поля будут объединены в вертикальную цепочку и будут связаны друг с другом.

Далее необходимо связать верхний край третьего поля (верхнего поля) с верхним краем квадратного поля. А нижний край пятого поля (нижнего поля) — с нижним краем квадратного. Таким образом все три поля будут размещены в пределах высоты второго поля. Левый край трех полей следует привязать к правому краю квадратного поля и добавить отступ от края в 16dp. Правый край полей нужно привязать к правому краю экрана и также добавить отступ в 16dp. Таким образом поля будут растянуты от второго поля до края экрана с соблюдением отступов.

Стоит заметить, что на текущий момент поля 3, 4 и 5 не имеют интервалов между друг другом. Для добавления интервалов необходимо "поиграться" с отступами от краев полей.
Так поля "Box Three" и "Box Five" не должны иметь отступов сверху и снизу.
А поле "Box Four" должно иметь отступы сверху и снизу по 16dp.

4. Добавление полей "Box Six" и "Box Seven"

В качестве простого упражнения, предлагается добавить поля "Box Six" и "Box Seven" самостоятельно в соответствии с изображением выше.

Ratio, Chains, Baseline

Рекомендуется ознакомиться с возможностью установки соотношения сторон графических элементов (ratio), разновидностей цепочек связи (chains) и установки базовой линии (baseline).

Документация на ConstraintLayout: https://developer.android.com/reference/android/support/constraint/ConstraintLayout.
Статья о разработке UI с помощью ConstraintLayout: https://developer.android.com/training/constraint-layout/.

Интернационализация

Интернационализация используется для адаптации приложения в соответствии с различиями в языках. Интернационализация — перевод строковых ресурсов на различные языки.

Для добавления переводов необходимо добавит дополнительные файлы ресурсов strings.xml в каталоги с именами типа values-{locale}. Например, для добавления переводов строк на русский язык, необходимо создать файл res/values-ru/strings.xml. Для французского потребуется файл res/values-fr/strings.xml. Т.е. для каждого нового языка, требуется создавать отдельный каталог values с отдельным файлом со строками.

В каждом файле strings.xml должно содержаться одинаковое количество строк с одинаковыми именами, по которым их можно идентифицировать на уровне кода. Содержимое строк как раз должно отличаться для каждого из языков.

Например, оригинальные строки на английском языке располагаются в файле res/values/strings.xml:

<string name="app_name">Locale Application</string>
<string name="hello_world">Hello world!</string>

Соответствующий файл для русского языка res/values-ru/strings.xml:

<string name="app_name">Локализованное приложение</string>
<string name="hello_world">Здравствуй, Мир!</string>

В коде приложения используются только ссылки на строки по их именам, например, R.strings.app_name, и нет необходимости проверять программно текущий язык в приложении, система Android сделает это автоматически.