Создание пользовательских компонентов представления

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

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

Неполный список доступных виджетов включает Button , TextView , EditText , ListView , CheckBox , RadioButton , Gallery , Spinner и более специализированные AutoCompleteTextView , ImageSwitcher и TextSwitcher .

Среди доступных макетов — LinearLayout , FrameLayout , RelativeLayout и другие. Дополнительные примеры см. в разделе Общие макеты .

Если ни один из готовых виджетов или макетов не соответствует вашим потребностям, вы можете создать свой собственный подкласс View . Если вам нужно внести лишь небольшие изменения в существующий виджет или макет, вы можете создать подкласс виджета или макета и переопределить его методы.

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

  • Вы можете создать полностью настраиваемый тип View — например, ручку «регулятор громкости», визуализированную с использованием 2D-графики, которая напоминает аналоговый электронный элемент управления.
  • Вы можете объединить группу компонентов View в новый компонент, например, чтобы создать что-то вроде поля со списком (комбинация всплывающего списка и текстового поля для свободного ввода), двухпанельного элемента управления селектором (левая и правая панели со списком). в каждом из которых можно переназначить, какой элемент в каком списке находится) и так далее.
  • Вы можете переопределить способ отображения компонента EditText на экране. Пример приложения «Блокнот» эффективно использует это для создания разлинованной страницы блокнота.
  • Вы можете захватывать другие события, например нажатия клавиш, и обрабатывать их по-своему, например, для игры.

В следующих разделах объясняется, как создавать собственные представления и использовать их в своем приложении. Подробную справочную информацию см. в классе View .

Основной подход

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

  1. Расширьте существующий класс или подкласс View своим собственным классом.
  2. Переопределить некоторые методы суперкласса. Переопределяемые методы суперкласса начинаются с on — например, onDraw() , onMeasure() и onKeyDown() . Это похоже на события on в Activity или ListActivity , которые вы переопределяете для жизненного цикла и других функциональных перехватчиков.
  3. Используйте новый класс расширения. После завершения вы можете использовать новый класс расширения вместо представления, на котором он был основан.

Полностью индивидуальные компоненты

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

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

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

  • Самым универсальным представлением, которое вы можете расширить, является View , поэтому обычно вы начинаете с его расширения для создания нового суперкомпонента.
  • Вы можете предоставить конструктор, который может брать атрибуты и параметры из XML, а также использовать свои собственные такие атрибуты и параметры, такие как цвет и диапазон волюметра или ширину и демпфирование стрелки.
  • Вероятно, вы захотите создать свои собственные прослушиватели событий, средства доступа к свойствам и модификаторы, а также более сложное поведение в своем классе компонента.
  • Вы почти наверняка захотите переопределить onMeasure() , а также, вероятно, потребуется переопределить onDraw() если вы хотите, чтобы компонент что-то показывал. Хотя оба имеют поведение по умолчанию, onDraw() по умолчанию ничего не делает, а onMeasure() по умолчанию всегда устанавливает размер 100x100, что вам, вероятно, не нужно.
  • При необходимости вы также можете переопределить другие методы on .

Расширение onDraw() и onMeasure()

Метод onDraw() предоставляет Canvas , на котором вы можете реализовать все, что захотите: 2D-графику, другие стандартные или пользовательские компоненты, стилизованный текст или все, что вы только можете придумать.

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

На высоком уровне реализация onMeasure() выглядит примерно так:

  • Переопределенный метод onMeasure() вызывается со спецификациями ширины и высоты, которые рассматриваются как требования для ограничений на производимые вами измерения ширины и высоты. Параметры widthMeasureSpec и heightMeasureSpec представляют собой целочисленные коды, представляющие размеры. Полную ссылку на ограничения, которые могут потребоваться для этих спецификаций, можно найти в справочной документации в разделе View.onMeasure(int, int) Эта справочная документация также объясняет всю операцию измерения.
  • Метод onMeasure() вашего компонента вычисляет ширину и высоту измерения, необходимые для визуализации компонента. Он должен стараться оставаться в рамках введенных спецификаций, хотя может и превосходить их. В этом случае родитель может выбрать, что делать, включая обрезку, прокрутку, создание исключения или запросить onMeasure() повторить попытку, возможно, с другими спецификациями измерения.
  • После расчета ширины и высоты вызовите метод setMeasuredDimension(int width, int height) с вычисленными измерениями. Если этого не сделать, возникает исключение.

Вот краткий обзор других стандартных методов, которые платформа вызывает в представлениях:

Категория Методы Описание
Создание Конструкторы Существует форма конструктора, которая вызывается, когда представление создается из кода, и форма, которая вызывается, когда представление раздувается из файла макета. Вторая форма анализирует и применяет атрибуты, определенные в файле макета.
onFinishInflate() Вызывается после того, как представление и все его дочерние элементы создаются из XML.
Макет onMeasure(int, int) Вызывается для определения требований к размеру этого представления и всех его дочерних элементов.
onLayout(boolean, int, int, int, int) Вызывается, когда это представление должно назначить размер и положение всем своим дочерним элементам.
onSizeChanged(int, int, int, int) Вызывается при изменении размера этого представления.
Рисунок onDraw(Canvas) Вызывается, когда представление должно отображать свое содержимое.
Обработка событий onKeyDown(int, KeyEvent) Вызывается, когда происходит событие нажатия клавиши.
onKeyUp(int, KeyEvent) Вызывается, когда происходит событие нажатия клавиши.
onTrackballEvent(MotionEvent) Вызывается при возникновении события движения трекбола.
onTouchEvent(MotionEvent) Вызывается при возникновении события движения сенсорного экрана.
Фокус onFocusChanged(boolean, int, Rect) Вызывается, когда представление получает или теряет фокус.
onWindowFocusChanged(boolean) Вызывается, когда окно, содержащее представление, получает или теряет фокус.
Прикрепление onAttachedToWindow() Вызывается, когда представление прикрепляется к окну.
onDetachedFromWindow() Вызывается, когда представление отделяется от окна.
onWindowVisibilityChanged(int) Вызывается, когда изменяется видимость окна, содержащего представление.

Сложные элементы управления

Если вы не хотите создавать полностью настраиваемый компонент, а вместо этого хотите собрать компонент многократного использования, состоящий из группы существующих элементов управления, то лучше всего создать составной компонент (или составной элемент управления). Таким образом, это объединяет ряд более атомарных элементов управления или представлений в логическую группу элементов, которые можно рассматривать как единое целое. Например, поле со списком может представлять собой комбинацию однострочного поля EditText и соседней кнопки с прикрепленным всплывающим списком. Если пользователь нажимает кнопку и выбирает что-то из списка, оно заполняет поле EditText , но он также может ввести что-то непосредственно в EditText , если захочет.

В Android для этого доступны два других представления: Spinner и AutoCompleteTextView . Тем не менее, эта концепция поля со списком является хорошим примером.

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

  • Как и в случае с Activity , используйте либо декларативный (на основе XML) подход для создания содержащихся в нем компонентов, либо программно встраивайте их из вашего кода. Обычно отправной точкой является какой-либо Layout , поэтому создайте класс, расширяющий Layout . В случае поля со списком вы можете использовать LinearLayout с горизонтальной ориентацией. Вы можете вкладывать внутрь другие макеты, поэтому составной компонент может быть сколь угодно сложным и структурированным.
  • В конструкторе нового класса возьмите любые параметры, ожидаемые суперклассом, и сначала передайте их конструктору суперкласса. Затем вы можете настроить другие представления для использования в новом компоненте. Здесь вы создаете поле EditText и всплывающий список. Вы можете ввести в XML свои собственные атрибуты и параметры, которые ваш конструктор сможет извлечь и использовать.
  • При необходимости создайте прослушиватели событий, которые могут генерировать ваши автономные представления. Примером является метод прослушивателя для прослушивателя щелчков по элементу списка, который обновляет содержимое EditText , если сделан выбор списка.
  • При желании создайте свои собственные свойства с аксессорами и модификаторами. Например, пусть значение EditText будет изначально установлено в компоненте и при необходимости будет запрашиваться его содержимое.
  • При желании переопределите onDraw() и onMeasure() . Обычно в этом нет необходимости при расширении Layout , поскольку макет имеет поведение по умолчанию, которое, скорее всего, работает нормально.
  • При необходимости переопределите другие методы on , такие как onKeyDown() , например, чтобы выбрать определенные значения по умолчанию из всплывающего списка поля со списком при нажатии определенной клавиши.

Использование Layout в качестве основы для пользовательского элемента управления имеет преимущества, в том числе следующие:

  • Вы можете указать макет с помощью декларативных файлов XML, как и в случае с экраном активности, или вы можете создавать представления программно и вкладывать их в макет из своего кода.
  • Методы onDraw() и onMeasure() , а также большинство других методов on имеют подходящее поведение, поэтому вам не придется их переопределять.
  • Вы можете быстро создавать составные представления произвольной сложности и повторно использовать их, как если бы они были одним компонентом.

Изменение существующего типа представления

Если есть компонент, похожий на тот, который вам нужен, вы можете расширить этот компонент и переопределить поведение, которое хотите изменить. Вы можете делать все, что делаете, с полностью настраиваемым компонентом, но, начав с более специализированного класса в иерархии View , вы можете бесплатно получить поведение, которое будет делать то, что вы хотите.

Например, пример приложения NotePad демонстрирует многие аспекты использования платформы Android. Среди них — расширение представления EditText для создания блокнота с линиями. Это не идеальный пример, и API для этого могут измениться, но он демонстрирует принципы.

Если вы еще этого не сделали, импортируйте образец NotePad в Android Studio или просмотрите исходный код по предоставленной ссылке. В частности, см. определение LinedEditText в файле NoteEditor.java .

Вот некоторые вещи, которые следует отметить в этом файле:

  1. Определение

    Класс определяется следующей строкой:
    public static class LinedEditText extends EditText

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

    Кроме того, LinedEditText является static , то есть он не генерирует так называемые «синтетические методы», которые позволяют ему получать доступ к данным из родительского класса. Это означает, что он ведет себя как отдельный класс, а не как что-то тесно связанное с NoteEditor . Это более чистый способ создания внутренних классов, если им не нужен доступ к состоянию из внешнего класса. Он сохраняет сгенерированный класс небольшим и позволяет легко использовать его из других классов.

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

  2. Инициализация класса

    Как всегда, первым вызывается супер. Это не конструктор по умолчанию, а параметризованный. EditText создается с этими параметрами, когда он создается из файла макета XML. Таким образом, конструктору необходимо взять их и передать конструктору суперкласса.

  3. Переопределенные методы

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

    В этом примере переопределение метода onDraw() позволяет рисовать синие линии на холсте представления EditText . Холст передается в переопределенный метод onDraw() . Метод super.onDraw() вызывается до завершения метода. Должен быть вызван метод суперкласса. В этом случае вызовите его в конце после того, как нарисуете строки, которые хотите включить.

  4. Пользовательский компонент

    Теперь у вас есть собственный компонент, но как его использовать? В примере с Блокнотом пользовательский компонент используется непосредственно из декларативного макета, поэтому посмотрите note_editor.xml в папке res/layout :

    <view xmlns:android="http://schemas.android.com/apk/res/android"
        class="com.example.android.notepad.NoteEditor$LinedEditText"
        android:id="@+id/note"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent"
        android:padding="5dp"
        android:scrollbars="vertical"
        android:fadingEdge="vertical"
        android:gravity="top"
        android:textSize="22sp"
        android:capitalize="sentences"
    />
    

    Пользовательский компонент создается как общее представление в XML, а класс указывается с использованием полного пакета. Ссылка на определяемый вами внутренний класс осуществляется с использованием нотации NoteEditor$LinedEditText , которая является стандартным способом ссылки на внутренние классы в языке программирования Java.

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

    <com.example.android.notepad.LinedEditText
      id="@+id/note"
      ... />
    

    Обратите внимание, что класс LinedEditText теперь является отдельным файлом класса. Если класс вложен в класс NoteEditor , этот метод не работает.

    Другие атрибуты и параметры в определении — это те, которые передаются в конструктор пользовательского компонента, а затем передаются в конструктор EditText , поэтому это те же параметры, которые вы используете для представления EditText . Также возможно добавить свои параметры.

Создание пользовательских компонентов настолько сложно, насколько это необходимо.

Более сложный компонент может еще больше on методы и ввести свои собственные вспомогательные методы, существенно настроив свои свойства и поведение. Единственным ограничением является ваше воображение и то, что вам нужно от компонента.