В этом уроке мы научимся создавать собственные 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;