View Lifecycle — sekvensen av metoder som Android anropar för att rendera och omrendera ett gränssnittselement (View) på skärmen. Till skillnad från Activity eller Fragment är View en lättviktskomponent som inte har en utökad livscykel, men genomgår en strikt trefasprocess: onMeasure (mätning), onLayout (placering), onDraw (ritning). Förståelse av View Lifecycle är nödvändig för att skapa anpassade View, optimera prestanda och lösa renderingsproblem. Enligt Google snabbar anpassade View upp UI med 15–40% jämfört med kombinationer av standard nästlade ViewGroup vid korrekt implementering. Android-dokumentation om anpassade View beskriver onMeasure, onLayout och onDraw som de tre pelarna i View Lifecycle.
Huvudpunkter
View Lifecycle — processen som Android View (och ViewGroup) genomgår för att visa sig på skärmen. Till skillnad från Activity eller Fragment har View inget onStart/onStop/onDestroy — dess “liv” består av en cyklisk process av mätning, placering och ritning. Denna cykel utlöses varje gång View behöver visas eller ritas om.
Tre faser av View Lifecycle:
Den fullständiga View Lifecycle-cykeln inkluderar även metoder relaterade till att fästa View vid fönstret: onAttachedToWindow (View är fäst vid fönstret, har HW acceleration) och onDetachedFromWindow (View är lossad, resurser frigörs). Dessa metoder anropas en gång under Viewns livstid och är viktiga för registrering/avbokning av animationer, sensorer.
Enligt Android Performance Blog är 65% av UI-prestandaproblemen (jank, hoppade bildrutor) relaterade till felaktig implementering av onMeasure och onDraw: överdriven överskuggning, anrop av requestLayout() i onödan, skapande av objekt i onDraw.
onMeasure — den viktigaste och mest komplexa fasen av View Lifecycle. I detta skede bestämmer Android hur mycket utrymme View kommer att ta på skärmen. Systemet skickar MeasureSpec — instruktioner packade i int bestående av läge och storlek.
Tre MeasureSpec-lägen:
| Läge | Konstant | Betydelse | Exempel |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Exakt storlek bestämd av förälder (match_parent eller fast bredd) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | View kan vara upp till angivet maximum (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Utan begränsningar — View kan vara valfri storlek (ScrollView, RecyclerView) | bredd obegränsad → MeasureSpec(0, UNSPECIFIED) |
Implementering av onMeasure bör:
setMeasuredDimension(int width, int height) för att spara uppmätta mått.getPaddingLeft() + getPaddingRight() från tillgänglig bredd.measureChild() eller measureChildWithMargins().Typiskt fel: att inte ta hänsyn till MeasureSpec vid wrap_content. Om View är inställd på wrap_content, men onMeasure inte bearbetar AT_MOST och returnerar en fast storlek, kommer View antingen att beskuras eller ta mer plats än nödvändigt.
onLayout — fasen där View eller ViewGroup placerar sina avkomlingar inom sina gränser. För en vanlig View (inte ViewGroup) krävs inte onLayout — systemet anropar själv layout() med parametrar från föräldern. För ViewGroup är onLayout obligatoriskt — utan det kommer underordnade View inte att placeras.
Signatur för onLayout:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// placering av underordnade View
}
Parametern changed anger om positionen eller storleken på View har ändrats jämfört med föregående layout. Om false — kan View hoppa över omberäkning av avkomlingars positioner för optimering.
För ViewGroup bör onLayout:
getChildCount() och getChildAt(i).child.layout(l, t, r, b) för varje avkomling.onLayout anropas efter onMeasure — uppmätta mått är tillgängliga via getMeasuredWidth()/getMeasuredHeight(). Om en underordnad View har andra verkliga mått efter layout(), kommer requestLayout() att anropas för omätning. Detta kallas “layout pass” och kan orsaka en kedjereaktion av omberäkningar.
onDraw — fasen där View ritar sig själv på Canvas. Detta är den enda fasen som kan anropas flera gånger utan onMeasure och onLayout — om View är markerad som invalidate(). Canvas tillhandahåller API för ritning: drawLine, drawRect, drawCircle, drawText, drawBitmap och drawPath.
Regler för onDraw:
canvas.clipRect() för att beskära osynliga delar.Ritningsordning i ViewGroup: bakgrund (setBackgroundDrawable) → onDraw (innehåll) → dispatchDraw (underordnade View) → onDrawForeground (förgrund). dispatchDraw anropar onDraw för varje avkomling. Överskuggning av dispatchDraw används för att applicera effekter ovanpå underordnade element.
Enligt Android Vitals statistik är de vanligaste orsakerna till hoppade bildrutor i onDraw — skapande av objekt inuti metoden (48%), anrop av decodeResource (22%) och komplexa operationer med Path utan cache (15%).
Invalidation — mekanismen som utlöser omritning av View. Anrop av invalidate() markerar View som “smutsig” och schemalägger anrop av onDraw i nästa ritningscykel. Anrop av requestLayout() — en tyngre operation som utlöser hela cykeln: onMeasure → onLayout → onDraw.
| Metod | Vad den gör | När ska användas |
|---|---|---|
| invalidate() | Anropar onDraw utan onMeasure/onLayout | Endast utseendet har ändrats (färg, text, förlopp) |
| invalidate(Rect) | Ritar endast om det angivna området | En del av View har ändrats — animation, markering |
| postInvalidate() | Anropar invalidate från icke-UI-tråd | Bakgrundstråd har uppdaterat data för ritning |
| requestLayout() | Utlöser onMeasure → onLayout → onDraw | Innehållets storlek har ändrats (text, bild) |
| forceLayout() | Markerar View för tvångsomätning | Internt tillstånd har ändrats, storleken kan ha ändrats |
Animationer och View Lifecycle: ViewPropertyAnimator och ValueAnimator anropar invalidate() vid varje bildruta i animationen. ObjectAnimator anropar en setter på View som, om settern ändrar storleken (width/height), automatiskt anropar requestLayout(). Detta kan vara kostsamt för komplexa ViewGroup: varje requestLayout utlöser hela hierarkin ända upp till rotvyn.
Optimeringsregel: invalidate() istället för requestLayout() överallt där endast utseendet ändras (färg, transparens, rotation utan storleksändring). Använd requestLayout endast vid ändring av mått eller innehåll som påverkar storleken.
Anpassade View — ett kraftfullt verktyg för att skapa unikt UI, men kräver strikt efterlevnad av prestandaregler. Här är Googles viktigaste rekommendationer för optimering av View Lifecycle.
setLayerType(LAYER_TYPE_HARDWARE) för View med animationer och setLayerType(LAYER_TYPE_NONE) efter slutförande.Enkel cirkulär förloppsindikator med korrekt implementering av onMeasure, onDraw och 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
}
}
Cirkulär förloppsindikator: onMeasure returnerar kvadratisk storlek baserat på MeasureSpec, onSizeChanged kommer ihåg måtten, onDraw ritar bakgrund och förloppsbåge. Invalidate anropas vid förloppsändring — onMeasure/onLayout påverkas inte. Paint skapas en gång i konstruktorn, inte i onDraw.
Anpassad ViewGroup som placerar underordnade View i rader (som 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 överskuggar onMeasure: mäter varje avkomling, flyttar till ny rad vid överskridning av bredd, beräknar total höjd. onLayout placerar barnen efter koordinater med hänsyn till radbrytningar. generateLayoutParams returnerar MarginLayoutParams för stöd av marginaler hos underordnade View.
Anpassad View ritar en mjuk Bezier-kurva, beräknar Path i förväg och cachar den.
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-cache: isPathDirty = true endast vid ändring av View-mått eller anrop av refreshWave(). I onDraw beräknas Path om endast om det är “smutsigt”. Detta förhindrar omberäkning av Bezier-kurvan vid varje animationsbildruta, vilket sparar CPU.
Vanliga frågor
View Lifecycle — en cyklisk ritningsprocess (onMeasure → onLayout → onDraw), oberoende av skapande/förstöring av Activity. View har inget onStart/onStop — den är antingen synlig (fäst vid fönstret) eller inte. Activity Lifecycle hanterar tillståndet för applikationskomponenten, View Lifecycle — ritningen av UI.
requestLayout() utlöser hela cykeln onMeasure → onLayout → onDraw för hela View-trädet från roten. Om requestLayout() anropas ofta (t.ex. varje bildruta i en animation) orsakar det jank och hoppade bildrutor. Enligt Google tar en requestLayout i genomsnitt 2–5 ms på en ViewGroup med 10 element. För animationer, använd invalidate().
onAttachedToWindow anropas när View fästs vid fönstret (Window) — blir en del av den synliga hierarkin. I detta ögonblick får View HW-acceleration och tillgång till fönstrets resurser (WindowManager, Display). onAttachedToWindow är rätt plats för att registrera animationslyssnare och BroadcastReceiver som lever medan View är synlig.
Overdraw — en situation där en pixel ritas flera gånger i en enda bildruta. Varje extra genomgång är slöseri med GPU-tid. Reduktionsmetoder: ställ in windowBackground i theme (rita inte bakgrund i layout), använd canvas.clipRect(), undvik sammanslagning av nästlade bakgrunder, använd ConstraintLayout istället för nästlade LinearLayout. Android Studio → Profile GPU Rendering → Overdraw visar en färgkarta över overdraw (blå = 1x, röd = 3x+).
Ja, om View har bakgrund (background). super.onDraw() ritar bakgrunden för View. Om din anpassade View inte har någon bakgrund eller du ritar din egen bakgrund, kan super.onDraw() utelämnas — detta sparar en ritningsgenomgång. För ViewGroup är super.dispatchDraw() obligatoriskt — det ritar underordnade View.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också