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("Персонализиран изглед",
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 — това води до алокации в цикъла на рисуване и задейства garbage collection, което намалява 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. След прочитане трябва да се извика recycle() на TypedArray за освобождаване на ресурси.
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) слой. 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) {
// Обекти, създадени веднъж в конструктора
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() в callback на анимацията за прерисуване. 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 и callback update за синхронизация на състоянието. Това позволява повторно използване на съществуващи Custom View в нови Compose проекти без пълно пренаписване.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също