Custom View w Android — rysowanie i obsługa zdarzeń

Autor: IT Sectr Opublikowano: 2026-07-20 Czas czytania: 9 min

Custom View — to niestandardowy komponent interfejsu w Android, tworzony poprzez dziedziczenie z klasy View lub jej podklas (Button, TextView, ImageView) i nadpisywanie kluczowych metod: onDraw do rysowania, onMeasure do określania rozmiarów oraz onTouchEvent do obsługi dotknięć. Według Google Android Developer Guide (2024), Custom View jest używany, gdy standardowe komponenty Android SDK nie zapewniają wymaganego zachowania lub wyglądu — na przykład do niestandardowych animacji, nietypowych kształtów, specjalistycznych wykresów i elementów gier.

Najważniejsze

  • Custom View — tworzony przez dziedziczenie z View lub jego podklas z nadpisaniem onDraw, onMeasure, onTouchEvent.
  • Rysowanie odbywa się przez Canvas API — figury geometryczne, tekst, ścieżki, Bitmap i animacje.
  • onMeasure odpowiada za prawidłowe rozmiary View z uwzględnieniem parametrów layout i padding.
  • Niestandardowe atrybuty XML są deklarowane w res/values/attrs.xml i odczytywane w konstruktorze View.
  • Obsługa dotknięć onTouchEvent umożliwia dodanie obsługi gestów, przeciągania i multitouch.

Czym jest Custom View w Android?

Custom View — to klasa dziedzicząca po android.view.View (lub jednej z jej podklas), która nadpisuje systemowe metody w celu implementacji własnej logiki rysowania, mierzenia i obsługi zdarzeń. Custom View to podstawowy mechanizm Android do tworzenia unikalnych komponentów UI.

Android udostępnia dwa podejścia do tworzenia Custom View: dziedziczenie z View (całkowicie niestandardowe rysowanie przez onDraw) oraz dziedziczenie z istniejącej podklasy View (np. nadpisanie Button lub TextView w celu dodania funkcjonalności przy zachowaniu podstawowego zachowania).

Podstawowy konstruktor Custom View musi przyjmować Context i AttributeSet — umożliwia to systemowi tworzenie View z kodu XML. Jeśli View będzie używane tylko z kodu, wystarczy konstruktor z Context. Trzeci konstruktor ze stylem jest potrzebny do obsługi motywów 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)
    }
}

Minimalny Custom View składa się z konstruktora i onDraw. Jednak do prawidłowego działania w systemie Layout Android konieczne jest również nadpisanie onMeasure — w przeciwnym razie View może wyświetlać się z zerowymi rozmiarami lub nieprawidłowo reagować na parametry layoutu wrap_content.

Według Google I/O 2023, niestandardowe View są używane w 65% aplikacji z top 100 Google Play. Najczęstsze powody tworzenia Custom View: niestandardowe animacje (35%), niestandardowe wykresy i diagramy (25%), specjalistyczne elementy sterujące (20%) i komponenty brandingowe (20%).

onMeasure i onLayout: zarządzanie rozmiarami

onMeasure — to metoda, w której View informuje system o swoich pożądanych rozmiarach na podstawie przekazanych MeasureSpec (ograniczeń nadrzędnych). Bez prawidłowej implementacji onMeasure View może mieć zerową wysokość przy wrap_content lub zajmować cały obszar ekranu przy match_parent.

MeasureSpec składa się z trybu (EXACTLY, AT_MOST, UNSPECIFIED) i wartości. EXACTLY — rodzic określił dokładny rozmiar (match_parent lub stała wartość). AT_MOST — rodzic określił maksimum (wrap_content). UNSPECIFIED — bez ograniczeń (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 — to użyteczna metoda Android, która upraszcza implementację onMeasure. Przyjmuje pożądany rozmiar i MeasureSpec, a zwraca prawidłową wartość: dla EXACTLY — dokładny rozmiar, dla AT_MOST — minimum z pożądanego i maksymalnego, dla UNSPECIFIED — pożądany rozmiar.

onLayout dla View (nie ViewGroup) zwykle nie jest nadpisywany — jest wywoływany przez rodzica do pozycjonowania elementów potomnych. Dla ViewGroup onLayout jest obowiązkowy — należy w nim wywołać layout() dla każdego potomnego View.

onDraw i Canvas API: rysowanie

onDraw — serce Custom View. Ta metoda jest wywoływana przez Android przy każdym przerysowaniu View. Otrzymuje obiekt Canvas, przez który wykonywane jest całe rysowanie: figury geometryczne, tekst, ścieżki, Bitmap i animacje.

Canvas API udostępnia metody dla wszystkich podstawowych operacji: drawCircle, drawRect, drawLine, drawPath, drawText, drawBitmap, drawArc, drawOval. Do konfiguracji stylu używany jest Paint — obiekt określający kolor, grubość, styl wypełnienia, czcionkę, cienie i efekty.

kotlin
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    // Tło
    canvas.drawRect(0f, 0f,
        width.toFloat(), height.toFloat(), bgPaint)

    // Tekst
    canvas.drawText("Niestandardowy widok",
        paddingLeft.toFloat(),
        paddingTop.toFloat() + textPaint.textSize,
        textPaint)

    // Koło
    canvas.drawCircle(
        width / 2f,
        height / 2f,
        radius, circlePaint)

    // Ścieżka (krzywa Beziera)
    val path = Path().apply {
        moveTo(0f, height.toFloat())
        quadTo(width / 2f, 0f,
            width.toFloat(), height.toFloat())
    }
    canvas.drawPath(path, pathPaint)
}

Paint — to konfigurator stylu rysowania. Antyaliasing (Paint.ANTI_ALIAS_FLAG) jest obowiązkowy dla gładkich krawędzi. Subpixel text (SUBPIXEL_TEXT_FLAG) poprawia jakość tekstu. Style.FILL, STROKE i FILL_AND_STROKE określają, czy figura będzie wypełniona, obrysowana konturem, czy jedno i drugie.

Ważne: nie twórz obiektów Paint, Path, Rect w onDraw — prowadzi to do alokacji w pętli rysowania i wywołuje garbage collection, co obniża FPS. Twórz wszystkie obiekty rysowania w konstruktorze lub w metodzie init. Do animacji używaj ValueAnimator lub ObjectAnimator z invalidate() do wywołania przerysowania.

Niestandardowe atrybuty XML w attrs.xml

Niestandardowe atrybuty umożliwiają konfigurację Custom View z kodu XML tak samo, jak standardowe android:layout_width czy android:background. Atrybuty są deklarowane w pliku res/values/attrs.xml z podaniem nazwy, typu i opcjonalnej wartości domyślnej.

Typy atrybutów: string, integer, float, boolean, color, dimension, enum, flag, fraction, reference (odnośnik do zasobu). Dla każdego typu Android automatycznie parsuje wartość z XML i przekazuje ją do 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>

W konstruktorze Custom View atrybuty są odczytywane przez context.obtainStyledAttributes, który zwraca TypedArray. TypedArray udostępnia metody dostępu z typowaniem: getColor, getDimension, getString, getBoolean, getInt. Po odczytaniu TypedArray należy wywołać recycle() w celu zwolnienia zasobów.

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()
}

Użycie w XML: dodaj przestrzeń nazw app (xmlns:app="http://schemas.android.com/apk/res-auto") i używaj niestandardowych atrybutów jako app:circleColor="@color/red". Android Studio będzie autouzupełniać i sprawdzać typy atrybutów, jeśli deklaracja w attrs.xml jest poprawna.

Obsługa dotknięć z onTouchEvent

onTouchEvent — metoda wywoływana przy każdym dotknięciu View. Otrzymuje obiekt MotionEvent z informacją o typie zdarzenia (ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL), współrzędnych, sile nacisku i liczbie palców (multitouch).

Do obsługi złożonych gestów (przeciągnięcie, skalowanie, długie naciśnięcie) używaj GestureDetector lub ScaleGestureDetector w kombinacji z onTouchEvent. GestureDetector upraszcza rozpoznawanie onSingleTapUp, onFling, onLongPress, onDoubleTap i innych standardowych gestów.

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)
}

Hit-testing — określenie, czy touch trafił w określony obszar View. Dla obszarów prostokątnych używaj Rect.contains(x, y). Dla okręgów — sprawdzenie odległości od środka: sqrt(dx^2 + dy^2) < radius. Dla dowolnych kształtów — Path.op() lub Region.contains().

Multitouch jest obsługiwany przez MotionEvent.getPointerCount() i getPointerId(index). Każdy palec otrzymuje unikalny ID, który jest zachowany od ACTION_DOWN do ACTION_POINTER_UP. ACTION_MOVE może zawierać dane wszystkich aktywnych palców — używaj getHistoricalX/Y do interpolacji ruchu.

Optymalizacja wydajności Custom View

Custom View może stać się wąskim gardłem wydajności, jeśli onDraw jest wywoływany często (animacja, scroll) lub zawiera ciężkie operacje. Android udostępnia kilka mechanizmów do optymalizacji rysowania niestandardowych komponentów.

Layer type dla złożonego rysowania

View.setLayerType umożliwia przełączenie renderowania View na programowy (LAYER_TYPE_SOFTWARE) lub sprzętowy (LAYER_TYPE_HARDWARE). Software layer jest przydatny do złożonej grafiki Canvas, która nie jest wspierana przez przyspieszenie sprzętowe — na przykład drawTextOnPath lub złożone efekty Path.

invalidate tylko potrzebnego obszaru

invalidate(Rect) — przerysowuje tylko wskazany obszar View, a nie cały komponent. Jest to krytycznie ważne dla dużych Custom View (grafiki, mapy, canvasy do rysowania), gdzie pełne przerysowanie każdej klatki powoduje spadek FPS. Używaj postInvalidateOnAnimation() do synchronizacji z vsync.

kotlin
class EfficientCustomView
    @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : View(context, attrs) {

    // Obiekty tworzone raz w konstruktorze
    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)
        // Przerysuj tylko obszar wokół punktu
        rect.set(
            (x - 10).toInt(),
            (y - 10).toInt(),
            (x + 10).toInt(),
            (y + 10).toInt()
        )
        invalidate(rect)
    }
}

Przyspieszenie sprzętowe jest domyślnie włączone na Android 3.0+ (API 11+). Operacje Canvas drawCircle, drawRect, drawBitmap są przyspieszane sprzętowo i wykonywane na GPU. Jednak drawTextOnPath, drawVertices, złożone operacje clipping nie są przyspieszane — dla nich używaj LAYER_TYPE_SOFTWARE.

Według Android Performance Patterns (Google, 2023), główne przyczyny lagów Custom View to alokacje w onDraw (tworzenie obiektów w każdej klatce), przerysowywanie całego View przy zmianie jednego elementu oraz brak LayerType.HARDWARE dla statycznej treści. Naprawienie tych trzech problemów daje wzrost FPS z 30 do 60 w większości scenariuszy.

MetodaPrzeznaczenieCzęstotliwość wywołania
onDrawRysowanie zawartości ViewPrzy każdym invalidate()
onMeasureOkreślanie rozmiarów ViewPrzy zmianie layoutu
onLayoutPozycjonowanie potomnych ViewPo onMeasure
onTouchEventObsługa dotknięćPrzy każdym dotknięciu
onSizeChangedReakcja na zmianę rozmiaruPrzy pierwszym layout i zmianie

Często zadawane pytania

Kiedy używać Custom View zamiast Compose?

Custom View (Canvas API) jest uzasadniony, gdy potrzebna jest maksymalna wydajność niestandardowego rysowania (grafiki, edytory wideo, mapy), integracja z istniejącym kodem View-based lub wsparcie wersji Android poniżej API 21. Jetpack Compose to nowoczesne podejście dla większości nowych projektów, wykorzystujące deklaratywny UI i lepiej nadające się do dynamicznych interfejsów.

Czy konieczne jest nadpisywanie onMeasure?

Tak, jeśli View używa wrap_content w kodzie XML. Bez onMeasure wrap_content będzie działać jak match_parent, ponieważ standardowa implementacja View.onMeasure nie ustawia domyślnego rozmiaru. Jeśli View zawsze ma stały rozmiar lub match_parent, onMeasure można pominąć.

Jak animować Custom View?

Używaj ValueAnimator lub ObjectAnimator do zmiany właściwości View (kolor, promień, pozycja) i wywołuj invalidate() w callbacku animacji w celu przerysowania. ValueAnimator działa na głównym wątku i jest zsynchronizowany z vsync. Do złożonych animacji (fizyka, cząstki) używaj Choreographer.FrameCallback lub Android Animation Framework.

Jak obsłużyć multitouch w Custom View?

W onTouchEvent używaj MotionEvent.getPointerCount() do określenia liczby palców i getPointerId(i) do śledzenia każdego palca po unikalnym ID. ACTION_POINTER_DOWN / ACTION_POINTER_UP — zdarzenia dodania/usunięcia palca. Do skalowania używaj ScaleGestureDetector w parze z onTouchEvent — upraszcza detekcję pinch-to-zoom.

Czy można używać Custom View w Jetpack Compose?

Tak, przez AndroidView — funkcję composable, która wstawia element View-based do hierarchii Compose. AndroidView przyjmuje fabrykę do tworzenia View i callback update do synchronizacji stanu. Umożliwia to ponowne wykorzystanie istniejących Custom View w nowych projektach Compose bez całkowitego przepisywania.

Podsumowanie

  • Custom View — niestandardowy komponent Android, tworzony przez dziedziczenie z View z nadpisaniem onDraw, onMeasure i onTouchEvent do niestandardowego rysowania i obsługi.
  • onDraw i Canvas API — podstawa rysowania: figury geometryczne, tekst, Bitmap, ścieżki — z konfiguracją przez Paint dla koloru, stylu i czcionki.
  • onMeasure jest ważny dla prawidłowego działania wrap_content; używa MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) i resolveSize do obliczania rozmiarów.
  • Niestandardowe atrybuty przez attrs.xml i TypedArray umożliwiają konfigurację Custom View z kodu XML z obsługą autouzupełniania w Android Studio.
  • onTouchEvent z GestureDetector zapewnia obsługę dotknięć, gestów i multitouch, a hit-testing określa obszar interakcji.
  • Wydajność: twórz obiekty raz w konstruktorze, używaj invalidate(Rect) do częściowego przerysowania i LayerType dla złożonych efektów.
  • Custom View integruje się z Jetpack Compose przez AndroidView, co umożliwia ponowne wykorzystanie gotowych komponentów w nowych projektach Compose.

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również