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
View

Базовый класс для всех UI-элементов в Android, таких как кнопки, текстовые поля и изображения. Он отвечает за отображение контента, обработку событий и управление размерами. Все виджеты интерфейса в Android наследуются от класса View или его подклассов.

onAttachToWindow

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

override fun onAttachToWindow() {
    super.onAttachToWindow()
}
onDetachedFromWindow

Вызывается, когда View удаляется из окна.

override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
}
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

Используется для определения расположения и размеров дочерних 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
        }
    }
}
onDraw

Используется для рисования содержимого на экране.

val paint = Paint().apply {
    color = Color.RED
    style = Paint.Style.FILL
}

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    canvas.drawRect(50F, 50F, 200F, 200F, paint) // Рисуем прямоугольник
}
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)
    }
}
onSizeChanged

Вызывается, когда размер View изменяется.

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    // Логика при изменении размера
}
onTouchEvent

Обрабатывает события касания.

override fun onTouchEvent(event: MotionEvent): Boolean {
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            // Действие при начале касания
        }
        MotionEvent.ACTION_MOVE -> {
            // Действие при перемещении
        }
        MotionEvent.ACTION_UP -> {
            // Действие при завершении касания
        }
    }
    return true // Возвращаем true, чтобы событие не было передано дальше
}
ViewGroup

Базовый класс для контейнеров, которые могут содержать другие View элементы. Он управляет иерархией виджетов, а также их размещением, размером и позиционированием.

onInterceptTouchEvent

Решает, перехватывать ли событие касания. Если возвращает true, событие не передается дочерним элементам.

override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
    // Возвращаем true, чтобы перехватить событие
    return true
}
dispatchTouchEvent

Передает событие касания соответствующему View. Он обрабатывает событие, вызывая onInterceptTouchEvent для перехвата, а затем передает его дочерним элементам через onTouchEvent.

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
    // Переадресация события в дочерние элементы
    return super.dispatchTouchEvent(event)
}
Вопросы на собесе (20)
  • Lifecycle (15)
    1. Какой жизненный цикл у View?

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

    1. Что делает каждый из методов жизненного цикла View?

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

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

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

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

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

    1. Что происходит с View после выполнения полного цикла отрисовки?

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

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

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

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

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

    1. Если View не содержит дочерние элементы, что будет делать метод onLayout?

      Позиционировать текущую View и ее отступы.

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

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

    1. Как часто может вызывается метод onDraw?

      Метод onDraw может вызываться до 60 раз в секунду, что соответствует частоте обновления экрана в 60 FPS. На практике частота может быть ниже из-за производительности устройства или сложных операций рисования.

    1. Что не рекомендуется делать в методе onDraw?

      Создавать объекты (чтобы избежать ненужного использования памяти и GC).

      Выполнять долгие операции (например, чтение данных с диска или сети).

      Вызывать методы, влияющие на макет или мерки, как requestLayout().

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

      onMeasure() и onLayout()

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

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

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

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

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

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

    1. Что не рекомендуется делать в методе onDraw() при создании Custom View?

      Создавать объекты, такие как шрифты, кисти или объекты графики — это приведет к частому выделению памяти и сборке мусора.

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

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

    1. Что не рекомендуется делать в методе onDraw() при создании Custom View?

      Использовать несколько объектов Paint.

      Использовать объект Canvas.

      Создавать объекты внутри метода onDraw().

  • Другие (5)
    1. Как сохранять состояние View?

      Указать id.

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

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

    1. Как восстановить состояние View после того как перевернулся экран?

      Переопределить метод onRestoreInstanceState() для восстановления состояния.

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

      Сanvas.drawCircle

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

      ConstraintLayout с ratio.

      onMeasure() вручную изменить MeasureSpec.

    1. Как сборщик мусора может заставить UI тормозить?

      Из-за механизма «‎stop-the-world»‎, когда приложение временно приостанавливается для очистки памяти.