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 — 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.
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 — 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).
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 — 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.
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 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.
<!-- 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.
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.
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.
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.
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.
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(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.
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.
| Metoda | Przeznaczenie | Częstotliwość wywołania |
|---|---|---|
| onDraw | Rysowanie zawartości View | Przy każdym invalidate() |
| onMeasure | Określanie rozmiarów View | Przy zmianie layoutu |
| onLayout | Pozycjonowanie potomnych View | Po onMeasure |
| onTouchEvent | Obsługa dotknięć | Przy każdym dotknięciu |
| onSizeChanged | Reakcja na zmianę rozmiaru | Przy pierwszym layout i zmianie |
Często zadawane pytania
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.
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ąć.
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.
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.
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
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.
Przeczytaj również