View & ViewGroup

https://d.android.com/training/custom-views/create-view
https://d.android.com/reference/android/view/View
https://d.android.com/reference/android/view/ViewGroup
https://d.android.com/guide/topics/ui/how-android-draws
https://itsobes.ru/AndroidSobes/kak-sozdat-kastomnuiu-view
https://itsobes.ru/AndroidSobes/kak-realizovat-metod-view-onmeasure
https://itsobes.ru/AndroidSobes/kak-realizovat-metod-view-ondraw
https://itsobes.ru/AndroidSobes/v-chem-raznitsa-mezhdu-invalidate-i-requestlayout
https://itsobes.ru/AndroidSobes/kak-rabotaet-metod-dispatchtouchevent
https://itsobes.ru/AndroidSobes/dlia-chego-nuzhen-metod-onintercepttouchevent
https://itsobes.ru/AndroidSobes/dlia-chego-nuzhen-metod-view-forcelayout
https://itsobes.ru/AndroidSobes/nazovite-osnovnye-motionevent-actions
https://itsobes.ru/AndroidSobes/kak-touch-event-dostavliaetsia-do-target-view

Save State

View сохраняет и восстанавливает свое состояние с помощью методов onSaveInstanceState и onRestoreInstanceState. Чтобы view сохраняло свое состояние при пересоздании activity нужно указать ей id. Если мы создадим 2 EditText с одинаковыми id при повороте экрана восстановится текст из последнего EditText.

onSizeChanged

Вызывается при изменении размеров View (смена ориентация устройства, изменение размера родительского контейнера). В этом методе мы выполняем расчеты и подготавливаем данные для отрисовки в onDraw.

onTouchEvent

Метод обработки событий пользовательского ввода. Вызывается при каждом событии касания View.

View Lifecycle

onAttachToWindow вызывается когда View прикрепляется к окну.

onMeasure вызывается для определения требований к размеру этого View и всех его дочерних элементов.

onLayout вызывается когда View должно назначить размер и позицию всем своим дочерним элементам.

onDraw вызывается когда View должен отрисовать свое содержимое.

onDetachedFromWindow вызывается когда View открепляется от своего окна.

ViewGroup

View которая может содержать дочерние view. Базовый класс для FrameLayout LinearLayout и других.

onInterceptTouchEvent

Позволяет перехватить ивент во ViewGroup и не отправлять его вниз по иерархии в таргет-view.

dispatchTouchEvent

Этот метод помогает доставить объект MotionEvent до View которой он предназначен.

invalidate

Используется для запроса перерисовки экрана. Когда вызывается этот метод, View помечается как "нуждающаяся в обновлении", что приводит к вызову метода onDraw в следующем цикле отрисовки, где обновляется её содержимое. Это позволяет обновить отображение View, например, при изменении её состояния или данных.

class CustomView(context: Context): View(context) {

    private var color: Int = Color.RED

    // Метод для установки нового цвета и запроса перерисовки
    fun setColor(newColor: Int) {
        color = newColor
        invalidate() // Запрашивает перерисовку
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // Отрисовываем круг с заданным цветом
        canvas.drawColor(color)
    }
}

postInvalidate

Используется для запроса перерисовки View из другого потока.

class CustomView(context: Context): View(context) {

    private var color: Int = Color.RED

    // Метод для установки нового цвета и запроса перерисовки из фонового потока
    fun setColorFromBackgroundThread(newColor: Int) {
        color = newColor
        postInvalidate() // Запрашивает перерисовку из любого потока
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // Отрисовываем круг с заданным цветом
        canvas.drawColor(color)
    }
}
requestLayout

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

class CustomView(context: Context): View(context) {

    private var customSize: Int = 100

    // Метод для изменения размера и запроса повторного размещения
    fun setCustomSize(size: Int) {
        if (customSize != size) {
            customSize = size
            requestLayout() // Запрашивает перерасчет и перераспределение размеров
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // Устанавливаем размеры view
        val width = resolveSize(customSize, widthMeasureSpec)
        val height = resolveSize(customSize, heightMeasureSpec)
        setMeasuredDimension(width, height)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // Отрисовка в зависимости от размера
        canvas.drawColor(Color.RED)
    }
}
forceLayout

Используется для принудительного выполнения перерасчета размеров и размещения View. Этот метод часто применяется в случаях, когда необходимо убедиться, что перерасчет и размещение View происходят немедленно, а не в следующем цикле отрисовки.

class CustomView(context: Context): View(context) {

    private var customSize: Int = 100

    // Метод для изменения размера и принудительного выполнения перерасчета
    fun setCustomSize(size: Int) {
        if (customSize != size) {
            customSize = size
            forceLayout() // Принудительно выполняет перерасчет и размещение
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // Устанавливаем размеры view
        val width = resolveSize(customSize, widthMeasureSpec)
        val height = resolveSize(customSize, heightMeasureSpec)
        setMeasuredDimension(width, height)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // Отрисовка в зависимости от размера
        canvas.drawColor(Color.RED)
    }
}
onMeasure

Отвечает за вычисление размера View перед его отображением. Он получает параметры widthMeasureSpec и heightMeasureSpec от родительского контейнера и на основе этих параметров определяет размеры View.

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)

    // Извлекаем режим и размер для ширины
    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
    val widthSize = MeasureSpec.getSize(widthMeasureSpec)
    
    // Извлекаем режим и размер для высоты
    val heightMode = MeasureSpec.getMode(heightMeasureSpec)
    val heightSize = MeasureSpec.getSize(heightMeasureSpec)

    // Пример желаемых размеров
    val desiredWidth = 200
    val desiredHeight = 100

    // Определяем конечные размеры с учетом режимов измерения
    val width = if (widthMode == MeasureSpec.EXACTLY) {
        widthSize
    } else {
        Math.min(desiredWidth, widthSize)
    }

    val height = if (heightMode == MeasureSpec.EXACTLY) {
        heightSize
    } else {
        Math.min(desiredHeight, heightSize)
    }

    // Устанавливаем измеренные размеры
    setMeasuredDimension(width, height)
}

MeasureSpec

Используется для передачи информации о размере и режиме измерения от родительского компонента к дочернему View. Это позволяет дочернему View адаптироваться к доступному пространству.

EXACTLY Родительский компонент предоставляет точный размер для View. View должна использовать этот размер и не изменять его. Этот режим устанавливается, когда родительский компонент определяет фиксированный размер для View, например, в случае LayoutParams с точными значениями. При использовании match_parent или layout_width/layout_height, когда размеры явно заданы.

val widthMode = MeasureSpec.getMode(widthMeasureSpec)
if (widthMode == MeasureSpec.EXACTLY) {
    val widthSize = MeasureSpec.getSize(widthMeasureSpec)
    // Ширина точно задана
}

AT_MOST Родительский компонент предоставляет максимальный размер, который View может занять. View должна быть не больше этого размера. Этот режим применяется, когда View имеет неопределенный размер и может быть ограничена только максимальными размерами, предоставленными родителем. При использовании wrap_content, когда View может занимать до определенного максимума, но не больше него.

val widthMode = MeasureSpec.getMode(widthMeasureSpec)
if (widthMode == MeasureSpec.AT_MOST) {
    val widthSize = MeasureSpec.getSize(widthMeasureSpec)
    // Ширина должна быть не больше максимального значения
}

UNSPECIFIED Родительский компонент не ограничивает размер View. View может быть любого размера. Этот режим применяется, когда родитель не устанавливает конкретные ограничения на размер View. View может занять любое пространство, которое она считает нужным. Когда родительский контейнер позволяет View занять любое пространство без ограничений.

val widthMode = MeasureSpec.getMode(widthMeasureSpec)
if (widthMode == MeasureSpec.UNSPECIFIED) {
    // Размер не ограничен, `View` может быть любым
}

MeasureSpec.getMode

Определяет режим измерения (EXACTLY, AT_MOST, UNSPECIFIED).

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)

    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
    val heightMode = MeasureSpec.getMode(heightMeasureSpec)
}

MeasureSpec.getSize

Получает размер, указанный в measureSpec, который можно использовать для вычислений размеров View.

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)

    val widthSize = MeasureSpec.getSize(widthMeasureSpec)
    val heightSize = MeasureSpec.getSize(heightMeasureSpec)
}

onLayout

Bспользуется для определения расположения и размеров дочерних View внутри ViewGroup. Этот метод вызывается после того, как ViewGroup завершил процесс измерения onMeasure и готов перейти к размещению дочерних элементов.

class CustomLayout(context: Context) : ViewGroup(context) {

    init {
        // Пример добавления дочерних View
        addView(View(context))
        addView(View(context))
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // Определение размеров для этого ViewGroup
        val width = MeasureSpec.getSize(widthMeasureSpec)
        val height = MeasureSpec.getSize(heightMeasureSpec)
        setMeasuredDimension(width, height)

        // Измерение дочерних View
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            child.measure(
                MeasureSpec.makeMeasureSpec(width / 2, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(height / childCount, MeasureSpec.EXACTLY)
            )
        }
    }

		/**
		 * changed - логическое значение, указывающее, изменилось ли расположение.
		 * l, t, r, b - координаты (левый, верхний, правый, нижний) текущего ViewGroup.
		 */
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        // Размещение дочерних View
        var topOffset = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            val childWidth = child.measuredWidth
            val childHeight = child.measuredHeight
            child.layout(0, topOffset, childWidth, topOffset + childHeight)
            topOffset += childHeight
        }
    }
}
Android View. Вопросы на собесе
  1. Что делает метод onMeasure?

    Используется для измерения размера View и его дочерних элементов, устанавливая ширину и высоту в соответствии с заданными условиями и ограничениями.

  1. Что делает метод onLayout?

    Отвечает за позиционирование и установку размеров дочерних элементов внутри ViewGroup или текущего элемента и его оступов в View.

  1. Что делает метод onDraw?

    Отвечает за отрисовку содержимого View на экране, где можно использовать Canvas для рисования графических элементов.

  1. Что делает метод invalidate?

    Помечает View как требующую перерисовки, вызывая метод onDraw() для обновления её внешнего вида.

  1. Что делает метод requestLayout?

    Инициирует перерасчет размеров и перерисовку View, вызывая onMeasure() и onLayout().

  1. Какой жизненный цикл у View?

    onAttachToWindow()onMeasure()onLayout()onDraw()onDetachedFromWindow()

  1. Какие есть инструменты для создания анимаций?

    ValueAnimator PropertyAnimator ObjectAnimator MotionLayout

  1. Как сохранять состояние View?

    Указать id.

    Переопределить методы onSaveInstanceState и onRestoreInstanceState.

    У View может быть своя ViewModel.

  1. Как сделать круглый View?

    Сanvas.drawCircle

  1. Как сделать одинаковое соотношение сторон у View?

    ConstraintLayout с ratio, onMeasure() вручную изменить MeasureSpec

  1. С помощью какого компонента лучше выполнять отложенный (по запросу) inflate дочерней View?

    ViewStub

  1. Какие методы наследника ViewGroup нужно переопределять чтобы управлять его размерами и позиционированием содержимого?

    onMeasure() и onLayout()