The View system
A View is a rectangle that measures itself, lays out its children and records drawing commands. The whole UI of Android before Compose is that one idea, applied recursively.
A is about as small an abstraction as a UI toolkit can have: an object with a rectangle, a
draw(Canvas), and a onTouchEvent(MotionEvent). Everything else — layouts, lists, text input — is
built from that by composition.
TL;DR#
- A View occupies a rectangle and knows how to draw itself and handle input.
- A is a View that contains other Views and positions them. Layouts (
LinearLayout,FrameLayout) are ViewGroups. - The tree goes through measure → layout → draw on every frame that needs it, on the main thread.1
- Drawing records a display list;
RenderThreadreplays it. The main thread does not rasterise.1 invalidate()redraws;requestLayout()re-measures. They are different costs, and confusing them is how screens become slow.2
Mental model#
A View tree is a set of nested boxes drawn on a wall.
Each box says "here is how big I would like to be, given the space you offered me" (measure), then "here is where my children actually go" (layout), then "here is what I paint" (draw). A parent asks its children first, then decides.
Where the analogy breaks#
The boxes do not just paint — they record instructions for a separate painter to execute later. That recording is cheap and can be replayed many times; it is also why a view's drawing can be cached and reused by its siblings. Nothing in a nested-box analogy prepares you for the fact that drawing is deferred and parallel.
The big picture#
Text description of this diagram
A walkthrough of one frame in the View system. It begins with invalidate or requestLayout setting dirty flags, then Choreographer runs the traversal on the next vsync. The measure pass goes top-down: each parent measures children with MeasureSpec constraints and then chooses its own size. The layout pass assigns positions from those sizes. The draw pass records display-list commands into RenderNodes rather than rasterising. Finally, sync hands the recorded display list to RenderThread, which replays it into GPU commands independently of the UI thread. A frame-budget bar shows that the sum of these passes must fit inside 16.7 ms at 60 Hz.
Step through it and watch the arrow between the app's process and RenderThread at the sync stage.
That handoff is the single biggest architectural decision in the View system.
Deep dive#
A View's three jobs#
class SparklineView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = context.getColor(R.color.accent)
strokeWidth = 2f
style = Paint.Style.STROKE
}
private val path = Path()
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val w = resolveSize(suggestedMinimumWidth, widthMeasureSpec)
val h = resolveSize(suggestedMinimumHeight, heightMeasureSpec)
setMeasuredDimension(w, h)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Build a path from the data and draw it. No allocation in a real version.
canvas.drawPath(path, paint)
}
}onMeasure receives two MeasureSpecs and calls setMeasuredDimension. onDraw receives a Canvas
that records commands. Neither may block, allocate heavily, or touch another thread.
MeasureSpec: the constraint language#
A MeasureSpec packs a mode and a size into one int:
| Mode | Meaning | Child's response |
|---|---|---|
EXACTLY | you must be this size | use the size |
AT_MOST | you may be at most this size | choose a size up to it |
UNSPECIFIED | no constraint | choose whatever you need |
val w = resolveSize(desiredWidth, widthMeasureSpec) // honours the mode for youresolveSize is the helper most custom views should use. Writing the when by hand is fine, but it
is where the classic bug lives: a view that ignores AT_MOST and returns its desired width overflows
its parent and gets clipped.
A measure pass is two-phase in a subtle way: a parent may measure a child twice — once to find out
how big it wants to be, once to decide the final layout. LinearLayout with layout_weight does
exactly that, and it is why deeply nested weights are expensive.
Layout and draw#
onLayout assigns each child's rectangle:
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var y = paddingTop
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child.visibility == GONE) continue
val h = child.measuredHeight
child.layout(paddingLeft, y, r - paddingRight, y + h)
y += h
}
}onDraw records into a RenderNode associated with the view. The framework caches that recording, so
a sibling's invalidation does not force this view to redraw unless its own bounds intersect the dirty
region.
// Redraw only: cheap, no re-measure.
invalidate()
// Re-measure the subtree: expensive, may cascade upward.
requestLayout()That distinction is the single most useful performance fact about the View system. Animating a
translation does not need requestLayout; changing a child's size does.
The dispatch of input#
Touch events go down the tree from the root:3
dispatchTouchEventon the root.onInterceptTouchEventon each ViewGroup, top-down.onTouchEventon the target view, then back up if unhandled.
A ViewGroup that intercepts steals the gesture from its children — that is how a scrolling parent takes over from a child that started handling a drag. Getting this wrong produces the classic "the list scrolls instead of the item dragging" bug.
Why the View system got hard#
The core is small. The difficulty is emergent:
- State lives in the views. Text is in a
TextView, selection in anEditText, scroll in aRecyclerView. Restoring after rotation means finding every piece. - Mutation is imperative.
setText,setVisibility,notifyItemChanged. There is no single place that describes what the screen should look like. - The tree is mutable. Views are added and removed, recycled and re-bound. Every mutation is a chance to forget one.
- Inflation is reflective. XML is parsed, reflected into constructors, and styled at runtime — cost paid on the main thread at startup.
Those four properties are exactly what Compose was designed to remove. They are also why the View system remains perfectly usable for simple, static screens.
Recycling: the View system's best idea#
RecyclerView does not create a view per item. It keeps a pool of scrap views, binds data into them as
they scroll into view, and recycles them as they leave.4 That is the pattern Compose adopted for
composition slots rather than views.
class NoteAdapter : ListAdapter<Note, NoteViewHolder>(DIFF) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
val binding = ItemNoteBinding.inflate(
LayoutInflater.from(parent.context), parent, false,
)
return NoteViewHolder(binding)
}
override fun onBindViewHolder(holder: NoteViewHolder, position: Int) {
holder.bind(getItem(position))
}
companion object {
private val DIFF = object : DiffUtil.ItemCallback<Note>() {
override fun areItemsTheSame(a: Note, b: Note) = a.id == b.id
override fun areContentsTheSame(a: Note, b: Note) = a == b
}
}
}ListAdapter plus DiffUtil is the modern minimum: it computes the minimal set of changes and
animates them, instead of notifyDataSetChanged() rebuilding everything.
Inflation, and what it costs#
setContentView(R.layout.activity_notes) is not a file read. It is:
- Parse the compiled XML (already binary, in the APK).
- For each element, reflectively find the constructor
(Context, AttributeSet). - Resolve every attribute, including theme attributes and style references.
- Apply the style, walking the theme's parent chain.
That is why the first frame of a screen with a deep layout is measurably slower than the second, and
why ViewStub and lazy inflation exist. It is also the cost that Compose removes — at the price of
building a composition instead. Neither is free; they are different shapes of the same work.
Accessibility and the tree shape#
The view tree is also the accessibility tree, and its shape determines how a screen reader reads the
screen. A LinearLayout containing five TextViews is five nodes; a RelativeLayout that visually
groups them is still five nodes, read in hierarchy order.
Two techniques change the shape:
<!-- Read the card's children as one unit. -->
<LinearLayout
android:focusable="true"
android:screenReaderFocusable="true"
android:importantForAccessibility="yes">screenReaderFocusable groups descendants into a single focus stop, which is what a card with a title
and a subtitle usually wants. Setting importantForAccessibility="no" on a decorative container removes
it from the tree without removing its children.
The tree shape also decides keyboard focus order, which matters on tablets and desktops. A layout
whose visual order differs from its declaration order will have a confusing focus order, and the fix is
to restructure rather than to add nextFocusForward overrides.
The cost model, in one paragraph#
A view costs: one object, one measure call per layout pass, one layout call, one display-list
recording, and one RenderNode in the render tree. A layout pass costs O(depth × children) in the worst
case, and a full redraw costs O(visible nodes). Nothing in that model is surprising, which is its
virtue: a slow View screen can usually be predicted by looking at the tree depth and the work in
onDraw. Compose's model is more precise but harder to predict without tooling.
What the View system still does better#
Two things remain genuinely easier with Views, and it is worth knowing them rather than treating the toolkit as obsolete:
Precise custom drawing with a stable API. Canvas, Paint and Path have not changed
fundamentally in fifteen years. A chart, a signature pad or a game surface written against them will
keep working.
Embedding in existing infrastructure. RemoteViews, SurfaceView, TextureView and the
WebView all assume a View host. Compose can be embedded in them and they can be embedded in Compose,
but the boundary always costs something.
Measuring the View system#
dumpsys gfxinfo reports per-frame timings for the app. The columns that matter are Draw,
Prepare, Process and Execute — and in modern Android, FrameTimeline gives the same data as a
system trace with deadline information.
adb shell dumpsys gfxinfo com.example.notes framestatsIf Draw or Prepare is consistently above a few milliseconds, the main thread is doing too much
work during the traversal. That is not a rendering problem; it is an app-code problem.
How it evolved#
| API | Version | What changed |
|---|---|---|
| 1 | 1.0 |
|
| 3 | 1.5 Cupcake |
|
| 9 | 2.3 Gingerbread |
|
| 11 | 3.0 Honeycomb |
|
| 12 | 3.1 Honeycomb |
|
| 16 | 4.1 Jelly Bean |
|
| 20 | 4.4W KitKat (Wear) |
|
| 21 | 5.0 Lollipop |
|
| 24 | 7.0 Nougat |
|
| 26 | 8.0 Oreo |
|
| 28 | 9 Pie |
|
| 29 | 10 |
|
| 30 | 11 |
|
| 31 | 12 |
|
| 33 | 13 Tiramisu |
|
| 36 | 16 Baklava |
|
The View system's architecture has been stable since API 1. What changed: hardware acceleration (API 11, default from 14), vsync-driven scheduling (4.1), and RenderThread (5.0). Each change moved work off the main thread without changing the programming model.
Under the hood#
- View.javathe measure/layout/draw contract and the invalidate flags
- ViewGroup.javachild dispatch, intercept and the traversal
- ViewRootImpl.javathe bridge between the view tree and WindowManager
- the native renderer: RenderNode, display lists, RenderThread
Curiosity corner#
Viewis one of the largest classes in the framework. Most of it is the accumulated compatibility surface of fifteen years of API additions.- A view can be drawn without being attached.
View.draw(Canvas)works on a detached view, which is how screenshot tests render a layout off-screen. - Hardware acceleration was opt-in in API 11 and default from API 14, because early hardware
paths could not render every
Canvasoperation correctly.
Try it#
adb shell dumpsys gfxinfo com.example.notes framestats
adb shell dumpsys gfxinfo com.example.notes reset
Check yourself#
Check yourself
What is the difference between `invalidate()` and `requestLayout()`?
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.
- docsRendering | App qualitydeveloper.android.comaccessed 2026-10-27↩
- docsการแสดงผลช้า | App qualitydeveloper.android.comaccessed 2026-10-27↩
- docsHandle input eventsdeveloper.android.comaccessed 2026-10-27↩
- docsCreate a view layoutdeveloper.android.comaccessed 2026-10-27↩
- docsCreate custom viewsdeveloper.android.comaccessed 2026-10-27↩
- docsInspect GPU rendering speeddeveloper.android.comaccessed 2026-10-27↩