Домой Android разработкаUI и UX. Графика в Android Android Custom View как комбинация стандартных элементов

Android Custom View как комбинация стандартных элементов

by dilix
Custom View Group из готовых компонент

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

Можно выделить три основных подхода к созданию Custom View в Android приложении

  1. Компоновка готовых элементов для создания инкапсулированного блока UI. По-честному это Custom ViewGroup на основе уже доступных Layoutов. На практике самый частый кейс. Все равно, что выносим блок кода в метод, с той лишь разницей, что выделяем и часть UI.
  2. Создание View с нуля и произвольная отрисовка элементов. Тогда вам нужно будет переопределить onMeasure(), onLayout(), onDraw() и т.д. Реализовать обработку нажатий, сохранение состояния и еще много чего интересного. Второй по популярности случай. Подходит, когда нужно сделать собственный элемент интерфейса, например график.
  3. Реализация собственной Custom ViewGroup, которая отвечает за расположения дочерних элементов внутри себя. Последний по популярности, но довольно кропотливый в реализации вариант. Linear, Constraint, Frame и другие Layoutы позволяют реализовать любой интерфейс. Прибегнуть к реализации Custom ViewGroup можно ради улучшения производительности или при сложном взаимодействии элементов внутри группы.

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

Создание Custom ViewGroup на базе готовых компонент

В данном примере можно все элементы разместить на одном экране и всю синхронизацию сделать на уровне Fragment. А можно вынести блок поиска в отдельную Custom ViewGroup и оставить только интерфейс взаимодействия с «внешним миром». Это значит, что пользователю этого компонента все равно как он ведет себя внутри. Есть внешний контракт, что поиск можно открыть, закрыть, он может сообщить об изменениях внутри себя.

Плюсы такого подхода:

  • Реализация компонента не зависит от внешней среды. Следовательно весь экран и непосредственно поиск могу делать два разных программиста.
  • Готовую View можно будет переиспользовать при необходимости.
  • Класс общего Fragment будет содержать меньше логики и его будет легче читать. SOLID в действии.
  • Компонент поисковой строки можно будет протестировать в отрыве от его использования.

Верстка нестандартного компонента поиска

Любой экран можно сверстать по-разному. От конкретной реализации может зависеть производительность приложения и общий look & feel. Общий принцип любой верстки: чем меньше вложенность — тем лучше.

Поисковая строка из примера — простой FrameLayout, с 1 кнопкой, 1 полем ввода. На самом деле там внутри еще список для реализации Autocomplete.

Не оптимальный способ верстки Custom ViewGroup

По описанию примера код может выглядеть так

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.appcompat.widget.AppCompatEditText
        android:id="@+id/etSearch"
        android:layout_width="match_parent"
        android:layout_height="@dimen/search_bar_height"
        android:background="@drawable/circle_search_text_bg"
        android:gravity="center_vertical"
        android:hint="@string/search_hint"
        android:lines="1"
        android:paddingStart="60dp"
        android:paddingEnd="8dp"
        android:singleLine="true"
        android:textColor="@color/lightGrey"
        android:textColorHint="@color/lightGrey" />

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/imgSearch"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:background="@drawable/circle_search_icon"
        android:scaleType="center"
        app:srcCompat="@drawable/ic_search_gray_24dp" />

</FrameLayout>

Если посмотреть в Design Preview, то кажется что все ок.

Превью для Custom ViewGroup в Android Studio

Обратим внимание на то, как инициализируется наша ViewGroup

internal class SearchExpandedView : FrameLayout {
    ...
    constructor(context: Context) : super(context) {
        init(context)
    }
    ...
    private fun init(context: Context) {
        _view = View.inflate(context, R.layout.view_search_expanded, this)
    }
    ...
}

Все бы ничего, но давайте подебажим и посмотрим Layout Inspector в Android studio.


Лишний FrameLayout в верстке

чем меньше вложенность — тем лучше

Merge как способ оптимизации верстки

Избавиться от одного лишнего FrameLayout поможет  тэг <merge>. Он позволяет вставить целиком верстку внутри себя в родительский элемент. То-есть, в тот самый SearchExpandedView, который наследуется от FrameLayout.

<?xml version="1.0" encoding="utf-8"?>
<merge 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">
    ...
</merge>
Preview для <merge> в Android studio

Упс, используя <merge> можно обнаружить, что в Design Preview Android Studio наш элемент выглядит не всегда так, как мы его себе представляем. Особенно, если базовая ViewGroup не FrameLayout (а например, Linear).

Исправить эту проблемы призван тэг tools:parentTag="...". ParentTag говорит о том, какой мы ждем внешний Layout. В случае с примером поиска, preview может быть корректный и без него, потому что по сути никаких внешних зависимостей на расположение элементов нет.

<?xml version="1.0" encoding="utf-8"?>
<merge 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:parentTag="android.widget.FrameLayout">

    <androidx.appcompat.widget.AppCompatEditText
        android:id="@+id/etSearch"
        android:layout_width="match_parent"
        android:layout_height="@dimen/search_bar_height"
        android:background="@drawable/circle_search_text_bg"
        android:fontFamily="@font/raleway_light"
        android:gravity="center_vertical"
        android:hint="@string/search_hint"
        android:lines="1"
        android:paddingStart="60dp"
        android:paddingEnd="8dp"
        android:singleLine="true"
        android:textColor="@color/lightGrey"
        android:textColorHint="@color/lightGrey" />

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/imgSearch"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:background="@drawable/circle_search_icon"
        android:scaleType="center"
        app:srcCompat="@drawable/ic_search_gray_24dp" />

</merge>

Реализация компонента поиска

Верстка элемента готова, перейдем к коду.

Интерфейс SearchExpandedView

Стоит заранее продумать внешний интерфейс к вашему элементу. То-есть то определенное поведение, которое вы закладываете в Custom ViewGroup.

В моем случае это

var isExpanded = false
    set(value) {
        if (value == field) return
        field = value
        setExpandedInner(value)
    }

И то, о чем наша вьюха может сообщать

    private class Listener {
        var onExpandedChanged: ((Boolean) -> Unit)? = null
        var onSearchText: ((String) -> Unit)? = null
        var onAtmSelected: ((Atm) -> Unit)? = null
        var onTextSearch: ((String) -> Unit)? = null
    }

Примеры выше на Котлине, а сделано через лямбды, чтобы можно было бы использовать так

fun onExpandedChanged(onExpandedChanged: (Boolean) -> Unit) {
        _listener.onExpandedChanged = onExpandedChanged
    }
searchView.onExpandedChanged {
    if (it) {
        ...    
    } else {
        ...
    }
}

Инициализация SearchExpandedView

internal class SearchExpandedView : FrameLayout {
    ...
    constructor(context: Context) : super(context) {
        init(context)
    }
    ...
    private fun init(context: Context) {
        _view = View.inflate(context, R.layout.view_search_expanded, this)
    }
    ...
}

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

Stay tuned! А пока, можете почитать про анимированные переходы в Android или про инструменты, полезные разработчику.

Хочешь обсудить Android разработку?
Заходи к нам Вконтакте, на Facebook и в Телеграм!

Добавить комментарий

1 комментарий

Александр Антонов 18.06.2022 - 17:55

Так и не понял как вынести набор базовых view в группу для переиспользования, примеров маловато а гугл почему отчаянно не хочет мне раскрывать эту тему

Ответить

Может быть интересно

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

Политика конфиденциальности и Cookies
%d