Custom Views are the foundation of any mobile app's unique interface. Custom Views in mobile apps let you create custom graphics, animations, and elements not available in standard components. According to Apple Developer, 2025, drawRect remains the primary custom drawing method in iOS mobile development. On Android, onDraw with Canvas serves the same role, while Flutter uses CustomPainter.
Key Takeaways
Custom Views are user-defined interface components that override standard drawing and measurement methods to create unique graphics. Charts, animations, diagrams, custom chat elements, and AR masks — these are typical use cases for Custom Views in mobile apps. Standard UI components (Button, TextView, Label) cover 80% of tasks, but the remaining 20% require full control over drawing in mobile development. Drawing in mobile development is a skill that distinguishes junior developers from seniors.
Custom Views in mobile apps are needed for unique 2D graphics, complex animations, charts, and data visualization. If the standard UIKit doesn't provide the required element — a custom View is created. In mobile apps, custom Views are used for progress bars, custom buttons, gradient fills, animated icons, and finger-drawing canvases. It's important not to overuse them — for simple styling, configuring standard components via the Appearance API is sufficient.
On iOS, custom Views are created by subclassing UIView and overriding the drawRect method. drawRect is called on first render and after each setNeedsDisplay() call. Inside drawRect, the developer gets a CGContext via UIGraphicsGetCurrentContext() — a canvas for lines, shapes, gradients, and text. Core Graphics (Quartz 2D) is a low-level CPU-based rendering engine that gives full control over every pixel. UIBezierPath is a convenient wrapper for creating paths with concise syntax.
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 provides CGContext — a canvas for all drawing operations. CGGradient creates linear and radial gradients with CGColorSpace support (sRGB, Display P3). Transformations are performed via CGAffineTransform — a 3x3 matrix for rotation, scaling, and translation. CGContextTranslateCTM, CGContextRotateCTM, and CGContextScaleCTM modify the coordinate system. Core Graphics runs on the CPU, so complex graphics can load the processor — use Metal for GPU acceleration.
UIBezierPath is an object-oriented wrapper over CGPath. The addArc(withCenter:radius:startAngle:endAngle:clockwise:) method creates arcs and circles, addCurve creates Bézier curves. Paths support union, intersection, and subtraction operations via addClip. UIBezierPath is convenient for custom masks (maskView), clipping paths, and complex shapes — stars, hearts, custom icons.
On Android, a custom View extends View with onDraw(Canvas) overridden. Canvas contains all drawing methods in mobile development: drawCircle, drawRect, drawLine, drawText, drawBitmap, drawPath, and drawRoundRect. Paint configures parameters — color, line width, style (FILL, STROKE, FILL_AND_STROKE), anti-aliasing (isAntiAlias), and text size. onDraw is called each time on invalidate() and should not contain heavy operations — all computations are cached.
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 on Android is the equivalent of UIBezierPath: addCircle, addRect, addOval, lineTo, cubicTo, and quadTo. PathMeasure calculates path length and coordinates at any point — useful for animating object movement along a path. For animations, use ValueAnimator with invalidate() in the callback. Animator can change color, radius, position — any property affecting drawing. Custom Views on Android also support StateListDrawable and animated-vector for animation via XML.
Gradients on Android are set via Shader — LinearGradient, RadialGradient, and SweepGradient. Shader is assigned to Paint via paint.setShader(shader). LinearGradient generates a linear gradient between two points with optional color positions. RadialGradient — radial from center to edges. SweepGradient — conical around a center point. Shader.TileMode (CLAMP, REPEAT, MIRROR) defines behavior beyond the area bounds.
Flutter uses CustomPainter — a class with paint(Canvas canvas, Size size) and shouldRepaint methods. Canvas in Flutter provides the same methods as in Android: drawCircle, drawRect, drawLine, drawPath, drawParagraph, and drawImage. Paint in Flutter configures color, thickness, style, and anti-aliasing. Flutter does not use drawRect or onDraw — all drawing goes through CustomPainter, embedded in the CustomPaint widget.
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 determines whether the CustomPainter needs to redraw. Return true only when data affecting rendering changes — color, size, chart data. RepaintBoundary isolates redrawing into a separate layer — use for widgets that rarely change. Flutter has no setNeedsDisplay or invalidate — redrawing is triggered via setState or ValueNotifier with change listeners.
Flutter supports LinearGradient, RadialGradient, and SweepGradient — the same types as in Android. Gradient creates a Shader for Paint via createShader(Rect bounds). Transformations are performed via canvas.rotate, canvas.scale, canvas.translate, and canvas.skew. Flutter uses a 4x4 transformation matrix (Matrix4) for the Transform widget. Drawing animations in Flutter are done via AnimationController combined with setState to redraw CustomPainter.
Correct measurement of Custom Views is the foundation of predictable interface behavior. On iOS, View size is determined via sizeThatFits and intrinsicContentSize. sizeThatFits returns the optimal size for given constraints. intrinsicContentSize — the natural content size without external constraints. On Android, measurement is implemented in onMeasure with MeasureSpec parameters (UNSPECIFIED, EXACTLY, AT_MOST). In Flutter, size is passed to paint(Canvas, Size) and can be calculated via computeDryLayout or LayoutBuilder.
intrinsicContentSize returns a CGSize — the natural size of the View based on its content. setContentHuggingPriority and setContentCompressionResistancePriority control behavior during stretching and compression. sizeThatFits is called by the system to compute size in Auto Layout. For custom Views, override intrinsicContentSize if your component has a natural size — for example, a circle should always be 100x100.
onMeasure accepts widthMeasureSpec and heightMeasureSpec. MeasureSpec contains mode and size: UNSPECIFIED (no constraints, View chooses its own size), EXACTLY (exact size from parent), AT_MOST (maximum size). setMeasuredDimension(width, height) saves the measurement result. When data changes, call requestLayout() for re-measurement. Nested ViewGroups require overriding onLayout to arrange child elements.
| Platform | Measurement Method | Call on Change | Returns |
|---|---|---|---|
| iOS | sizeThatFits / intrinsicContentSize | setNeedsLayout / invalidateIntrinsicContentSize | CGSize |
| Android | onMeasure / setMeasuredDimension | requestLayout / invalidate | void (saves size) |
| Flutter | computeDryLayout | setState / LayoutBuilder | Size |
computeDryLayout is a RenderObject method for computing size without a full layout cycle. LayoutBuilder.getSize — an alternative for getting available space in the build method. In Flutter, the canvas size is passed to paint as Size and doesn't require separate measurement — CustomPainter draws within the passed bounds. However, for custom RenderObjects, you need to override performLayout and computeDryLayout.
Frequently Asked Questions
drawRect is a UIView method for custom drawing via Core Graphics. It is called on the first render of the View and after each setNeedsDisplay() call. Use CGContext inside drawRect for lines, shapes, and text.
Create a class extending View, override onDraw(Canvas canvas), and use the Canvas API: drawCircle, drawRect, drawPath. Paint configures color, thickness, and style. Use invalidate() to trigger updates.
CustomPainter is a Flutter class for custom drawing on Canvas. Implement the paint(Canvas, Size) and shouldRepaint methods. It is placed via the CustomPaint widget. Flutter has no drawRect — all drawing goes through CustomPainter.
On iOS use sizeThatFits and intrinsicContentSize. On Android — onMeasure with MeasureSpec. In Flutter, size is passed to paint(Canvas, Size) or calculated via computeDryLayout. Each platform uses its own measurement method.
Custom Views are needed for unique graphics, animations, charts, and elements that cannot be implemented with standard UIKit or View components. For simple styling, configuring standard components is sufficient — don't complicate the architecture unnecessarily.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.