В этом уроке мы научимся создавать собственные View
.
Обычно термин Custom View
обозначает View
, которого нет в sdk Android. Или другими словами - это View
которое мы сделали сами.
Когда может понадобиться реализация собственного View
:
Как правило, создание custom view можно избежать используя темы, различные параметры View
, а иногда и лисенеры. Но, если все таки вам действительно нужно сделать что-то особенное, давайте разберемся как же это сделать.
Для начала, давайте вспомним о том, как выглядит иерархия базовых компонентов:
Все ui компоненты наследуются от View
, а лейауты от ViewGroup
. В свою очередь ViewGroup наследуется от View
.
Прежде чем наследоваться от базового класса View
посмотрите, может быть вам ближе функциональность уже какого-то существующего элемента. Например Button
, это не написанный с нуля компонент, а наследник TextView
.
Первостепенно давайте разберемся с жизненным циклом View
.
Каждый элемент начинает свое существование с конструктора. У View
их целых четыре штуки:
Создание View из кода:
public CustomView(Context context)
Создание View из XML:
public CustomView(Context context, @Nullable AttributeSet attrs);
Создание View из XML со стилем из темы:
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr);
Создание View из XML со стилем из темы и/или с ресурсом стиля:
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)
Последний конструктор добавлен в sdk версии 21. Каждый из конструкторов каскадно вызывает следующий.
Для создания View из котлина, следует использовать следующий конструктор:
class CustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : View(context, attrs, defStyleAttr)
После того как родитель View
вызовет метод addView(View)
, наш View
будет прикреплён к окну. На этой стадии наш View-компонент попадает в иерархию родителя.
Этот метод означает, что наш View
находится на стадии определения собственного размера. Для того что бы понять как распределить элементы на экране и сколько они занимают место нужно получить от каждого View
его размер. В методе measure
как раз и происходят расчеты.
Давайте посмотрим на сам метод:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
Метод onMeasure()
принимает 2 аргумента: widthMeasureSpec
и heightMeasureSpec
. Это значения, которые содержат в себе информацию о том, каким размером хочет видеть ваше View
родительский элемент.
Каждое из значений на самом деле содержит 2 параметра:
int spec = mode | size
mode
. Указывает на то, какие правила применяются ко второму параметру size;size
. Непосредственно размер View
.Получить эти параметры можно при помощи методов класса MeasureSpec
:
MeasureSpec.getMode(widthMeasureSpec)
MeasureSpec.getSize(widthMeasureSpec)
mode может принимать следующие значения:
MeasureSpec.EXACTLY
. Означает, что размер задан жёстко. Независимо от размера вашего View
, вы должны установить определённую ширину и высоту;MeasureSpec.AT_MOST
. Означает что View
может быть любого размера, которого пожелает, но, не больше чем размер родителя. Это значение match_parent
;MeasureSpec.UNSPECIFIED
. Означение что View
может само решить какой размер ему нужен не взирая ни на какие ограничения. Это значение wrap_content
.В коде это можно описать следующим образом:
int width;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(wrapWidth, widthSize);
} else {
width = wrapWidth;
}
где wrapWidth
, это наша желаемая ширина. Аналогичный подход применяется и к высоте View
.
Конечно же не нужно каждый раз писать эту конструкцию из условий. Для упрощения работы у View есть метод
public static int resolveSize(int size, int measureSpec)
который уже включает в себя все необходимые условия.
После того как мы выполнили все расчеты, необходимо установить рассчитанные размеры при помощи метода:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)
Расчет размера можно разделить на 4 стадии:
View
хочет быть, определение LayoutParams
наследника. Это может быть сделано как через xml, так и кодом:android:layout_width="match_parent"
android:layout_height="wrap_content"
LinearLayout.LayoutParams
View
и просит рассчитать их размеры.Этот метод позволяет присваивать позицию и размер дочерним элементам ViewGroup
. В случае, если мы наследовались от View
, нам не нужно переопределять этот метод.
Это основной метод при разработки собственной View
. В onDraw
вы можете рисовать все что вам нужно. Метод имеет следующую сигнатуру:
protected void onDraw(Canvas canvas)
На полученном Canvas
вам требуется непосредственно изобразить саму View
. Рисование на Canvas
происходит при мощи объекта Paint
. Paint
отвечает за то, как именно будет отрисован контент вашего View
и имеет множество параметров.
Стоит обратить внимание, что onDraw
вызывается не один раз и может занимать много времени. Поэтому стоит максимально аккуратно работать с отрисовкой, не аллоцировать никаких объектов и не делать лишних операций.
Из диаграммы жизненного цикла видно, что существуют два метода, которые заставляют View
перерисовываться:
invalidate()
. Используется когда нужно только перерисовать ваш элемент. Когда изменился цвет или текст или нужно сделать какие-то еще визуальные изменения;
requestLayout()
. Используется когда нужно изменить размеры вашего View
. Вызов requestLayout
не только заставит View
заново измериться, но и перерисует элемент.
Вызовы всех методов View
проходят от базового View
к потомкам, сверху вниз.
Во время расчета размера View
потомок принимает “пожелания” от родителя, рассчитывает свои размеры, а также размеры своих потомков. (Measure pass)
После того как размеры известны, родитель проставляет размеры и расположение своим потомкам. (Layout pass)
Последним этапом является отрисовка. Она также происходит от родителя к потомку
View
, воспользуйтесь ей;View
, это поможет избежать множества проблем;invalidate
только когда это действительно нужно;dp
и sp
;resolveSize
;