Canvas w Android — to klasa z pakietu android.graphics, udostępniająca API do rysowania 2D na View. Canvas działa jak płótno, na którym programista rysuje figury geometryczne, tekst, obrazy i ścieżki za pomocą różnych metod draw*. Według Android Developers Documentation (2025), Canvas jest głównym narzędziem do niestandardowego rysowania w Android i jest używany razem z klasą Paint do ustawiania stylów i kolorów. Każde wywołanie onDraw() otrzymuje gotową instancję Canvas powiązaną z buforem bitmap widoku.
Najważniejsze
Canvas — to centralna klasa grafiki 2D w Android, która udostępnia metody do rysowania na płaszczyźnie. Każdy Canvas jest powiązany z Bitmap — obszarem pamięci, w którym zapisywane są wyniki operacji rysowania. Gdy View jest przerysowywany, system przekazuje do onDraw() Canvas już powiązany z buforem tego View.
Canvas działa w układzie współrzędnych, gdzie (0,0) to lewy górny róg, oś X idzie w prawo, oś Y w dół. Wszystkie operacje rysowania wykorzystują bieżący stan Canvas, w tym macierz transformacji, obszar przycięcia (clip) i styl Paint. Według Android Documentation (2025), API Canvas zawiera ponad 30 metod draw* dla różnych typów prymitywów graficznych.
Od Androida 3.0 (API 11) Canvas obsługuje przyspieszenie sprzętowe przez DisplayList. W trybie przyspieszonym operacje Canvas są zapisywane do DisplayList, a następnie wykonywane na GPU. Daje to znaczący wzrost wydajności, ale nakłada ograniczenia: niektóre operacje Canvas (clipPath z nie-Path-clipem) nie są obsługiwane w trybie przyspieszonym.
Canvas udostępnia zestaw metod do rysowania podstawowych prymitywów. Każda metoda przyjmuje parametry geometryczne i obiekt Paint, określający styl wizualny.
| Metoda | Przeznaczenie | Przykład |
|---|---|---|
| drawRect() | Rysowanie prostokąta | canvas.drawRect(10f, 10f, 100f, 100f, paint) |
| drawCircle() | Rysowanie koła | canvas.drawCircle(50f, 50f, 30f, paint) |
| drawLine() | Rysowanie linii | canvas.drawLine(0f, 0f, 100f, 100f, paint) |
| drawText() | Rysowanie tekstu | canvas.drawText("Hello", 10f, 20f, paint) |
| drawBitmap() | Rysowanie obrazu | canvas.drawBitmap(bitmap, 0f, 0f, paint) |
| drawPath() | Rysowanie dowolnej ścieżki | canvas.drawPath(path, paint) |
| drawOval() | Rysowanie owalu | canvas.drawOval(rectF, paint) |
| drawArc() | Rysowanie łuku lub sektora | canvas.drawArc(rectF, 0f, 90f, true, paint) |
Każda metoda może być wywołana w dowolnej kolejności — Canvas rysuje w kolejności wywołań, nakładając nowe elementy na poprzednie. W przypadku złożonych kompozycji stosuje się technikę rysowania wielowarstwowego z zapisywaniem i przywracaniem stanu Canvas.
Paint — to klasa, która określa, jak Canvas będzie rysować. Bez Paint metody Canvas są bezużyteczne — ustawia on kolor, grubość linii, styl wypełnienia, rozmiar tekstu, efekty (cień, gradient, rozmycie) i tryb mieszania.
Paint ma kluczowe właściwości: color (kolor), style (FILL, STROKE lub FILL_AND_STROKE), strokeWidth (grubość linii), textSize (rozmiar tekstu), isAntiAlias (wygładzanie), shader (gradient lub obraz). Tworzenie obiektu Paint jest kosztowną operacją, dlatego należy go tworzyć raz i wielokrotnie używać.
private val fillPaint = Paint().apply {
color = Color.parseColor("#FF6200EE")
style = Paint.Style.FILL
isAntiAlias = true
}
private val strokePaint = Paint().apply {
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = 4f
isAntiAlias = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(100f, 100f, 80f, fillPaint)
canvas.drawCircle(100f, 100f, 80f, strokePaint)
}
W tym przykładzie używane są dwa Paint: jeden do wypełnienia koła kolorem fioletowym, drugi do białego obrysu. Podział na fill i stroke to standardowa praktyka, pozwalająca elastycznie zarządzać wyglądem bez odtwarzania obiektów.
Do wypełnień gradientowych Paint przyjmuje Shader — podklasy LinearGradient, RadialGradient, SweepGradient. Shader jest przypisywany do Paint przez metodę setShader() i określa, jak kolor zmienia się w obszarze rysowania.
Canvas obsługuje macierzowe transformacje układu współrzędnych: translate() (przesunięcie), rotate() (obrót), scale() (skalowanie) i skew() (pochylenie). Transformacje kumulują się: każde kolejne wywołanie jest stosowane na poprzednich.
Zarządzanie stosem transformacji odbywa się przez save() i restore(). save() zapisuje bieżący stan Canvas (macierz, clip) na stosie, restore() przywraca ostatni zapisany stan. Między save() a restore() mogą znajdować się zagnieżdżone transformacje.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.save() // Zapisz stan początkowy
canvas.translate(100f, 100f) // Przesuń początek układu
// Pierwszy kwadrat
canvas.drawRect(0f, 0f, 50f, 50f, fillPaint)
canvas.save() // Zapisz po przesunięciu
canvas.rotate(45f, 25f, 25f) // Obróć wokół środka kwadratu
// Obrócony kwadrat
canvas.drawRect(0f, 0f, 50f, 50f, strokePaint)
canvas.restore() // Przywróć po obrocie
canvas.restore() // Przywróć stan początkowy
}
Zawsze używaj save() przed rozpoczęciem transformacji i restore() po ich zakończeniu. Niezrównoważone save/restore prowadzą do kumulacji macierzy i nieprawidłowego rysowania kolejnych elementów.
W przypadku złożonej grafiki Canvas jest łączony z Bitmap i Path. Bitmap służy do ładowania obrazów i buforowania wyrenderowanych klatek. Path — do budowania dowolnych krzywych Beziera, wielokątów i złożonych konturów.
Technika offscreen-renderowania: tworzona jest Bitmap, na której rysowana jest złożona scena przez osobny Canvas, a następnie gotowa Bitmap jest wyświetlana w onDraw() przez drawBitmap(). Pozwala to aktualizować złożoną grafikę bez pełnego przerysowywania każdej klatki.
private var offscreenBitmap: Bitmap? = null
private var offscreenCanvas: Canvas? = null
fun buildOffscreenScene(width: Int, height: Int) {
offscreenBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
offscreenCanvas = Canvas(offscreenBitmap)
// Renderuj scenę raz
offscreenCanvas!!.drawColor(Color.WHITE)
drawGrid(offscreenCanvas!!)
drawComplexShape(offscreenCanvas!!)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
offscreenBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) }
}
Path jest używany do budowania złożonych konturów: krzywe Beziera (quadTo, cubicTo), łuki (arcTo), linie (lineTo). Path może być zamknięty (close()) dla wypełnionych figur lub otwarty dla linii. Główne metody drawPath() łączą Path i Paint do końcowego rysowania.
Wydajność rysowania Canvas bezpośrednio wpływa na FPS aplikacji. Główne problemy: nadmierne alokacje w onDraw(), częste invalidate() i nieoptymalne wykorzystanie Hardware Acceleration.
Paint, Path, Rect, RectF i inne obiekty powinny być tworzone jednorazowo w inicjalizatorze lub metodzie setup(). Każda alokacja w onDraw() obciąża garbage collector, co prowadzi do mikrozacięć. Wyjątkiem są static lub companion object dla obiektów immutable.
Jeśli duża część View jest ukryta poza widocznym obszarem (np. w ScrollView), używaj canvas.clipRect() do ograniczenia rysowania. Hardware Acceleration w tym przypadku dodatkowo optymalizuje przycinanie niewidocznych pikseli.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Przytnij do widocznego obszaru
canvas.save()
canvas.clipRect(scrollX, scrollY, scrollX + width, scrollY + height)
// Renderuj dużą scenę
renderScene(canvas)
canvas.restore()
}
Używaj View.setLayerType() z LAYER_TYPE_HARDWARE dla View ze złożoną grafiką Canvas, która aktualizuje się rzadko. W przypadku często aktualizowanych animacji używaj LAYER_TYPE_NONE, aby uniknąć kosztownej aktualizacji warstwy sprzętowej. Profiluj przez GPU Profile Rendering i Android Studio Profiler.
Często zadawane pytania
Tak, można utworzyć Canvas(Bitmap) — konstruktor przyjmujący Bitmap. Jest to używane do offscreen-renderowania: rysujesz na Bitmap, a następnie wyświetlasz ją przez drawBitmap(). Canvas może być również utworzony z Surface (dla SurfaceView) lub przez Picture.
Canvas — to API do rysowania na View w wątku UI. SurfaceView — to osobny Surface, który może być aktualizowany z wątku tła przez własny Canvas (lockCanvas() / unlockCanvasAndPost()). SurfaceView jest preferowany do gier i wideo.
Najczęstszą przyczyną są niezrównoważone save() i restore(). Jeśli po transformacji (translate, rotate) nie wywołasz restore(), Canvas pozostaje w zmienionym układzie współrzędnych, a kolejne elementy są rysowane z przesunięciem. Sprawdź pary save()/restore().
Używaj Paint.setTextAlign(Paint.Align.CENTER) do wyrównania poziomego oraz Paint.getTextBounds() lub Paint.descent() / ascent() do pionowego. Wyśrodkowanie: canvas.drawText(text, centerX, centerY - (ascent + descent) / 2, paint).
Canvas sam w sobie nie animuje — on tylko rysuje bieżący stan. Animacja osiągana jest przez sekwencyjne wywołania invalidate() (lub postInvalidateOnAnimation()), każdorazowo rysując nową klatkę w onDraw(). Do płynnych animacji używaj ValueAnimator lub Choreographer do synchronizacji z Vsync.
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ż