View Lifecycle — what is it, onMeasure onLayout onDraw processes

Author: IT Sectr Published: 2026-03-05 Reading time: 10 min

View Lifecycle — the sequence of methods that Android calls to draw and redraw a user interface element (View) on screen. Unlike Activity or Fragment, View is a lightweight component that does not have an extended lifecycle, but goes through a strict three-phase process: onMeasure (measurement), onLayout (positioning), onDraw (drawing). Understanding View Lifecycle is necessary for creating custom Views, optimizing performance, and solving drawing issues. According to Google, custom Views speed up UI by 15–40% compared to a combination of standard nested ViewGroups when properly implemented. Android documentation on custom Views describes onMeasure, onLayout and onDraw as the three pillars of View Lifecycle.

Key Takeaways

  • View Lifecycle consists of three phases: onMeasure (sizes), onLayout (positions), onDraw (drawing) — and is triggered by invalidate() or requestLayout().
  • onMeasure calculates the width and height of a View based on MeasureSpec (AT_MOST, EXACTLY, UNSPECIFIED).
  • onLayout arranges child Views inside a ViewGroup, determining their left, top, right, bottom coordinates.
  • onDraw renders the View content onto Canvas: background, text, shapes, images.
  • Incorrect View Lifecycle is the main cause of UI performance problems (jank, dropped frames) and hierarchy issues.

View Lifecycle — what is it in Android

View Lifecycle is the process that an Android View (and ViewGroup) goes through to display itself on screen. Unlike Activity or Fragment, View does not have onStart/onStop/onDestroy — its “life” consists of a cyclic process of measuring, positioning, and drawing. This cycle is triggered every time a View needs to be displayed or redrawn.

The three phases of View Lifecycle:

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec) — determines the desired dimensions of the View. The system passes MeasureSpec — an instruction about which dimensions are allowed (exact value, maximum, or unrestricted).
  • onLayout(boolean changed, int left, int top, int right, int bottom) — positions the View and its children on screen. For a View, it defines its own boundaries; for a ViewGroup, it positions child elements.
  • onDraw(Canvas canvas) — draws the View content onto the provided Canvas. The system provides a Canvas that translates commands into bitmap or GPU.

The full View Lifecycle cycle also includes methods related to attaching a View to a window: onAttachedToWindow (View is attached to a window, has HW acceleration) and onDetachedFromWindow (View is detached, resources are freed). These methods are called once per View lifetime and are important for registering/canceling animations and sensors.

According to the Android Performance Blog, 65% of UI performance problems (jank, frame drops) are related to incorrect implementation of onMeasure and onDraw: excessive overriding, calling requestLayout() unnecessarily, creating objects in onDraw.

onMeasure: measuring View dimensions

onMeasure — the most important and most complex phase of View Lifecycle. At this stage, Android determines how much space the View will occupy on screen. The system passes MeasureSpec — int-packed instructions consisting of a mode and a size.

The three MeasureSpec modes:

ModeConstantMeaningExample
EXACTLYMeasureSpec.EXACTLYExact size set by the parent (match_parent or fixed width)width=400dp → MeasureSpec(400, EXACTLY)
AT_MOSTMeasureSpec.AT_MOSTView can be up to the specified maximum size (wrap_content)width ≤ 400dp → MeasureSpec(400, AT_MOST)
UNSPECIFIEDMeasureSpec.UNSPECIFIEDNo restrictions — View can be any size (ScrollView, RecyclerView)width unlimited → MeasureSpec(0, UNSPECIFIED)

onMeasure implementation should:

  • Call setMeasuredDimension(int width, int height) to save the measured dimensions.
  • Account for padding — subtract getPaddingLeft() + getPaddingRight() from the available width.
  • For ViewGroup — measure all children via measureChild() or measureChildWithMargins().
  • For wrap_content — calculate size based on content (text, image).
  • Do not call requestLayout() inside onMeasure — this will cause an infinite loop.

Typical mistake: not accounting for MeasureSpec when using wrap_content. If a View is set to wrap_content, but onMeasure does not handle AT_MOST and returns a fixed size, the View will either be clipped or take more space than needed.

onLayout: placing Views on screen

onLayout — the phase in which a View or ViewGroup arranges its children within its boundaries. For a regular View (not a ViewGroup), onLayout is not required — the system calls layout() with parameters passed from the parent. For a ViewGroup, onLayout is mandatory — without it, child Views will not be placed.

onLayout signature:

java
@Override
protected void onLayout(boolean changed,
        int left, int top,
        int right, int bottom) {
    // arranging child Views
}

The changed parameter indicates whether the View’s position or size has changed compared to the previous layout. If false, the View can skip recalculating child positions for optimization.

For ViewGroup, onLayout should:

  • Iterate through all children via getChildCount() and getChildAt(i).
  • For each child, determine left, top, right, bottom — coordinates within the ViewGroup (accounting for padding).
  • Call child.layout(l, t, r, b) for each child.
  • Account for gravity, margins, alignment.

onLayout is called after onMeasure — measured dimensions are available via getMeasuredWidth()/getMeasuredHeight(). If a child View has different actual dimensions after layout(), requestLayout() will be called for remeasuring. This is called a “layout pass” and can trigger a chain reaction of recalculations.

onDraw: drawing Canvas content

onDraw — the phase in which a View draws itself on Canvas. This is the only phase that can be called multiple times without onMeasure and onLayout — if the View is marked as invalidate(). Canvas provides drawing API: drawLine, drawRect, drawCircle, drawText, drawBitmap, and drawPath.

onDraw rules:

  • Do not create objects in onDraw — each onDraw call should use pre-created objects (Path, Paint, Rect). Creating objects in onDraw causes GC pauses and dropped frames.
  • Do not call requestLayout() or invalidate() inside onDraw — this will trigger an infinite redraw loop.
  • Do not perform lengthy computations — onDraw runs on the UI thread. Complex calculations should be moved to a background thread or pre-calculated.
  • Use Hardware Acceleration — since API 14+, Canvas can work via GPU. For complex graphics (gradients, shadows, rotations), HW acceleration provides up to 300% performance improvement.
  • Draw only the visible area — use canvas.clipRect() to clip invisible parts.

Drawing order in ViewGroup: background (setBackgroundDrawable) → onDraw (content) → dispatchDraw (child Views) → onDrawForeground (foreground). dispatchDraw calls onDraw of each child. Overriding dispatchDraw is used for applying effects on top of child elements.

According to Android Vitals statistics, the most common causes of frame drops in onDraw are creating objects inside the method (48%), calling decodeResource (22%), and complex Path operations without caching (15%).

Invalidation: when a View redraws

Invalidation — the mechanism that triggers View redrawing. Calling invalidate() marks the View as “dirty” and schedules onDraw to be called in the next drawing cycle. Calling requestLayout() is a more “heavy” operation, triggering the full cycle: onMeasure → onLayout → onDraw.

MethodWhat it doesWhen to use
invalidate()Triggers onDraw without onMeasure/onLayoutOnly the appearance changed (color, text, progress)
invalidate(Rect)Redraws only the specified areaPart of the View changed — animation, selection
postInvalidate()Calls invalidate from a non-UI threadBackground thread updated data for drawing
requestLayout()Triggers onMeasure → onLayout → onDrawContent size changed (text, image)
forceLayout()Marks View for forced remeasuringInternal state changed, size may have changed

Animations and View Lifecycle: ViewPropertyAnimator and ValueAnimator call invalidate() on each animation frame. ObjectAnimator calls a setter on the View, which, if the setter changes the size (width/height), automatically calls requestLayout(). This can be expensive for complex ViewGroups: each requestLayout triggers the full hierarchy up to the root view.

Optimization rule: invalidate() instead of requestLayout() everywhere where only the appearance changes (color, transparency, rotation without size change). Use requestLayout only when changing sizes or content that affects size.

Custom View optimization: best practices

Custom Views are a powerful tool for creating unique UI, but they require strict adherence to performance rules. Here are Google’s key recommendations for View Lifecycle optimization.

  • Pre-calculate everything that can be calculated — sizes, coordinates, path, gradient colors. In onDraw, only perform drawing.
  • Cache measurement results — if a View has fixed dimensions, save the MeasureSpec and return setMeasuredDimension without additional calculations.
  • Use ViewConfiguration — getScaledTouchSlop, getScaledMinimumFlingVelocity — for touch handling.
  • Minimize the number of Views in the hierarchy — custom Views that combine multiple elements are always faster than a ViewGroup with 3–5 nested Views. Google recommends no more than 10 nested Views per screen.
  • Use ConstraintLayout for flat hierarchy — it builds a single ViewGroup with performance close to RelativeLayout, but without nesting.
  • Disable hardware layer after redrawing — use setLayerType(LAYER_TYPE_HARDWARE) for Views with animations and setLayerType(LAYER_TYPE_NONE) after completion.
  • Use invalidate() with Rect — redraw only the changed area, not the entire View.
  • Avoid overdraw — use Profile GPU Rendering in Android Studio to identify unnecessary redraws. The average overdraw for Google apps is 1.5x, the maximum recommended is 2.5x.

View code examples in Kotlin

Example 1: Custom View — progress indicator

A simple circular progress indicator with correct implementation of onMeasure, onDraw, and invalidate.

kotlin
class CircularProgressView constructor(
    context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {

    private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.BLUE
        style = Paint.Style.STROKE
        strokeWidth = 8f
        strokeCap = Paint.Cap.ROUND
    }

    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.LTGRAY
        style = Paint.Style.STROKE
        strokeWidth = 8f
    }

    private var progress = 0f
    private var viewWidth = 0
    private var viewHeight = 0

    fun setProgress(value: Float) {
        progress = value.coerceIn(0f, 100f)
        invalidate()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val desiredSize = 100 * resources.displayMetrics.density.toInt()
        val width = MeasureSpec.getSize(widthMeasureSpec)
        val height = MeasureSpec.getSize(heightMeasureSpec)
        val size = minOf(width, height).coerceAtLeast(desiredSize)
        setMeasuredDimension(size, size)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val padding = progressPaint.strokeWidth / 2
        val radius = (minOf(viewWidth, viewHeight) - padding) / 2
        val cx = viewWidth / 2f
        val cy = viewHeight / 2f
        canvas.drawCircle(cx, cy, radius, backgroundPaint)
        val sweepAngle = (progress / 100f) * 360f
        canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius,
            -90f, sweepAngle, false, progressPaint)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        viewWidth = w
        viewHeight = h
    }
}

Circular progress bar: onMeasure returns a square size based on MeasureSpec, onSizeChanged remembers dimensions, onDraw draws the background and the progress arc. Invalidate is called when progress changes — onMeasure/onLayout are not affected. Paint is created once in the constructor, not in onDraw.

Example 2: ViewGroup — simple FlowLayout

A custom ViewGroup that arranges child Views in rows (like Flexbox wrap).

kotlin
class FlowLayout constructor(
    context: Context, attrs: AttributeSet? = null
) : ViewGroup(context, attrs) {

    private val horizontalSpacing = 8.dpToPx(resources)
    private val verticalSpacing = 8.dpToPx(resources)

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val width = MeasureSpec.getSize(widthMeasureSpec)
        var totalHeight = paddingTop + paddingBottom
        var rowWidth = paddingLeft
        var rowHeight = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight)
            if (rowWidth + child.measuredWidth > width - paddingRight) {
                totalHeight += rowHeight + verticalSpacing
                rowWidth = paddingLeft
                rowHeight = 0
            }
            rowWidth += child.measuredWidth + horizontalSpacing
            rowHeight = maxOf(rowHeight, child.measuredHeight)
        }
        totalHeight += rowHeight
        setMeasuredDimension(
            MeasureSpec.getSize(widthMeasureSpec),
            resolveSize(totalHeight, heightMeasureSpec)
        )
    }

    override fun onLayout(changed: Boolean,
        l: Int, t: Int, r: Int, b: Int) {
        var rowTop = paddingTop
        var rowLeft = paddingLeft
        var rowHeight = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (rowLeft + child.measuredWidth > r - paddingRight) {
                rowTop += rowHeight + verticalSpacing
                rowLeft = paddingLeft
                rowHeight = 0
            }
            child.layout(rowLeft, rowTop, rowLeft + child.measuredWidth, rowTop + child.measuredHeight)
            rowLeft += child.measuredWidth + horizontalSpacing
            rowHeight = maxOf(rowHeight, child.measuredHeight)
        }
    }

    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        return MarginLayoutParams(context, attrs)
    }
}

FlowLayout overrides onMeasure: measures each child, wraps to a new line when width is exceeded, calculates total height. onLayout positions children by coordinates accounting for line breaks. generateLayoutParams returns MarginLayoutParams to support margin on child Views.

Example 3: onDraw with Path caching

A custom View draws a smooth Bezier curve, pre-calculating the Path and caching it.

kotlin
class WaveView constructor(
    context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {

    private val wavePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#4A90D9")
        style = Paint.Style.FILL
    }

    private val wavePath = Path()
    private var isPathDirty = true
    private var viewWidth = 0
    private var viewHeight = 0

    fun refreshWave() {
        isPathDirty = true
        invalidate()
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        viewWidth = w
        viewHeight = h
        isPathDirty = true
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (isPathDirty) {
            wavePath.reset()
            val amplitude = viewHeight * 0.1f
            wavePath.moveTo(0f, viewHeight * 0.5f)
            for (x in 0..viewWidth step 4) {
                val y = viewHeight * 0.5f + amplitude * Math.sin(x * 2 * Math.PI / viewWidth).toFloat()
                wavePath.lineTo(x.toFloat(), y)
            }
            wavePath.lineTo(viewWidth.toFloat(), viewHeight.toFloat())
            wavePath.lineTo(0f, viewHeight.toFloat())
            wavePath.close()
            isPathDirty = false
        }
        canvas.drawPath(wavePath, wavePaint)
    }
}

Path caching: isPathDirty = true only when View dimensions change or refreshWave() is called. In onDraw, Path is recalculated only if it is “dirty”. This prevents recalculating the Bezier curve on every animation frame, saving CPU.

Frequently Asked Questions

How is View Lifecycle different from Activity Lifecycle?

View Lifecycle is a cyclic drawing process (onMeasure → onLayout → onDraw) independent of Activity creation/destruction. View does not have onStart/onStop — it is either visible (attached to a window) or not. Activity Lifecycle manages the application component state, View Lifecycle manages UI drawing.

What effect does requestLayout() have on performance?

requestLayout() triggers the full onMeasure → onLayout → onDraw cycle for the entire View tree from the root. If requestLayout() is called frequently (e.g., every animation frame), it causes jank and dropped frames. According to Google, one requestLayout takes on average 2–5 ms on a ViewGroup of 10 elements. For animations, use invalidate().

When is onAttachedToWindow called?

onAttachedToWindow is called when a View is attached to a Window — becomes part of the visible hierarchy. At this moment, the View receives HW acceleration and access to Window resources (WindowManager, Display). onAttachedToWindow is the right place to register animation listeners and BroadcastReceivers that live while the View is visible.

What is overdraw and how to reduce it?

Overdraw is a situation when a pixel is drawn multiple times in a single frame. Each extra pass wastes GPU time. Reduction methods: set windowBackground in theme (don’t draw background in layout), use canvas.clipRect(), avoid merging nested backgrounds, use ConstraintLayout instead of nested LinearLayout. Android Studio → Profile GPU Rendering → Overdraw shows a color map of overdraw (blue = 1x, red = 3x+).

Is super.onDraw() needed in a custom View?

Yes, if the View has a background. super.onDraw() draws the View background. If your custom View does not have a background or you draw your own background, super.onDraw() can be omitted — this saves one drawing pass. For ViewGroup, super.dispatchDraw() is mandatory — it draws child Views.

Summary

  • View Lifecycle — three drawing phases: onMeasure (dimensions), onLayout (position), onDraw (rendering).
  • onMeasure processes MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) and calls setMeasuredDimension.
  • onLayout in ViewGroup positions child Views with left/top/right/bottom coordinates.
  • onDraw renders content on Canvas — do not create objects inside this method.
  • invalidate() triggers only onDraw, requestLayout() triggers the full onMeasure → onLayout → onDraw cycle.
  • Custom Views speed up UI by 15–40%, but require correct onMeasure implementation and object caching in onDraw.
  • For complex graphics, use Hardware Acceleration and cache Path/Bitmap.

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.

Discuss the project

Read also