Zygote
UI toolkitsbeginner

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.

11 minAPI 1–37 · 17interactivequizverified 2026-10-27

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; RenderThread replays 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#

Loading diagram…
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:

ModeMeaningChild's response
EXACTLYyou must be this sizeuse the size
AT_MOSTyou may be at most this sizechoose a size up to it
UNSPECIFIEDno constraintchoose whatever you need
val w = resolveSize(desiredWidth, widthMeasureSpec)   // honours the mode for you

resolveSize 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

  1. dispatchTouchEvent on the root.
  2. onInterceptTouchEvent on each ViewGroup, top-down.
  3. onTouchEvent on 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 an EditText, scroll in a RecyclerView. 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:

  1. Parse the compiled XML (already binary, in the APK).
  2. For each element, reflectively find the constructor (Context, AttributeSet).
  3. Resolve every attribute, including theme attributes and style references.
  4. 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 framestats

If 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#

Interface changes16 releases
APIVersionWhat changed
11.0
  • Pull-down notification shade
  • Home screen with widgets
31.5 Cupcake
  • Home screen widgets and folders
92.3 Gingerbread
  • New UI with dark theme and green accents
113.0 Honeycomb
  • The Holo design language arrives
123.1 Honeycomb
  • Resizable widgets
164.1 Jelly Bean
  • Project Butter: vsync-driven, smooth UI
  • Expandable notifications
204.4W KitKat (Wear)
  • Voice-driven card UI
215.0 Lollipop
  • Material Design
  • Notifications on the lock screen
247.0 Nougat
  • Quick Settings tiles
  • Direct Reply in notifications
268.0 Oreo
  • Notification channels
289 Pie
  • Dark theme groundwork
2910
  • System-wide dark theme
3011
  • Conversation notifications and bubbles
  • Media controls in Quick Settings
3112
  • Material You dynamic colour
3313 Tiramisu
  • Themed app icons
  • Runtime notification permission
3616 Baklava
  • Material 3 Expressive
  • Live Updates notifications

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#

Curiosity corner#

  • View is 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 Canvas operation correctly.

Try it#

Per-frame timing for an app

adb shell dumpsys gfxinfo com.example.notes framestats

Reset the frame stats, then use the app

adb shell dumpsys gfxinfo com.example.notes reset

Check yourself#

Check yourself

1 / 12

What is the difference between `invalidate()` and `requestLayout()`?

0 answered · 0 correct

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.

  1. docsRendering | App qualitydeveloper.android.comaccessed 2026-10-27↩
  2. docsการแสดงผลช้า | App qualitydeveloper.android.comaccessed 2026-10-27↩
  3. docsHandle input eventsdeveloper.android.comaccessed 2026-10-27↩
  4. docsCreate a view layoutdeveloper.android.comaccessed 2026-10-27↩
  5. docsCreate custom viewsdeveloper.android.comaccessed 2026-10-27↩
  6. docsInspect GPU rendering speeddeveloper.android.comaccessed 2026-10-27↩