View Lifecycle — die Abfolge von Methoden, die Android zum Zeichnen und Neuzeichnen eines Benutzeroberflächenelements (View) auf dem Bildschirm aufruft. Im Gegensatz zu Activity oder Fragment ist View eine leichte Komponente ohne erweiterten Lebenszyklus, durchläuft jedoch einen strengen Dreiphasenprozess: onMeasure (Messung), onLayout (Positionierung), onDraw (Zeichnung). Das Verständnis des View Lifecycle ist für die Erstellung benutzerdefinierter Views, die Optimierung der Leistung und die Behebung von Zeichenproblemen erforderlich. Laut Google beschleunigen benutzerdefinierte Views bei korrekter Implementierung die UI um 15–40% im Vergleich zu einer Kombination aus standardmäßig verschachtelten ViewGroups. Android-Dokumentation zu benutzerdefinierten Views beschreibt onMeasure, onLayout und onDraw als die drei Säulen des View Lifecycle.
Wichtige Punkte
View Lifecycle ist der Prozess, den eine Android View (und ViewGroup) durchläuft, um sich auf dem Bildschirm anzuzeigen. Im Gegensatz zu Activity oder Fragment hat View kein onStart/onStop/onDestroy — sein „Leben“ besteht aus einem zyklischen Prozess des Messens, Positionierens und Zeichnens. Dieser Zyklus wird jedes Mal ausgelöst, wenn eine View angezeigt oder neu gezeichnet werden muss.
Die drei Phasen des View Lifecycle:
Der vollständige View Lifecycle-Zyklus umfasst auch Methoden zum Anhängen einer View an ein Fenster: onAttachedToWindow (View ist an ein Fenster angehängt, hat HW-Beschleunigung) und onDetachedFromWindow (View ist getrennt, Ressourcen werden freigegeben). Diese Methoden werden einmal pro View-Lebensdauer aufgerufen und sind wichtig zum Registrieren/Abbestellen von Animationen und Sensoren.
Laut Android Performance Blog stehen 65% der UI-Leistungsprobleme (Ruckeln, Frame-Ausfälle) im Zusammenhang mit falscher Implementierung von onMeasure und onDraw: übermäßiges Überschreiben, unnötiges Aufrufen von requestLayout(), Erstellen von Objekten in onDraw.
onMeasure — die wichtigste und komplexeste Phase des View Lifecycle. In dieser Phase bestimmt Android, wie viel Platz die View auf dem Bildschirm einnehmen wird. Das System übergibt MeasureSpec — in int gepackte Anweisungen, bestehend aus einem Modus und einer Größe.
Die drei MeasureSpec-Modi:
| Modus | Konstante | Bedeutung | Beispiel |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Genauer, vom Eltern festgelegter Wert (match_parent oder feste Breite) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | View kann bis zur angegebenen Maximalgröße haben (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Keine Einschränkungen — View kann beliebige Größe haben (ScrollView, RecyclerView) | Breite unbegrenzt → MeasureSpec(0, UNSPECIFIED) |
Die Implementierung von onMeasure sollte:
setMeasuredDimension(int width, int height) aufrufen, um die gemessenen Abmessungen zu speichern.getPaddingLeft() + getPaddingRight() von der verfügbaren Breite abziehen.measureChild() oder measureChildWithMargins() messen.Typischer Fehler: MeasureSpec bei wrap_content nicht berücksichtigen. Wenn eine View auf wrap_content gesetzt ist, aber onMeasure AT_MOST nicht behandelt und eine feste Größe zurückgibt, wird die View entweder abgeschnitten oder nimmt mehr Platz als nötig ein.
onLayout — die Phase, in der eine View oder ViewGroup ihre Kinder anordnet innerhalb ihrer Grenzen. Für eine normale View (keine ViewGroup) ist onLayout nicht erforderlich — das System ruft layout() mit den vom Eltern übergebenen Parametern auf. Für eine ViewGroup ist onLayout obligatorisch — ohne es werden untergeordnete Views nicht platziert.
Signatur von onLayout:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// Anordnung untergeordneter Views
}
Der Parameter changed gibt an, ob sich die Position oder Größe der View im Vergleich zum vorherigen Layout geändert hat. Bei false kann die View zur Optimierung die Neuberechnung der Kindpositionen überspringen.
Für ViewGroup sollte onLayout:
getChildCount() und getChildAt(i) durchlaufen.child.layout(l, t, r, b) für jedes Kind aufrufen.onLayout wird nach onMeasure aufgerufen — gemessene Abmessungen sind via getMeasuredWidth()/getMeasuredHeight() verfügbar. Wenn eine untergeordnete View nach layout() andere tatsächliche Abmessungen hat, wird requestLayout() zur erneuten Messung aufgerufen. Dies wird als „Layout-Pass“ bezeichnet und kann eine Kettenreaktion von Neuberechnungen auslösen.
onDraw — die Phase, in der eine View sich selbst auf Canvas zeichnet. Dies ist die einzige Phase, die mehrfach ohne onMeasure und onLayout aufgerufen werden kann — wenn die View als invalidate() markiert ist. Canvas bietet eine Zeichen-API: drawLine, drawRect, drawCircle, drawText, drawBitmap und drawPath.
onDraw-Regeln:
canvas.clipRect() zum Beschneiden unsichtbarer Teile verwenden.Zeichenreihenfolge in ViewGroup: Hintergrund (setBackgroundDrawable) → onDraw (Inhalt) → dispatchDraw (untergeordnete Views) → onDrawForeground (Vordergrund). dispatchDraw ruft onDraw jedes Kindes auf. Das Überschreiben von dispatchDraw wird verwendet, um Effekte über untergeordneten Elementen anzuwenden.
Laut Android Vitals-Statistiken sind die häufigsten Ursachen für Frame-Ausfälle in onDraw das Erstellen von Objekten innerhalb der Methode (48%), das Aufrufen von decodeResource (22%) und komplexe Path-Operationen ohne Caching (15%).
Invalidierung — der Mechanismus, der das Neuzeichnen der View auslöst. Der Aufruf von invalidate() markiert die View als „schmutzig“ und plant den Aufruf von onDraw im nächsten Zeichenzyklus. Der Aufruf von requestLayout() ist eine „schwerere“ Operation, die den vollständigen Zyklus auslöst: onMeasure → onLayout → onDraw.
| Methode | Funktion | Verwendungszweck |
|---|---|---|
| invalidate() | Löst onDraw ohne onMeasure/onLayout aus | Nur das Aussehen hat sich geändert (Farbe, Text, Fortschritt) |
| invalidate(Rect) | Zeichnet nur den angegebenen Bereich neu | Teil der View hat sich geändert — Animation, Auswahl |
| postInvalidate() | Ruft invalidate von einem Nicht-UI-Thread auf | Hintergrundthread hat Daten zum Zeichnen aktualisiert |
| requestLayout() | Löst onMeasure → onLayout → onDraw aus | Die Inhaltsgröße hat sich geändert (Text, Bild) |
| forceLayout() | Markiert View zur erzwungenen Neuvermessung | Interner Zustand geändert, Größe könnte sich geändert haben |
Animationen und View Lifecycle: ViewPropertyAnimator und ValueAnimator rufen invalidate() in jedem Animationsframe auf. ObjectAnimator ruft einen Setter auf der View auf, der, wenn der Setter die Größe (Breite/Höhe) ändert, automatisch requestLayout() aufruft. Dies kann für komplexe ViewGroups teuer sein: Jede requestLayout löst die gesamte Hierarchie bis zur Root-View aus.
Optimierungsregel: invalidate() statt requestLayout() überall dort, wo sich nur das Aussehen ändert (Farbe, Transparenz, Drehung ohne Größenänderung). Verwenden Sie requestLayout nur beim Ändern von Größen oder Inhalten, die die Größe beeinflussen.
Benutzerdefinierte Views sind ein leistungsstarkes Werkzeug zur Erstellung einzigartiger UIs, erfordern jedoch die strikte Einhaltung von Leistungsregeln. Hier sind die wichtigsten Empfehlungen von Google zur Optimierung des View Lifecycle.
setLayerType(LAYER_TYPE_HARDWARE) für Views mit Animationen und setLayerType(LAYER_TYPE_NONE) nach Abschluss verwenden.Eine einfache kreisförmige Fortschrittsanzeige mit korrekter Implementierung von onMeasure, onDraw und 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
}
}
Kreisförmige Fortschrittsanzeige: onMeasure gibt eine quadratische Größe basierend auf MeasureSpec zurück, onSizeChanged merkt sich die Abmessungen, onDraw zeichnet den Hintergrund und den Fortschrittsbogen. Invalidate wird bei Änderung des Fortschritts aufgerufen — onMeasure/onLayout bleiben unbeeinflusst. Paint wird einmal im Konstruktor erstellt, nicht in onDraw.
Eine benutzerdefinierte ViewGroup, die untergeordnete Views in Zeilen anordnet (wie 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 überschreibt onMeasure: misst jedes Kind, bricht bei Überschreitung der Breite in eine neue Zeile um, berechnet die Gesamthöhe. onLayout positioniert Kinder nach Koordinaten unter Berücksichtigung von Zeilenumbrüchen. generateLayoutParams gibt MarginLayoutParams zurück, um Margin bei untergeordneten Views zu unterstützen.
Eine benutzerdefinierte View zeichnet eine glatte Bezier-Kurve, berechnet den Path vorab und cached ihn.
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 nur bei Änderung der View-Abmessungen oder Aufruf von refreshWave(). In onDraw wird Path nur neu berechnet, wenn er „schmutzig“ ist. Dies verhindert die Neuberechnung der Bezier-Kurve in jedem Animationsframe und spart CPU.
Häufig gestellte Fragen
View Lifecycle ist ein zyklischer Zeichenprozess (onMeasure → onLayout → onDraw), unabhängig von der Erstellung/Zerstörung der Activity. View hat kein onStart/onStop — es ist entweder sichtbar (an ein Fenster angehängt) oder nicht. Activity Lifecycle verwaltet den Zustand der Anwendungskomponente, View Lifecycle verwaltet das UI-Zeichnen.
requestLayout() löst den vollständigen onMeasure → onLayout → onDraw-Zyklus für den gesamten View-Baum von der Wurzel aus aus. Bei häufigem Aufruf (z.B. jedem Animationsframe) verursacht es Ruckeln und verworfene Frames. Laut Google dauert ein requestLayout bei einer ViewGroup mit 10 Elementen durchschnittlich 2–5 ms. Für Animationen verwenden Sie invalidate().
onAttachedToWindow wird aufgerufen, wenn eine View an ein Fenster angehängt wird — Teil der sichtbaren Hierarchie wird. In diesem Moment erhält die View HW-Beschleunigung und Zugriff auf Fensterressourcen (WindowManager, Display). onAttachedToWindow ist der richtige Ort, um Animations-Listener und BroadcastReceiver zu registrieren, die leben, solange die View sichtbar ist.
Overdraw ist eine Situation, in der ein Pixel mehrmals in einem einzelnen Frame gezeichnet wird. Jeder zusätzliche Durchgang verschwendet GPU-Zeit. Reduktionsmethoden: windowBackground im Theme setzen (keinen Hintergrund im Layout zeichnen), canvas.clipRect() verwenden, Zusammenführen verschachtelter Hintergründe vermeiden, ConstraintLayout anstelle von verschachteltem LinearLayout verwenden. Android Studio → Profile GPU Rendering → Overdraw zeigt eine Farbkarte des Overdraw (blau = 1x, rot = 3x+).
Ja, wenn die View einen Hintergrund hat. super.onDraw() zeichnet den View-Hintergrund. Wenn Ihre benutzerdefinierte View keinen Hintergrund hat oder Sie einen eigenen Hintergrund zeichnen, kann super.onDraw() weggelassen werden — dies spart einen Zeichendurchgang. Für ViewGroup ist super.dispatchDraw() obligatorisch — es zeichnet die untergeordneten Views.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch