Niestandardowe widoki są podstawą unikalnego interfejsu każdej aplikacji mobilnej. Niestandardowe widoki w aplikacjach mobilnych pozwalają tworzyć własną grafikę, animacje i elementy niedostępne w standardowych komponentach. Według Apple Developer, 2025, drawRect pozostaje główną metodą niestandardowego rysowania w mobilnym rozwoju iOS. Na Androidzie tę samą rolę pełni onDraw z Canvas, a we Flutter — CustomPainter.
Najważniejsze
Niestandardowe widoki to komponenty interfejsu zdefiniowane przez użytkownika, które nadpisują standardowe metody rysowania i pomiaru w celu tworzenia unikalnej grafiki. Wykresy, animacje, diagramy, niestandardowe elementy czatu i maski AR — to typowe scenariusze użycia niestandardowych widoków w aplikacjach mobilnych. Standardowe komponenty UI (Button, TextView, Label) pokrywają 80% zadań, ale pozostałe 20% wymaga pełnej kontroli nad rysowaniem w rozwoju mobilnym. Rysowanie w rozwoju mobilnym to umiejętność, która odróżnia programistę juniora od seniora.
Niestandardowe widoki w aplikacjach mobilnych są potrzebne do unikalnej grafiki 2D, złożonych animacji, wykresów i wizualizacji danych. Jeśli standardowy UIKit nie zapewnia wymaganego elementu — tworzony jest niestandardowy widok. W aplikacjach mobilnych niestandardowe widoki są używane do pasków postępu, niestandardowych przycisków, wypełnień gradientowych, animowanych ikon i kanw do rysowania palcem. Ważne jest, aby nie nadużywać — do prostych stylów wystarczy skonfigurować standardowe komponenty przez API wyglądu.
Na iOS niestandardowe widoki są tworzone przez dziedziczenie po UIView i nadpisanie metody drawRect. drawRect jest wywoływany przy pierwszym renderowaniu i po każdym wywołaniu setNeedsDisplay(). Wewnątrz drawRect programista otrzymuje CGContext przez UIGraphicsGetCurrentContext() — płótno dla linii, kształtów, gradientów i tekstu. Core Graphics (Quartz 2D) to niskopoziomowy silnik renderowania na CPU, który daje pełną kontrolę nad każdym pikselem. UIBezierPath to wygodna nakładka do tworzenia ścieżek ze zwięzłą składnią.
class CircleView: UIView {
private var fillColor: UIColor = .systemBlue
override func drawRect(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.setFillColor(fillColor.cgColor)
ctx.addEllipse(in: bounds.insetBy(dx: 4, dy: 4))
ctx.drawPath(using: .fill)
}
func updateColor(_ newColor: UIColor) {
fillColor = newColor
setNeedsDisplay()
}
}
Core Graphics udostępnia CGContext — płótno dla wszystkich operacji rysowania. CGGradient tworzy gradienty liniowe i radialne z obsługą CGColorSpace (sRGB, Display P3). Transformacje są wykonywane przez CGAffineTransform — macierz 3x3 do obrotu, skalowania i przesunięcia. CGContextTranslateCTM, CGContextRotateCTM i CGContextScaleCTM modyfikują układ współrzędnych. Core Graphics działa na CPU, więc złożona grafika może obciążać procesor — użyj Metal do akceleracji GPU.
UIBezierPath to obiektowa nakładka na CGPath. Metoda addArc(withCenter:radius:startAngle:endAngle:clockwise:) tworzy łuki i okręgi, addCurve tworzy krzywe Béziera. Ścieżki obsługują operacje union, intersection i subtraction przez addClip. UIBezierPath jest wygodny do niestandardowych masek (maskView), ścieżek przycinania i złożonych kształtów — gwiazd, serc, niestandardowych ikon.
Na Androidzie niestandardowy widok rozszerza View z nadpisanym onDraw(Canvas). Canvas zawiera wszystkie metody rysowania w rozwoju mobilnym: drawCircle, drawRect, drawLine, drawText, drawBitmap, drawPath i drawRoundRect. Paint konfiguruje parametry — kolor, grubość linii, styl (FILL, STROKE, FILL_AND_STROKE), wygładzanie (isAntiAlias) i rozmiar tekstu. onDraw jest wywoływany przy każdym invalidate() i nie powinien zawierać ciężkich operacji — wszystkie obliczenia są buforowane.
class CustomCircleView(context: Context, attrs: AttributeSet?)
: View(context, attrs) {
private val paint = Paint().apply {
style = Paint.Style.FILL
color = Color.BLUE
isAntiAlias = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val cx = width / 2f
val cy = height / 2f
val radius = minOf(cx, cy) - 4f
canvas.drawCircle(cx, cy, radius, paint)
}
}
Path na Androidzie jest odpowiednikiem UIBezierPath: addCircle, addRect, addOval, lineTo, cubicTo i quadTo. PathMeasure oblicza długość ścieżki i współrzędne w dowolnym punkcie — przydatne do animowania ruchu obiektu wzdłuż ścieżki. Do animacji używa się ValueAnimator z invalidate() w callbacku. Animator może zmieniać kolor, promień, pozycję — dowolną właściwość wpływającą na rysowanie. Niestandardowe widoki na Androidzie obsługują również StateListDrawable i animated-vector do animacji przez XML.
Gradienty na Androidzie są ustawiane przez Shader — LinearGradient, RadialGradient i SweepGradient. Shader jest przypisywany do Paint przez paint.setShader(shader). LinearGradient generuje gradient liniowy między dwoma punktami z opcjonalnymi pozycjami kolorów. RadialGradient — radialny od środka do krawędzi. SweepGradient — stożkowy wokół punktu środkowego. Shader.TileMode (CLAMP, REPEAT, MIRROR) określa zachowanie poza granicami obszaru.
Flutter używa CustomPainter — klasy z metodami paint(Canvas canvas, Size size) i shouldRepaint. Canvas we Flutter zapewnia te same metody co w Androidzie: drawCircle, drawRect, drawLine, drawPath, drawParagraph i drawImage. Paint we Flutter konfiguruje kolor, grubość, styl i wygładzanie. Flutter nie używa drawRect ani onDraw — całe rysowanie odbywa się przez CustomPainter, osadzony w widgecie CustomPaint.
class CirclePainter extends CustomPainter {
final Color color;
CirclePainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill
..isAntiAlias = true;
final radius = size.shortestSide / 2 - 4;
canvas.drawCircle(size.center(Offset.zero), radius, paint);
}
@override
bool shouldRepaint(CirclePainter oldDelegate) =>
oldDelegate.color != color;
}
shouldRepaint określa, czy CustomPainter wymaga przerysowania. Zwracaj true tylko wtedy, gdy zmieniają się dane wpływające na renderowanie — kolor, rozmiar, dane wykresu. RepaintBoundary izoluje przerysowanie w oddzielną warstwę — używaj dla widgetów, które rzadko się zmieniają. Flutter nie ma setNeedsDisplay ani invalidate — przerysowanie jest wyzwalane przez setState lub ValueNotifier z nasłuchem zmian.
Flutter obsługuje LinearGradient, RadialGradient i SweepGradient — te same typy co w Androidzie. Gradient tworzy Shader dla Paint przez createShader(Rect bounds). Transformacje są wykonywane przez canvas.rotate, canvas.scale, canvas.translate i canvas.skew. Flutter używa macierzy transformacji 4x4 (Matrix4) dla widgetu Transform. Animacje rysowania we Flutter są tworzone przez AnimationController w połączeniu z setState do przerysowania CustomPainter.
Prawidłowy pomiar niestandardowych widoków jest podstawą przewidywalnego zachowania interfejsu. Na iOS rozmiar widoku jest określany przez sizeThatFits i intrinsicContentSize. sizeThatFits zwraca optymalny rozmiar dla danych ograniczeń. intrinsicContentSize — naturalny rozmiar treści bez zewnętrznych ograniczeń. Na Androidzie pomiar jest implementowany w onMeasure z parametrami MeasureSpec (UNSPECIFIED, EXACTLY, AT_MOST). We Flutter rozmiar jest przekazywany do paint(Canvas, Size) i może być obliczony przez computeDryLayout lub LayoutBuilder.
intrinsicContentSize zwraca CGSize — naturalny rozmiar widoku na podstawie jego treści. setContentHuggingPriority i setContentCompressionResistancePriority kontrolują zachowanie podczas rozciągania i ściskania. sizeThatFits jest wywoływany przez system do obliczenia rozmiaru w Auto Layout. Dla niestandardowych widoków nadpisz intrinsicContentSize, jeśli twój komponent ma naturalny rozmiar — na przykład okrąg powinien być zawsze 100x100.
onMeasure przyjmuje widthMeasureSpec i heightMeasureSpec. MeasureSpec zawiera tryb i rozmiar: UNSPECIFIED (bez ograniczeń, widok sam wybiera rozmiar), EXACTLY (dokładny rozmiar od rodzica), AT_MOST (maksymalny rozmiar). setMeasuredDimension(width, height) zapisuje wynik pomiaru. Przy zmianie danych wywołaj requestLayout() do ponownego pomiaru. Zagnieżdżone ViewGroup wymagają nadpisania onLayout do rozmieszczenia elementów potomnych.
| Platforma | Metoda pomiaru | Wywołanie przy zmianie | Zwrot |
|---|---|---|---|
| iOS | sizeThatFits / intrinsicContentSize | setNeedsLayout / invalidateIntrinsicContentSize | CGSize |
| Android | onMeasure / setMeasuredDimension | requestLayout / invalidate | void (zapisuje rozmiar) |
| Flutter | computeDryLayout | setState / LayoutBuilder | Size |
computeDryLayout to metoda RenderObject do obliczania rozmiaru bez pełnego cyklu układu. LayoutBuilder.getSize — alternatywa do uzyskania dostępnej przestrzeni w metodzie build. We Flutter rozmiar płótna jest przekazywany do paint jako Size i nie wymaga oddzielnego pomiaru — CustomPainter rysuje w przekazanych granicach. Jednak dla niestandardowych RenderObject należy nadpisać performLayout i computeDryLayout.
Często zadawane pytania
drawRect to metoda UIView do niestandardowego rysowania przez Core Graphics. Jest wywoływana przy pierwszym renderowaniu widoku i po każdym wywołaniu setNeedsDisplay(). Użyj CGContext wewnątrz drawRect dla linii, kształtów i tekstu.
Utwórz klasę rozszerzającą View, nadpisz onDraw(Canvas canvas) i użyj Canvas API: drawCircle, drawRect, drawPath. Paint konfiguruje kolor, grubość i styl. Do aktualizacji wywołaj invalidate().
CustomPainter to klasa Flutter do niestandardowego rysowania na Canvas. Zaimplementuj metody paint(Canvas, Size) i shouldRepaint. Jest umieszczany przez widget CustomPaint. Flutter nie ma drawRect — całe rysowanie odbywa się przez CustomPainter.
Na iOS użyj sizeThatFits i intrinsicContentSize. Na Androidzie — onMeasure z MeasureSpec. We Flutter rozmiar jest przekazywany do paint(Canvas, Size) lub obliczany przez computeDryLayout. Każda platforma używa własnej metody pomiaru.
Niestandardowe widoki są potrzebne do unikalnej grafiki, animacji, wykresów i elementów, których nie można zaimplementować za pomocą standardowego UIKit lub View. Do prostych stylów wystarczy skonfigurować standardowe komponenty — nie komplikuj architektury bez potrzeby.
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.