View Lifecycle — de reeks methoden die Android aanroept om een gebruikersinterface-element (View) op het scherm weer te geven en opnieuw weer te geven. In tegenstelling tot Activity of Fragment is View een lichtgewicht component zonder uitgebreide levenscyclus, maar doorloopt het een strikt driefasenproces: onMeasure (meten), onLayout (plaatsen), onDraw (tekenen). Inzicht in View Lifecycle is noodzakelijk voor het maken van aangepaste Views, prestatie-optimalisatie en het oplossen van weergaveproblemen. Volgens Google versnellen aangepaste Views de UI met 15–40% in vergelijking met een combinatie van standaard geneste ViewGroups bij correcte implementatie. Android-documentatie over aangepaste Views beschrijft onMeasure, onLayout en onDraw als de drie pijlers van View Lifecycle.
Belangrijkste punten
View Lifecycle — het proces dat Android View (en ViewGroup) doorloopt om zichzelf op het scherm weer te geven. In tegenstelling tot Activity of Fragment heeft View geen onStart/onStop/onDestroy — zijn „leven” bestaat uit een cyclisch proces van meten, plaatsen en tekenen. Deze cyclus wordt elke keer gestart wanneer View moet worden weergegeven of opnieuw getekend.
Drie fasen van View Lifecycle:
De volledige View Lifecycle-cyclus omvat ook methoden gerelateerd aan het hechten van View aan het venster: onAttachedToWindow (View is aan venster gehecht, heeft HW acceleration) en onDetachedFromWindow (View is losgemaakt, bronnen worden vrijgegeven). Deze methoden worden één keer in het leven van View aangeroepen en zijn belangrijk voor het registreren/annuleren van animaties, sensoren.
Volgens de Android Performance Blog is 65% van de UI-prestatieproblemen (jank, overgeslagen frames) gerelateerd aan onjuiste implementatie van onMeasure en onDraw: overmatig overschrijven, onnodig aanroepen van requestLayout(), objecten maken in onDraw.
onMeasure — de belangrijkste en meest complexe fase van View Lifecycle. In deze fase bepaalt Android hoeveel ruimte View op het scherm inneemt. Het systeem geeft MeasureSpec door — in int verpakte instructies bestaande uit modus en grootte.
Drie modi van MeasureSpec:
| Modus | Constante | Betekenis | Voorbeeld |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Exacte grootte bepaald door ouder (match_parent of vaste breedte) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | View kan grootte hebben tot opgegeven maximum (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Geen beperkingen — View kan elke grootte hebben (ScrollView, RecyclerView) | breedte onbeperkt → MeasureSpec(0, UNSPECIFIED) |
Implementatie van onMeasure moet:
setMeasuredDimension(int width, int height) aanroepen om gemeten afmetingen op te slaan.getPaddingLeft() + getPaddingRight() aftrekken van beschikbare breedte.measureChild() of measureChildWithMargins().Typische fout: geen rekening houden met MeasureSpec bij wrap_content. Als View is ingesteld op wrap_content, maar onMeasure verwerkt AT_MOST niet en retourneert een vaste grootte, wordt View ofwel bijgesneden, of neemt het meer ruimte in dan nodig.
onLayout — de fase waarin View of ViewGroup zijn afstammelingen plaatst binnen zijn grenzen. Voor een gewone View (niet ViewGroup) is onLayout niet vereist — het systeem roept zelf layout() aan met parameters van de ouder. Voor ViewGroup is onLayout verplicht — zonder worden onderliggende Views niet geplaatst.
Handtekening van onLayout:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// plaatsen van onderliggende Views
}
Parameter changed geeft aan of de positie of grootte van View is veranderd ten opzichte van de vorige layout. Indien false — kan View het herberekenen van posities van afstammelingen overslaan voor optimalisatie.
Voor ViewGroup moet onLayout:
getChildCount() en getChildAt(i).child.layout(l, t, r, b) aanroepen voor elke afstammeling.onLayout wordt aangeroepen na onMeasure — gemeten afmetingen zijn beschikbaar via getMeasuredWidth()/getMeasuredHeight(). Als een onderliggende View andere werkelijke afmetingen heeft na layout(), wordt requestLayout() aangeroepen voor hermeting. Dit wordt een „layout pass” genoemd en kan een kettingreactie van herberekeningen veroorzaken.
onDraw — de fase waarin View zichzelf tekent op Canvas. Dit is de enige fase die meerdere keren kan worden aangeroepen zonder onMeasure en onLayout — als View is gemarkeerd als invalidate(). Canvas biedt API voor tekenen: drawLine, drawRect, drawCircle, drawText, drawBitmap en drawPath.
Regels voor onDraw:
canvas.clipRect() om onzichtbare delen af te snijden.Tekenvolgorde in ViewGroup: achtergrond (setBackgroundDrawable) → onDraw (inhoud) → dispatchDraw (onderliggende Views) → onDrawForeground (voorgrond). dispatchDraw roept onDraw van elke afstammeling aan. Het overschrijven van dispatchDraw wordt gebruikt om effecten over onderliggende elementen aan te brengen.
Volgens Android Vitals-statistieken zijn de meest voorkomende oorzaken van overgeslagen frames in onDraw — objecten maken binnen de methode (48%), aanroep van decodeResource (22%) en complexe bewerkingen met Path zonder caching (15%).
Invalidation — het mechanisme dat het hertekenen van View activeert. Aanroep van invalidate() markeert View als „vuil” en plant een onDraw-aanroep in de volgende tekencyclus. Aanroep van requestLayout() — een zwaardere bewerking die de volledige cyclus start: onMeasure → onLayout → onDraw.
| Methode | Wat het doet | Wanneer gebruiken |
|---|---|---|
| invalidate() | Roept onDraw aan zonder onMeasure/onLayout | Alleen uiterlijk veranderd (kleur, tekst, voortgang) |
| invalidate(Rect) | Hertekent alleen het opgegeven gebied | Een deel van View veranderd — animatie, selectie |
| postInvalidate() | Roept invalidate aan vanuit niet-UI-thread | Achtergrondthread heeft tekengegevens bijgewerkt |
| requestLayout() | Start onMeasure → onLayout → onDraw | Inhoudsgrootte veranderd (tekst, afbeelding) |
| forceLayout() | Markeert View voor geforceerde hermeting | Interne status veranderd, grootte kan zijn gewijzigd |
Animaties en View Lifecycle: ViewPropertyAnimator en ValueAnimator roepen invalidate() aan op elk fram van de animatie. ObjectAnimator roept een setter aan op View die, als de setter de grootte (width/height) wijzigt, automatisch requestLayout() aanroept. Dit kan kostbaar zijn voor complexe ViewGroups: elke requestLayout start de volledige hiërarchie tot aan de root-view.
Optimalisatieregel: invalidate() in plaats van requestLayout() overal waar alleen het uiterlijk verandert (kleur, transparantie, rotatie zonder groottewijziging). Gebruik requestLayout alleen bij wijziging van afmetingen of inhoud die de grootte beïnvloedt.
Aangepaste Views — een krachtig hulpmiddel voor het creëren van unieke UI, maar vereisen strikte naleving van prestatieregels. Hier zijn de belangrijkste aanbevelingen van Google voor het optimaliseren van View Lifecycle.
setLayerType(LAYER_TYPE_HARDWARE) voor Views met animaties en setLayerType(LAYER_TYPE_NONE) na voltooiing.Eenvoudige cirkelvormige voortgangsindicator met correcte implementatie van onMeasure, onDraw en 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
}
}
Cirkelvormige voortgangsbalk: onMeasure retourneert een vierkante grootte op basis van MeasureSpec, onSizeChanged onthoudt afmetingen, onDraw tekent de achtergrond en de voortgangsboog. Invalidate wordt aangeroepen bij wijziging van voortgang — onMeasure/onLayout worden niet beïnvloed. Paint wordt één keer in de constructor gemaakt, niet in onDraw.
Aangepaste ViewGroup die onderliggende Views in rijen plaatst (zoals 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 overschrijft onMeasure: meet elke afstammeling, gaat naar een nieuwe rij bij overschrijding van de breedte, berekent de totale hoogte. onLayout plaatst kinderen op coördinaten met inachtneming van regeleinden. generateLayoutParams retourneert MarginLayoutParams voor ondersteuning van marges bij onderliggende Views.
Aangepaste View tekent een vloeiende Bezier-curve, berekent Path van tevoren en cacht het.
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 alleen bij wijziging van View-afmetingen of aanroep van refreshWave(). In onDraw wordt Path alleen herberekend als het „vuil” is. Dit voorkomt herberekening van de Bezier-curve op elk animatieframe, wat CPU bespaart.
Veelgestelde vragen
View Lifecycle — een cyclisch tekenproces (onMeasure → onLayout → onDraw), onafhankelijk van het maken/vernietigen van Activity. View heeft geen onStart/onStop — het is ofwel zichtbaar (aan venster gehecht) of niet. Activity Lifecycle beheert de status van de applicatiecomponent, View Lifecycle — het tekenen van de UI.
requestLayout() start de volledige cyclus onMeasure → onLayout → onDraw voor de gehele View-boom vanaf de root. Als requestLayout() vaak wordt aangeroepen (bijvoorbeeld elk animatieframe), veroorzaakt dit jank en overgeslagen frames. Volgens Google duurt één requestLayout gemiddeld 2–5 ms op een ViewGroup met 10 elementen. Gebruik voor animaties invalidate().
onAttachedToWindow wordt aangeroepen wanneer View aan het venster (Window) wordt gehecht — onderdeel wordt van de zichtbare hiërarchie. Op dat moment krijgt View HW acceleration en toegang tot Window-bronnen (WindowManager, Display). onAttachedToWindow is de juiste plaats voor het registreren van animatie-listeners en BroadcastReceiver die leeft zolang View zichtbaar is.
Overdraw — een situatie waarin een pixel meerdere keren in één frame wordt getekend. Elke extra doorgang is GPU-tijdverspilling. Reductiemethoden: stel windowBackground in in theme (teken geen achtergrond in layout), gebruik canvas.clipRect(), vermijd het samenvoegen van geneste achtergronden, pas ConstraintLayout toe in plaats van geneste LinearLayouts. Android Studio → Profile GPU Rendering → Overdraw toont een kleurenkaart van overdraw (blauw = 1x, rood = 3x+).
Ja, als View een achtergrond (background) heeft. super.onDraw() tekent de achtergrond van View. Als uw aangepaste View geen achtergrond heeft of u tekent uw eigen achtergrond, kan super.onDraw() worden weggelaten — dit bespaart één tekenpassage. Voor ViewGroup is super.dispatchDraw() verplicht — het tekent onderliggende Views.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook