Custom View — это пользовательский компонент интерфейса в Android, создаваемый путём наследования от класса View или его подклассов (Button, TextView, ImageView) и переопределения ключевых методов: onDraw для рисования, onMeasure для измерения размеров и onTouchEvent для обработки касаний. По данным Google Android Developer Guide (2024), Custom View используется, когда стандартные компоненты Android SDK не обеспечивают требуемое поведение или внешний вид — например, для кастомной анимации, нестандартных форм, специализированных графиков и игровых элементов.
Главное
Custom View — это класс, наследующий android.view.View (или один из его подклассов), который переопределяет системные методы для реализации собственной логики рисования, измерения и обработки событий. Custom View — это фундаментальный механизм Android для создания уникальных UI-компонентов.
Android предоставляет два подхода к созданию Custom View: наследование от View (полностью кастомная отрисовка через onDraw) и наследование от существующего View-подкласса (например, переопределение Button или TextView для добавления функциональности при сохранении базового поведения).
Базовый конструктор Custom View должен принимать Context и AttributeSet — это позволяет системе создавать View из XML-разметки. Если View будет использоваться только из кода, достаточно конструктора с Context. Третий конструктор со стилем нужен для поддержки тем Android.
class CustomView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLUE
strokeWidth = 4f
style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(width / 2f,
height / 2f,
50f, paint)
}
}
Минимальный Custom View состоит из конструктора и onDraw. Однако для корректной работы в Layout-системе Android необходимо также переопределить onMeasure — иначе View может отображаться с нулевыми размерами или неправильно реагировать на layout-параметры wrap_content.
По данным Google I/O 2023, кастомные View используются в 65% приложений из топ-100 Google Play. Наиболее частые причины создания Custom View: нестандартные анимации (35%), кастомные графики и диаграммы (25%), специализированные элементы управления (20%) и брендированные компоненты (20%).
onMeasure — это метод, в котором View сообщает системе свои желаемые размеры на основе переданных MeasureSpec (родительские ограничения). Без правильной реализации onMeasure View может иметь нулевую высоту при wrap_content или занимать всю площадь экрана при match_parent.
MeasureSpec состоит из режима (EXACTLY, AT_MOST, UNSPECIFIED) и значения. EXACTLY — родитель задал точный размер (match_parent или фиксированное значение). AT_MOST — родитель задал максимум (wrap_content). UNSPECIFIED — без ограничений (ScrollView, ListView).
override fun onMeasure(
widthMeasureSpec: Int,
heightMeasureSpec: Int
) {
val desiredWidth = paddingLeft + paddingRight
+ DEFAULT_WIDTH
val desiredHeight = paddingTop + paddingBottom
+ DEFAULT_HEIGHT
val measuredWidth = MeasureSpec.getSize(widthMeasureSpec)
val modeWidth = MeasureSpec.getMode(widthMeasureSpec)
val resultWidth = when (modeWidth) {
MeasureSpec.EXACTLY -> measuredWidth
MeasureSpec.AT_MOST ->
desiredWidth.coerceAtMost(measuredWidth)
else -> desiredWidth
}
val resultHeight = resolveSize(desiredHeight,
heightMeasureSpec)
setMeasuredDimension(resultWidth, resultHeight)
}
resolveSize — это утилитарный метод Android, который упрощает реализацию onMeasure. Он принимает желаемый размер и MeasureSpec и возвращает корректное значение: для EXACTLY — точный размер, для AT_MOST — минимум из желаемого и максимального, для UNSPECIFIED — желаемый размер.
onLayout для View (не ViewGroup) обычно не переопределяется — он вызывается родителем для позиционирования дочерних элементов. Для ViewGroup onLayout обязателен — в нём нужно вызвать layout() для каждого дочернего View.
onDraw — сердце Custom View. Этот метод вызывается Android при каждой перерисовке View. В него передаётся объект Canvas, через который выполняется всё рисование: геометрические фигуры, текст, пути, Bitmap и анимации.
Canvas API предоставляет методы для всех базовых операций: drawCircle, drawRect, drawLine, drawPath, drawText, drawBitmap, drawArc, drawOval. Для настройки стиля используется Paint — объект, определяющий цвет, толщину, стиль заливки, шрифт, тени и эффекты.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Background
canvas.drawRect(0f, 0f,
width.toFloat(), height.toFloat(), bgPaint)
// Text
canvas.drawText("Custom View",
paddingLeft.toFloat(),
paddingTop.toFloat() + textPaint.textSize,
textPaint)
// Circle
canvas.drawCircle(
width / 2f,
height / 2f,
radius, circlePaint)
// Path (Bezier curve)
val path = Path().apply {
moveTo(0f, height.toFloat())
quadTo(width / 2f, 0f,
width.toFloat(), height.toFloat())
}
canvas.drawPath(path, pathPaint)
}
Paint — это конфигуратор стиля рисования. Антив aliasing (Paint.ANTI_ALIAS_FLAG) обязателен для гладких краёв. Subpixel text (SUBPIXEL_TEXT_FLAG) улучшает качество текста. Style.FILL, STROKE и FILL_AND_STROKE определяют, будет ли фигура закрашена, обведена контуром или и то и другое.
Важно: не создавайте объекты Paint, Path, Rect в onDraw — это приводит к аллокациям в цикле отрисовки и вызывает сборку мусора, что снижает FPS. Создавайте все объекты рисования в конструкторе или в методе init. Для анимации используйте ValueAnimator или ObjectAnimator с invalidate() для запуска перерисовки.
Кастомные атрибуты позволяют настраивать Custom View из XML-разметки так же, как стандартные android:layout_width или android:background. Атрибуты объявляются в файле res/values/attrs.xml с указанием имени, типа и опционального значения по умолчанию.
Типы атрибутов: string, integer, float, boolean, color, dimension, enum, flag, fraction, reference (ссылка на ресурс). Для каждого типа Android автоматически парсит значение из XML и передаёт его в TypedArray.
<!-- res/values/attrs.xml -->
<resources>
<declare-styleable name="CustomView">
<attr name="circleColor"
format="color" />
<attr name="circleRadius"
format="dimension" />
<attr name="labelText"
format="string" />
<attr name="showAnimation"
format="boolean" />
</declare-styleable>
</resources>
В конструкторе Custom View атрибуты читаются через context.obtainStyledAttributes, который возвращает TypedArray. TypedArray предоставляет типизированные методы доступа: getColor, getDimension, getString, getBoolean, getInt. После чтения TypedArray обязательно нужно вызвать recycle() для освобождения ресурсов.
init {
val typedArray = context.obtainStyledAttributes(
attrs,
R.styleable.CustomView
)
circleColor = typedArray.getColor(
R.styleable.CustomView_circleColor,
Color.BLUE
)
circleRadius = typedArray.getDimension(
R.styleable.CustomView_circleRadius,
50f
)
labelText = typedArray.getString(
R.styleable.CustomView_labelText
) ?: "Default"
typedArray.recycle()
}
Использование в XML: добавьте пространство имён app (xmlns:app="http://schemas.android.com/apk/res-auto") и используйте кастомные атрибуты как app:circleColor="@color/red". Android Studio будет autocomplete и проверять типы атрибутов, если объявление в attrs.xml корректно.
onTouchEvent — метод, вызываемый при каждом касании View. Он получает объект MotionEvent с информацией о типе события (ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL), координатах, давлении и количестве пальцев (мультитач).
Для обработки сложных жестов (свайп, масштабирование, долгое нажатие) используйте GestureDetector или ScaleGestureDetector в комбинации с onTouchEvent. GestureDetector упрощает распознавание onSingleTapUp, onFling, onLongPress, onDoubleTap и других стандартных жестов.
private val gestureDetector = GestureDetector(
context, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
handleTap(e.x, e.y)
return true
}
})
override fun onTouchEvent(event: MotionEvent): Boolean {
val handled = gestureDetector.onTouchEvent(event)
when (event.action) {
MotionEvent.ACTION_MOVE -> {
currentX = event.x
currentY = event.y
invalidate()
return true
}
}
return handled || super.onTouchEvent(event)
}
Хит-тестинг — определение, попал ли touch в определённую область View. Для прямоугольных областей используйте Rect.contains(x, y). Для кругов — проверка расстояния от центра: sqrt(dx^2 + dy^2) < radius. Для произвольных фигур — Path.op() или Region.contains().
Мультитач обрабатывается через MotionEvent.getPointerCount() и getPointerId(index). Каждый палец получает уникальный ID, который сохраняется от ACTION_DOWN до ACTION_POINTER_UP. ACTION_MOVE может содержать данные по всем активным пальцам — используйте getHistoricalX/Y для интерполяции движения.
Custom View может стать узким местом производительности, если onDraw вызывается часто (анимация, скролл) или содержит тяжёлые операции. Android предоставляет несколько механизмов для оптимизации отрисовки кастомных компонентов.
View.setLayerType позволяет переключить рендеринг View на программный (LAYER_TYPE_SOFTWARE) или аппаратный (LAYER_TYPE_HARDWARE) слой. Software layer полезен для сложной Canvas-графики, которая не поддерживается аппаратным ускорением — например, drawTextOnPath или сложные Path-эффекты.
invalidate(Rect) — перерисовывает только указанную область View, а не весь компонент. Это критически важно для больших Custom View (графики, карты, канвасы для рисования), где полная перерисовка каждого кадра вызывает падение FPS. Используйте postInvalidateOnAnimation() для синхронизации с vsync.
class EfficientCustomView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
// Objects created once in constructor
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val rect = Rect()
private val position = PointF()
fun updatePosition(x: Float, y: Float) {
position.set(x, y)
// Redraw only area around the point
rect.set(
(x - 10).toInt(),
(y - 10).toInt(),
(x + 10).toInt(),
(y + 10).toInt()
)
invalidate(rect)
}
}
Аппаратное ускорение включено по умолчанию на Android 3.0+ (API 11+). Canvas операции drawCircle, drawRect, drawBitmap аппаратно ускоряются и выполняются на GPU. Однако drawTextOnPath, drawVertices, сложные clipping-операции не ускоряются — для них используйте LAYER_TYPE_SOFTWARE.
По данным Android Performance Patterns (Google, 2023), основные причины лагов Custom View — аллокации в onDraw (создание объектов на каждый кадр), перерисовка всего View при изменении одного элемента и отсутствие LayerType.HARDWARE для статического контента. Исправление этих трёх проблем даёт прирост FPS с 30 до 60 в большинстве сценариев.
| Метод | Назначение | Частота вызова |
|---|---|---|
| onDraw | Рисование содержимого View | При каждом invalidate() |
| onMeasure | Определение размеров View | При изменении layout |
| onLayout | Позиционирование дочерних View | После onMeasure |
| onTouchEvent | Обработка касаний | При каждом касании |
| onSizeChanged | Реакция на изменение размера | При первом layout и изменении |
Часто задаваемые вопросы
Custom View (Canvas API) оправдан, когда нужна максимальная производительность кастомной отрисовки (графики, видеоредакторы, карты), интеграция с существующим View-based кодом или поддержка Android версий ниже API 21. Jetpack Compose — современный подход для большинства новых проектов, использующий декларативный UI и лучше подходящий для динамических интерфейсов.
Да, если View использует wrap_content в XML-разметке. Без onMeasure wrap_content будет работать как match_parent, так как стандартная реализация View.onMeasure не задаёт дефолтный размер. Если View всегда имеет фиксированный размер или match_parent, onMeasure можно не переопределять.
Используйте ValueAnimator или ObjectAnimator для изменения свойств View (цвет, радиус, позиция) и вызывайте invalidate() в колбэке анимации для перерисовки. ValueAnimator работает на главном потоке и синхронизирован с vsync. Для сложных анимаций (физика, частицы) используйте Choreographer.FrameCallback или Android Animation Framework.
В onTouchEvent используйте MotionEvent.getPointerCount() для определения количества пальцев и getPointerId(i) для отслеживания каждого пальца по уникальному ID. ACTION_POINTER_DOWN / ACTION_POINTER_UP — события добавления/удаления пальца. Для масштабирования используйте ScaleGestureDetector в паре с onTouchEvent — он упрощает детекцию pinch-to-zoom.
Да, через AndroidView — composable-функцию, которая внедряет View-based элемент в Compose-иерархию. AndroidView принимает фабрику для создания View и колбэк update для синхронизации состояния. Это позволяет переиспользовать существующие Custom View в новых Compose-проектах без полного переписывания.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также