Custom View это удобный и правильный способ инкапсуляции части UI. Он позволяет вынести интерфейс в отдельный блок и переиспользовать его. Создавать их проще чем кажется на первый взгляд.
Можно выделить три основных подхода к созданию Custom View в Android приложении
- Компоновка готовых элементов для создания инкапсулированного блока UI. По-честному это Custom ViewGroup на основе уже доступных Layoutов. На практике самый частый кейс. Все равно, что выносим блок кода в метод, с той лишь разницей, что выделяем и часть UI.
- Создание View с нуля и произвольная отрисовка элементов. Тогда вам нужно будет переопределить onMeasure(), onLayout(), onDraw() и т.д. Реализовать обработку нажатий, сохранение состояния и еще много чего интересного. Второй по популярности случай. Подходит, когда нужно сделать собственный элемент интерфейса, например график.
- Реализация собственной 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, то кажется что все ок.
Обратим внимание на то, как инициализируется наша 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.
чем меньше вложенность — тем лучше
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 или про инструменты, полезные разработчику.
1 комментарий
Так и не понял как вынести набор базовых view в группу для переиспользования, примеров маловато а гугл почему отчаянно не хочет мне раскрывать эту тему