View & ViewGroup
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)
- Какой жизненный цикл у View?
onAttachToWindow()
→onMeasure()
→onLayout()
→onDraw()
→onDetachedFromWindow()
- Что делает каждый из методов жизненного цикла View?
•
onAttachToWindow
вызывается когдаView
прикрепляется к окну.•
onMeasure
вызывается для определения требований к размеру этогоView
и всех его дочерних элементов.•
onLayout
вызывается когдаView
должно назначить размер и позицию всем своим дочерним элементам.•
onDraw
вызывается когда View должен отрисовать свое содержимое.•
onDetachedFromWindow
вызывается когда View открепляется от своего окна.
- Что происходит с View после выполнения полного цикла отрисовки?
Обновляет свое состояние, например, положения и размеры, и переходит в состояние, в котором он готов к следующему циклу отрисовки, а также сохраняет состояние, чтобы отразить любые изменения, произошедшие во время этого цикла.
- Что делает метод onMeasure?
Используется для измерения размера View и его дочерних элементов, устанавливая ширину и высоту в соответствии с заданными условиями и ограничениями.
- Что делает метод onLayout?
Отвечает за позиционирование и установку размеров дочерних элементов внутри
ViewGroup
или текущего элемента и его оступов вView
.
- Если View не содержит дочерние элементы, что будет делать метод onLayout?
Позиционировать текущую
View
и ее отступы.
- Что делает метод onDraw?
Отвечает за отрисовку содержимого
View
на экране, где можно использоватьCanvas
для рисования графических элементов.
- Как часто может вызывается метод onDraw?
Метод
onDraw
может вызываться до 60 раз в секунду, что соответствует частоте обновления экрана в 60 FPS. На практике частота может быть ниже из-за производительности устройства или сложных операций рисования.
- Что не рекомендуется делать в методе onDraw?
• Создавать объекты (чтобы избежать ненужного использования памяти и GC).
• Выполнять долгие операции (например, чтение данных с диска или сети).
• Вызывать методы, влияющие на макет или мерки, как
requestLayout()
.
- Какие методы наследника ViewGroup нужно переопределять чтобы управлять его размерами и позиционированием содержимого?
onMeasure()
иonLayout()
- Что делает метод invalidate?
Помечает
View
как требующую перерисовки, вызывая методonDraw()
для обновления её внешнего вида.
- Что делает метод requestLayout?
Инициирует перерасчет размеров и перерисовку View, вызывая
onMeasure()
иonLayout()
.
- Что делает метод forceLayout?
Вызывает немедленный перерасчет и повторное построение разметки для текущего View и его дочерних элементов, игнорируя любые флаги, указывающие на то, что разметка уже была выполнена.
- Что не рекомендуется делать в методе onDraw() при создании Custom View?
• Создавать объекты, такие как шрифты, кисти или объекты графики — это приведет к частому выделению памяти и сборке мусора.
• Выполнять долгие операции, такие как чтение файлов или запросы к сети, так как это замедлит отрисовку и снизит производительность.
• Изменять состояние View или триггерить перерисовку (например, вызывать
invalidate()
), так как это может вызвать бесконечный цикл перерисовки.
- Что не рекомендуется делать в методе onDraw() при создании Custom View?
• Использовать несколько объектов
Paint
.• Использовать объект
Canvas
.• Создавать объекты внутри метода
onDraw()
.
- Какой жизненный цикл у View?
Другие (5)
- Как сохранять состояние View?
• Указать
id
.• Переопределить методы
onSaveInstanceState
иonRestoreInstanceState
.• У
View
может быть свояViewModel
.
- Как восстановить состояние View после того как перевернулся экран?
Переопределить метод
onRestoreInstanceState()
для восстановления состояния.
- Как сделать круглый View?
Сanvas.drawCircle
- Как сделать одинаковое соотношение сторон у View?
•
ConstraintLayout
сratio
.•
onMeasure()
вручную изменитьMeasureSpec
.
- Как сборщик мусора может заставить UI тормозить?
Из-за механизма «stop-the-world», когда приложение временно приостанавливается для очистки памяти.
- Как сохранять состояние View?