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)
// Фон
canvas.drawRect(0f, 0f,
width.toFloat(), height.toFloat(), bgPaint)
// Текст
canvas.drawText("Custom View",
paddingLeft.toFloat(),
paddingTop.toFloat() + textPaint.textSize,
textPaint)
// Коло
canvas.drawCircle(
width / 2f,
height / 2f,
radius, circlePaint)
// Шлях (крива Безьє)
val path = Path().apply {
moveTo(0f, height.toFloat())
quadTo(width / 2f, 0f,
width.toFloat(), height.toFloat())
}
canvas.drawPath(path, pathPaint)
}
Paint — це конфігуратор стилю малювання. Антивіалісинг (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="логічний" />
</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 буде автодоповнювати та перевіряти типи атрибутів, якщо оголошення в 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)
}
Хіт-тестінг — визначення, чи потрапив дотик у певну область 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) шар. Програмний шар корисний для складної Canvas-графіки, яка не підтримується апаратним прискоренням — наприклад, drawTextOnPath або складні Path-ефекти.
invalidate(Rect) — перемальовує тільки вказану область View, а не весь компонент. Це критично важливо для великих Custom View (графіки, карти, канваси для малювання), де повне перемальовування кожного кадру викликає падіння FPS. Використовуйте postInvalidateOnAnimation() для синхронізації з vsync.
class EfficientCustomView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
// Об'єкти, створені один раз у конструкторі
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)
// Перемальовувати лише область навколо точки
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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також