Освой Android играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

TextInputLayout

Создание выпадающего меню

Макет TextInputLayout сначала появился в библиотеке Android Design Support Library и добавляет немного красоты к текстовому полю. Когда пользователь начинает вводить текст в текстовом поле, то подсказка, заданная в этом компоненте, всплывает над ним в специальном TextView. Пример можно увидеть на видео.

Библиотека больше не развивается, поэтому используйте AndroidX, которую я и буду теперь использовать в описании.

Компонент можно найти на панели инструментов в разделе Text.

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


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".Main2Activity">

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/textInputLayout"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <com.google.android.material.textfield.TextInputEditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="hint" />
    </com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

Так выглядел компонент в составе Support Library.

Подсказку не обязательно указывать в атрибуте android:hint у текстового поля. Можно программно присвоить через метод:


textInputLayout.setHint(getString(R.string.hint));

Стилизация

В AndroidX у компонента появились новые стили: OutlinedBox, FilledBox и другие. Можете самостоятельно попробовать пример с двумя стилями и посмотреть на отличия.


<com.google.android.material.textfield.TextInputLayout
    style="@style/Widget.MaterialComponents.TextInputEditText.OutlinedBox"
    ... />

<com.google.android.material.textfield.TextInputLayout
    style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
    ... />

Общая стилизация доступна следующим образом. Пропишем стили в styles.xml:


<!--Floating label text style-->  
<style name="MyHintText" parent="TextAppearance.AppCompat.Small">  
    <item name="android:textColor">@color/pink</item>
</style>

<!--Input field style-->  
<style name="MyEditText" parent="Theme.AppCompat.Light">  
    <item name="colorControlNormal">@color/indigo</item>
    <item name="colorControlActivated">@color/pink</item>
</style>  

Применим стили


<com.google.android.material.textfield.TextInputLayout  
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:hintTextAppearance="@style/MyHintText">

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/Title"
        android:theme="@style/MyEditText" />

</com.google.android.material.textfield.TextInputLayout>

Обработка ошибки

Предыдущий пример показывал применение подсказки. Но также можно выводить сообщения об ошибке. Здесь потребуется написать немного кода. За вывод ошибки отвечает атрибут app:errorEnabled. Назначим текстовому полю тип клавиатуры и однострочный текст. При наборе текста после нажатия на клавишу Enter проверяем длину текста. Если текст меньше четырёх символов, то выводим сообщение об ошибке.


Добавить пространство имён к корневому элементу
xmlns:app="http://schemas.android.com/apk/res-auto"

<com.google.android.material.textfield.TextInputLayout
    android:id="@+id/textInputLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:errorEnabled="true">

    <EditText
        android:id="@+id/editTextName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:imeOptions="actionGo"
        android:inputType="text"
        android:singleLine="true"/>
</com.google.android.material.textfield.TextInputLayout>

Код


package ru.alexanderklimov.design;

import ...

public class MainActivity extends AppCompatActivity {

    private static final int MIN_TEXT_LENGTH = 4;
    private static final String EMPTY_STRING = "";

    private TextInputLayout mTextInputLayout;
    private EditText mEditText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        mTextInputLayout = findViewById(R.id.textInputLayout);
        mEditText = findViewById(R.id.editTextName);

        mTextInputLayout.setHint(getString(R.string.hint));
        mEditText.setOnEditorActionListener(ActionListener.newInstance(this));
    }

    private boolean shouldShowError() {
        int textLength = mEditText.getText().length();
        return textLength > 0 && textLength < MIN_TEXT_LENGTH;
    }

    private void showError() {
        mTextInputLayout.setError(getString(R.string.error));
    }

    private void hideError() {
        mTextInputLayout.setError(EMPTY_STRING);
    }

    private static final class ActionListener implements TextView.OnEditorActionListener {
        private final WeakReference<MainActivity> mainActivityWeakReference;

        public static ActionListener newInstance(MainActivity mainActivity) {
            WeakReference<MainActivity> mainActivityWeakReference = new WeakReference<>(mainActivity);
            return new ActionListener(mainActivityWeakReference);
        }

        private ActionListener(WeakReference<MainActivity> mainActivityWeakReference) {
            this.mainActivityWeakReference = mainActivityWeakReference;
        }

        @Override
        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
            MainActivity mainActivity = mainActivityWeakReference.get();
            if (mainActivity != null) {
                if (actionId == EditorInfo.IME_ACTION_GO && mainActivity.shouldShowError()) {
                    mainActivity.showError();
                } else {
                    mainActivity.hideError();
                }
            }
            return true;
        }
    }
}

Текст ошибки выводится снизу от текстового поля.

TextInputLayout

Стиль для сообщения об ошибке можно стилизовать. Добавим новый атрибут.


app:errorEnabled="true"
app:errorTextAppearance="@style/ErrorText"

В файле стилей res/values/styles.xml добавим новый стиль:


<style name="ErrorText" parent="TextAppearance.AppCompat.Small">
    <item name="android:textStyle">bold|italic</item>
    <item name="android:textColor">@color/colorPrimary</item>
</style>

Теперь выводится другим цветом.

TextInputLayout

Расширенный вид стилей:


<!--Error label text style-->  
<style name="MyErrorText" parent="TextAppearance.AppCompat.Small">  
    <item name="android:textColor">@color/colorPrimary</item>
</style>

<!--Input field style-->  
<style name="MyEditText" parent="Theme.AppCompat.Light">  
    <item name="colorControlNormal">@color/indigo</item>
    <item name="colorControlActivated">@color/pink</item>
</style>  

Применяем через атрибуты app:errorTextAppearance и android:theme.


<com.google.android.material.textfield.TextInputLayout  
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:errorTextAppearance="@style/MyErrorText"
    app:errorEnabled="true">

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/Title"
        android:theme="@style/MyEditText" />

</com.google.android.material.textfield.TextInputLayout>

Счётчик символов

С помощью атрибутов app:counterEnabled и app:counterMaxLength можно установить счётчик символов с указанием предела, который будет выводиться под текстовым полем.


<com.google.android.material.textfield.TextInputLayout
    ...
    app:counterEnabled="true"
    app:counterMaxLength="140">

    <EditText
        .../>

</com.google.android.material.textfield.TextInputLayout>

Counter TextInputLayout

Когда будет превышен лимит, то цвет счётчика изменится. Этот цвет можно стилизовать через стиль.


<style name="MyCounterText" parent="TextAppearance.AppCompat.Small">
    <item name="android:textColor">@android:color/holo_orange_dark</item>
</style>

Стиль применяется к атрибуту app:counterOverflowTextAppearance:


<com.google.android.material.textfield.TextInputLayout
    ...
    app:counterOverflowTextAppearance="@style/MyCounterText"
    app:counterEnabled="true"
    app:counterMaxLength="10">

Связка TextInputLayout и AutoCompleteTextView позволяет создать выпадающее меню взамен стандартного компонента Spinner. Для этого задействуем один стилей, в котором присутствует ExposedDropdownMenu.

TextInputLayout ExposedDropdownMenu

Разметка экрана.


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.textfield.TextInputLayout
        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <AutoCompleteTextView
            android:id="@+id/autoCompleteTextView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:inputType="none"
            android:text="Барсик" />
    </com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

В AutoCompleteTextView установлен тип android:inputType="none", чтобы у пользователя не было возможности изменять текст. Ведь нам нужно меню, а не текстовое поле.

Для создания элементов меню воспользуемся массивом строк в ресурсах. Добавляем в res/values/strings.xml.


<string-array name="cats">
    <item>Барсик</item>
    <item>Мурзик</item>
    <item>Рыжик</item>
    <item>Васька</item>
</string-array>

Создадим отдельную разметку для элементов меню в файле res/layout/dropdown_item.xml.


<?xml version="1.0" encoding="utf-8"?>
<TextView 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="14dp"
    android:textStyle="bold">
</TextView>

Присоединяем созданный макет к AutoCompleteTextView через адаптер.


override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val autoCompleteTextView: AutoCompleteTextView = findViewById(R.id.autoCompleteTextView)

    val cats = resources.getStringArray((R.array.cats))
    val arrayAdapter = ArrayAdapter(this, R.layout.dropdown_item, cats)

    autoCompleteTextView.setAdapter(arrayAdapter)
}

Посмотрим на результат. Нажимаем на AutoCompleteTextView и получаем выпадающий список из массива строк.

TextInputLayout ExposedDropdownMenu

Мы получили рабочий прототип, но при повороте меню работать не будет. Проверьте самостоятельно. Поэтому немного поправим код.


class MainActivity : AppCompatActivity() {

    private lateinit var autoCompleteTextView: AutoCompleteTextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        autoCompleteTextView = findViewById(R.id.autoCompleteTextView)
    }

    override fun onResume() {
        super.onResume()

        val cats = resources.getStringArray((R.array.cats))
        val arrayAdapter = ArrayAdapter(this, R.layout.dropdown_item, cats)

        autoCompleteTextView.setAdapter(arrayAdapter)
    }
}

Можно добавить к элементам меню значок. Заодно добавим подсказку.


<com.google.android.material.textfield.TextInputLayout
    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:layout_marginEnd="16dp"
    android:hint="Коты"
    app:startIconDrawable="@drawable/ic_pets"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent">

TextInputLayout ExposedDropdownMenu

TextInputEditText

Казалось бы простой компонент, никаких трудностей не возникает. Но не торопитесь. Стоит повернуть устройство в альбомный режим, как текстовое поле растянется на весь экран и никакой подсказки вы не увидите. Возможно, это баг, который когда-нибудь починят. Но проблему легко решить, если вместо стандартного EditText использовать специальный компонент TextInputEditText:


<com.google.android.material.textfield.TextInputLayout
    ...>

    <com.google.android.material.textfield.TextInputEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Введите текст"/>

</com.google.android.material.textfield.TextInputLayout>

Дополнительное чтение

TextInputEditText

Реклама