I03 — Custom view gauge
Draw a gauge by hand: measure, layout, draw, invalidate. A custom view is the shortest path to understanding why a frame costs what it costs, because you write the code that produces it.
A custom view is where the View system stops being a framework you use and becomes code you wrote. The three passes — measure, layout, draw — are not abstractions when you are the one implementing them.
What you are building#
A gauge view that draws an arc, a needle and a value, and animates the needle when the value changes. It has a custom attribute for the maximum value, handles padding, and requests a redraw only when the value actually changes.
Steps#
- Create the view class
Extend
Viewand add the three constructors, including the one that takes anAttributeSetso the view can be inflated from XML.1You should seeThe class compiles and can be placed in a layout. - Read the custom attribute
Declare a custom attribute in
attrs.xml, read it in the constructor, and give it a default. This is the mechanism every library uses for configuration.You should seeThe maximum value comes from the layout. - Measure
Override
onMeasure, compute the desired size from the content plus padding, and callsetMeasuredDimensionwith a value reconciled against the measure spec. A view that ignores the spec breaks its parent's layout.You should seeThe view has a sensible size under wrap_content and match_parent. - Draw
Override
onDraw, create thePaintobjects once (not per frame), and draw the arc and needle. Then draw the value as text, centred in the available space.You should seeAn arc and a needle appear. - Handle padding
Compute the drawing area from the padded bounds rather than the full view bounds. This is the detail that separates a view that works in one layout from one that works anywhere.
You should seeThe drawing respects padding. - Animate the value
Use a
ValueAnimatorand callsetValueon each update. InsetValue, callinvalidate()and return early if the value has not changed: an unnecessary invalidation is an unnecessary frame.You should seeThe needle moves smoothly. - Count the draws
Log in
onDrawand confirm that it runs once per frame. If it runs several times, something else is invalidating the view; a common cause is a layout pass triggered insideonDraw.You should seeOne draw per animation frame, not more. - Compare with Compose
Build a Compose version using
Canvasand an animated state. The phases are analogous: composition replaces measure, but the draw is still a draw. The comparison makes the View system's phases concrete.2You should seeThe same gauge, declaratively.
The skeleton is three overrides:
class GaugeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : View(context, attrs) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val size = resolveSize(desiredSize(), widthMeasureSpec)
setMeasuredDimension(size, size)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// arc, needle, value
}
}adb shell dumpsys gfxinfo com.example.gauge reset
adb shell dumpsys gfxinfo com.example.gauge
adb shell dumpsys gfxinfo com.example.gauge framestats | head -30Example output
Illustrative output — counters depend on your device.Verification#
Rotate the device while the gauge is visible. The view should redraw correctly at the new size, with the padding respected and the value preserved. If it is clipped or off-centre, the measure pass or the padded bounds are wrong, and that is the lesson.
What you learned#
- The three passes are explicit in a custom view: measure decides size, layout decides position, draw produces the pixels.
invalidateschedules, it does not draw. The frame boundary is the Choreographer's vsync.- Allocation in
onDrawcosts frames. Paint objects and shaders are created once. - Padding and measure specs are the difference between a demo and a reusable view.
Read The View system for the tree model, and
The life of a frame for where onDraw sits.
Extend it#
- Add a second needle and an overlapping arc, and watch the draw cost rise without the frame count changing.
- Add a
saveLayerand measure the difference; layers are a common hidden cost. - Make the gauge accessible by exposing the value through
onInitializeAccessibilityNodeInfo.
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsCreate custom viewsdeveloper.android.comaccessed 2026-10-27↩
- docsCompose phasesdeveloper.android.comaccessed 2026-10-27↩
- docsCreate a view layoutdeveloper.android.comaccessed 2026-10-27↩
- docsRendering | App qualitydeveloper.android.comaccessed 2026-10-27↩
- docsCompose performancedeveloper.android.comaccessed 2026-10-27↩