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 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:
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 — 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:
| Mode | Constant | Meaning | Example |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Exact size set by the parent (match_parent or fixed width) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | View can be up to the specified maximum size (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | No restrictions — View can be any size (ScrollView, RecyclerView) | width unlimited → MeasureSpec(0, UNSPECIFIED) |
onMeasure implementation should:
setMeasuredDimension(int width, int height) to save the measured dimensions.getPaddingLeft() + getPaddingRight() from the available width.measureChild() or measureChildWithMargins().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 — 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:
@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:
getChildCount() and getChildAt(i).child.layout(l, t, r, b) for each child.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 — 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:
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 — 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.
| Method | What it does | When to use |
|---|---|---|
| invalidate() | Triggers onDraw without onMeasure/onLayout | Only the appearance changed (color, text, progress) |
| invalidate(Rect) | Redraws only the specified area | Part of the View changed — animation, selection |
| postInvalidate() | Calls invalidate from a non-UI thread | Background thread updated data for drawing |
| requestLayout() | Triggers onMeasure → onLayout → onDraw | Content size changed (text, image) |
| forceLayout() | Marks View for forced remeasuring | Internal 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 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.
setLayerType(LAYER_TYPE_HARDWARE) for Views with animations and setLayerType(LAYER_TYPE_NONE) after completion.A simple circular progress indicator with correct implementation of onMeasure, onDraw, and invalidate.
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.
A custom ViewGroup that arranges child Views in rows (like Flexbox wrap).
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.
A custom View draws a smooth Bezier curve, pre-calculating the Path and caching it.
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
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.
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().
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.
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+).
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
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.
Read also