Custom View в Android — рисуване и обработка на събития

Автор: IT Sectr Публикувано: 2026-07-20 Време за четене: 9 мин

Custom View — това е персонализиран компонент на интерфейса в Android, създаван чрез наследяване от класа View или неговите подкласове (Button, TextView, ImageView) и предефиниране на ключови методи: onDraw за рисуване, onMeasure за измерване на размери и onTouchEvent за обработка на докосвания. Според Google Android Developer Guide (2024), Custom View се използва, когато стандартните компоненти на Android SDK не осигуряват необходимото поведение или външен вид — например за персонализирана анимация, нестандартни форми, специализирани графики и игрови елементи.

Основни точки

  • Custom View — създава се чрез наследяване от View или неговите подкласове с предефиниране на onDraw, onMeasure, onTouchEvent.
  • Рисуването се извършва чрез Canvas API — геометрични фигури, текст, пътища, Bitmap и анимация.
  • onMeasure отговаря за коректните размери на View, като взема предвид layout параметрите и padding.
  • Персонализирани XML атрибути се декларират в res/values/attrs.xml и се четат в конструктора на View.
  • Обработката на докосвания onTouchEvent позволява добавяне на поддръжка за жестове, drag-and-drop и мултитъч.

Какво е Custom View в Android?

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 теми.

kotlin
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 и onLayout: управление на размери

onMeasure — това е методът, в който View съобщава на системата своите желани размери въз основа на подадения MeasureSpec (родителски ограничения). Без правилна реализация на onMeasure, View може да има нулева височина при wrap_content или да заема цялата площ на екрана при match_parent.

MeasureSpec се състои от режим (EXACTLY, AT_MOST, UNSPECIFIED) и стойност. EXACTLY — родителят е задал точен размер (match_parent или фиксирана стойност). AT_MOST — родителят е задал максимум (wrap_content). UNSPECIFIED — без ограничения (ScrollView, ListView).

kotlin
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 и Canvas API: рисуване

onDraw — сърцето на Custom View. Този метод се извиква от Android при всяко прерисуване на View. В него се подава Canvas обект, чрез който се извършва цялото рисуване: геометрични фигури, текст, пътища, Bitmap и анимации.

Canvas API предоставя методи за всички основни операции: drawCircle, drawRect, drawLine, drawPath, drawText, drawBitmap, drawArc, drawOval. За настройка на стила се използва Paint — обект, който определя цвят, дебелина, стил на запълване, шрифт, сенки и ефекти.

kotlin
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() за задействане на прерисуване.

Персонализирани XML атрибути в attrs.xml

Персонализирани атрибути позволяват настройка на 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.

xml
<!-- 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 за освобождаване на ресурси.

kotlin
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

onTouchEvent — метод, който се извиква при всяко докосване на View. Той получава MotionEvent обект с информация за типа събитие (ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL), координати, натиск и брой пръсти (мултитъч).

За обработка на сложни жестове (свайп, мащабиране, продължително натискане) използвайте GestureDetector или ScaleGestureDetector в комбинация с onTouchEvent. GestureDetector опростява разпознаването на onSingleTapUp, onFling, onLongPress, onDoubleTap и други стандартни жестове.

kotlin
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

Custom View може да стане тясно място в производителността, ако onDraw се извиква често (анимация, скрол) или съдържа тежки операции. Android предоставя няколко механизма за оптимизация на рисуването на персонализирани компоненти.

Layer type за сложно рисуване

View.setLayerType позволява превключване на рендерирането на View към софтуерен (LAYER_TYPE_SOFTWARE) или хардуерен (LAYER_TYPE_HARDWARE) слой. Software layer е полезен за сложна Canvas графика, която не се поддържа от хардуерно ускорение — например drawTextOnPath или сложни Path ефекти.

invalidate само на необходимата област

invalidate(Rect) — прерисува само указаната област на View, а не целия компонент. Това е критично важно за големи Custom View (графики, карти, платна за рисуване), където пълното прерисуване на всеки кадър причинява спад на FPS. Използвайте postInvalidateOnAnimation() за синхронизация с vsync.

kotlin
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 вместо Compose?

Custom View (Canvas API) е оправдан, когато е необходима максимална производителност на персонализирано рисуване (графики, видео редактори, карти), интеграция със съществуващ View-based код или поддръжка на Android версии под API 21. Jetpack Compose е модерният подход за повечето нови проекти, използващ декларативен UI и по-подходящ за динамични интерфейси.

Задължително ли е да се предефинира onMeasure?

Да, ако View използва wrap_content в XML кода. Без onMeasure, wrap_content ще работи като match_parent, тъй като стандартната реализация на View.onMeasure не задава размер по подразбиране. Ако View винаги има фиксиран размер или match_parent, onMeasure може да се пропусне.

Как да анимирам Custom View?

Използвайте ValueAnimator или ObjectAnimator за промяна на свойствата на View (цвят, радиус, позиция) и извикайте invalidate() в callback на анимацията за прерисуване. ValueAnimator работи на основната нишка и е синхронизиран с vsync. За сложни анимации (физика, частици) използвайте Choreographer.FrameCallback или Android Animation Framework.

Как да обработя мултитъч в Custom View?

В onTouchEvent използвайте MotionEvent.getPointerCount() за определяне на броя пръсти и getPointerId(i) за проследяване на всеки пръст по уникален ID. ACTION_POINTER_DOWN / ACTION_POINTER_UP — събития за добавяне/премахване на пръст. За мащабиране използвайте ScaleGestureDetector в комбинация с onTouchEvent — опростява откриването на pinch-to-zoom.

Може ли да се използва Custom View в Jetpack Compose?

Да, чрез AndroidView — composable функция, която вмъква View-based елемент в Compose йерархията. AndroidView приема фабрика за създаване на View и callback update за синхронизация на състоянието. Това позволява повторно използване на съществуващи Custom View в нови Compose проекти без пълно пренаписване.

Обобщение

  • Custom View — персонализиран Android компонент, създаден чрез наследяване на View с предефиниране на onDraw, onMeasure и onTouchEvent за персонализирано рисуване и обработка.
  • onDraw и Canvas API — основата на рисуването: геометрични фигури, текст, Bitmap, пътища — с конфигурация чрез Paint за цвят, стил и шрифт.
  • onMeasure е важен за коректната работа на wrap_content; използва MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) и resolveSize за изчисляване на размери.
  • Персонализирани атрибути чрез attrs.xml и TypedArray позволяват настройка на Custom View от XML код с поддръжка на автоматично довършване в Android Studio.
  • onTouchEvent с GestureDetector осигурява обработка на докосвания, жестове и мултитъч, а хит-тестването определя областта на взаимодействие.
  • Производителност: създавайте обекти веднъж в конструктора, използвайте invalidate(Rect) за частично прерисуване и LayerType за сложни ефекти.
  • Custom View се интегрира с Jetpack Compose чрез AndroidView, което позволява повторно използване на готови компоненти в нови Compose проекти.

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също