Wie die meisten anderen UI-Toolkits rendert auch Compose einen Frame durch mehrere verschiedene Phasen. Das Android View-System besteht aus drei messen, Layout und Zeichnen. Die Funktion „Schreiben“ ist sehr ähnlich, wichtige zusätzliche Phase, die Komposition genannt, zu Beginn.
Die Komposition wird in allen unseren „Compose-Dokumenten“ beschrieben, einschließlich Thinking in Compose und State and Jetpack Compose.
Die drei Phasen eines Frames
Das Schreiben gliedert sich in drei Hauptphasen:
- Komposition: Welche UI soll angezeigt werden? Compose führt zusammensetzbare Funktionen aus erstellt eine Beschreibung Ihrer UI.
- Layout: Wo die Benutzeroberfläche platziert werden soll? Diese Phase umfasst zwei Schritte: Analyse und Platzierung. Layout-Elemente messen und platzieren sich selbst und alle untergeordneten Elemente in 2D-Koordinaten für jeden Knoten im Layoutbaum.
- Zeichnung: Darstellung des Renderings UI-Elemente werden in einen Canvas gezeichnet, Gerätebildschirms.
Die Reihenfolge dieser Phasen ist im Allgemeinen gleich, sodass die Daten in einer einzigen Phase fließen.
Richtung von der Komposition über das Layout bis hin zur Zeichnung, um einen Frame zu erstellen (auch bekannt
als unidirektionalen Datenfluss).
BoxWithConstraints
und
LazyColumn
und LazyRow
sind bemerkenswert
Ausnahmen, bei denen die Zusammensetzung der untergeordneten Elemente vom Layout des übergeordneten Elements abhängt.
.
Sie können davon ausgehen, dass diese drei Phasen praktisch für jeden Frame stattfinden. Um die Leistung zu verbessern, vermeidet die Compose-Funktion die Wiederholung von Arbeiten, die gleichen Ergebnisse aus denselben Eingaben in allen diesen Phasen. Verfassen skips das Ausführen einer zusammensetzbaren Funktion , wenn ein früheres Ergebnis wiederverwendet werden kann und die Benutzeroberfläche zum Schreiben weder das Layout noch den gesamten Baum neu zeichnen, wenn das nicht nötig ist. Mit „Compose“ werden nur die den Arbeitsaufwand, der für die Aktualisierung der Benutzeroberfläche erforderlich ist. Diese Optimierung ist möglich da „Compose“ Statuslesevorgänge innerhalb der verschiedenen Phasen verfolgt.
Die Phasen verstehen
In diesem Abschnitt wird beschrieben, wie die drei Erstellungsphasen für zusammensetzbare Funktionen ausgeführt werden. genauer an.
Komposition
In der Erstellungsphase führt die Compose-Laufzeit zusammensetzbare Funktionen aus gibt eine Baumstruktur aus, die Ihre Benutzeroberfläche darstellt. Dieser UI-Baum besteht aus Layoutknoten, die alle für die nächsten Phasen erforderlichen Informationen enthalten, wie im folgenden Video gezeigt:
Abbildung 2: Baum, der Ihre Benutzeroberfläche darstellt, die in der Komposition erstellt wird .
Ein Unterabschnitt des Codes und des UI-Baums sieht so aus:
<ph type="x-smartling-placeholder">In diesen Beispielen ist jede zusammensetzbare Funktion im Code einem einzelnen Layout zugeordnet. Knoten in der Struktur der Benutzeroberfläche. In komplexeren Beispielen können zusammensetzbare Funktionen Logik und den Ablauf zu steuern und eine andere Baumstruktur für verschiedene Zustände zu erstellen.
Layout
In der Layout-Phase verwendet Compose die UI-Struktur, die in der Erstellungsphase erstellt wurde. als Eingabe verwenden. Die Sammlung von Layoutknoten enthält alle Informationen, die für Größe und Position jedes Knotens im 2D-Raum entscheiden.
Abbildung 4: Die Messung und Platzierung jedes Layoutknotens im UI-Baum während der Layoutphase.
Während der Layout-Phase wird der Baum in den folgenden drei Schritten durchlaufen. Algorithmus:
- Untergeordnete Elemente messen: Ein Knoten misst seine untergeordneten Elemente, falls vorhanden.
- Eigene Größe festlegen: Auf der Grundlage dieser Messungen entscheidet ein Knoten für sich allein. Größe.
- Untergeordnete Knoten platzieren: Jeder untergeordnete Knoten wird relativ zum eigenen Knoten eines Knotens platziert. .
Am Ende dieser Phase hat jeder Layoutknoten Folgendes:
- eine zugewiesene Breite und Höhe.
- Eine x- und y-Koordinate, in der sie gezeichnet werden soll
Rufen Sie sich noch einmal den UI-Baum aus dem vorherigen Abschnitt ins Gedächtnis:
Für diesen Baum funktioniert der Algorithmus so:
- Der
Row
misst die untergeordneten ElementeImage
undColumn
. Image
wird gemessen. Da es keine untergeordneten Elemente hat, entscheidet sie selbst. und gibt die Größe anRow
zurück.- Als Nächstes wird
Column
gemessen. Er misst seine eigenen Kinder (zweiText
zusammensetzbare Funktionen) zuerst. - Die erste
Text
wird gemessen. Da sie keine untergeordneten Elemente hat, entscheidet sie, und gibt ihre Größe anColumn
zurück.- Die zweite
Text
wird gemessen. Da sie keine untergeordneten Elemente hat, entscheidet sie, und gibt sie anColumn
zurück.
- Die zweite
Column
verwendet die untergeordneten Messwerte, um seine eigene Größe zu bestimmen. Dabei werden die maximale Breite der untergeordneten Elemente und die Summe der Höhe ihrer untergeordneten Elemente.- In der
Column
werden die untergeordneten Elemente relativ zu sich selbst platziert. vertikal miteinander verbunden. Row
verwendet die untergeordneten Messwerte, um seine eigene Größe zu bestimmen. Dabei werden die maximale Höhe der untergeordneten Elemente und die Summe der Breiten ihrer untergeordneten Elemente. Dann platziert es ihre untergeordneten Elemente.
Beachten Sie, dass jeder Knoten nur einmal aufgerufen wurde. Für die Compose-Laufzeit ist nur eine den UI-Baum durchlaufen, um alle Knoten zu messen und zu platzieren, die Leistung. Wenn die Anzahl der Knoten in der Baumstruktur zunimmt, ist die aufgewendete Zeit beim Durchlaufen linear zu. Wenn dagegen jeder Knoten erhöht sich die Durchsuchungszeit exponentiell.
Zeichnung
In der Zeichenphase wird der Baum erneut von oben nach unten durchlaufen. zieht sich der Reihe nach auf den Bildschirm.
Abbildung 5: In der Zeichenphase werden die Pixel auf den Bildschirm gezeichnet.
Im vorherigen Beispiel wird der Bauminhalt wie folgt gezeichnet:
- Mit
Row
werden alle Inhalte gezeichnet, die es enthalten kann, z. B. eine Hintergrundfarbe. Image
zieht sich von selbst.Column
zieht sich von selbst.- Die erste und die zweite
Text
zeichnen sich jeweils selbst.
Abbildung 6: Ein UI-Baum und seine gezeichnete Darstellung.
Status-Lesevorgänge
Wenn Sie den Wert eines Snapshot-Status während eines aus den oben genannten Phasen, verfolgt „Compose“ automatisch den Vorgang, der Wert gelesen wurde. Durch diese Verfolgung kann Compose den Reader neu ausführen, ändert sich der Statuswert und bildet die Grundlage für die Beobachtbarkeit des Status in Compose.
Der Status wird in der Regel mit mutableStateOf()
erstellt und dann über einen abgerufen
auf zwei Arten: durch direkten Zugriff auf die value
-Property oder durch
mit einem Kotlin-Property-Delegaten. Weitere Informationen hierzu finden Sie unter Status in
zusammensetzbare Funktionen. Zum Zweck der
„State Read“ (Zustand lesen). bezieht sich auf einen der beiden äquivalenten
.
// State read without property delegate. val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(paddingState.value) )
// State read with property delegate. var padding: Dp by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(padding) )
Im Hintergrund der Property
delegieren,
"getter" und „Setter“ werden für den Zugriff auf und die Aktualisierung der
value
Diese Getter- und Setter-Funktionen werden nur aufgerufen, wenn Sie auf
die Eigenschaft als Wert und nicht beim Erstellen. Deshalb gibt es zwei Möglichkeiten,
oben gleichwertige Werte.
Jeder Codeblock, der neu ausgeführt werden kann, wenn sich ein Lesestatus ändert, ist einen Neustartbereich. Mit „Compose“ werden Änderungen des Statuswerts verfolgt und neu gestartet in verschiedenen Phasen.
Lesevorgänge in Phasen
Wie bereits erwähnt, gibt es drei Hauptphasen in den Tracks „Komponieren“ und „Komponieren“. welche Status jeweils gelesen werden. So werden über die Funktion "Schreiben" nur für jedes betroffene Element Ihrer UI.
Sehen wir uns die einzelnen Phasen an und beschreiben, was passiert, wenn ein Statuswert gelesen wird. darin enthalten sind.
Phase 1: Zusammensetzung
Statuslesevorgänge innerhalb einer @Composable
-Funktion oder eines Lambda-Blocks beeinflussen die Zusammensetzung
und möglicherweise auch die weiteren Phasen. Wenn sich der Statuswert ändert,
recomposer plant Wiederholungen aller zusammensetzbaren Funktionen, die den Code
state-Wert auf. Es kann sein, dass die Laufzeit einige oder alle der
zusammensetzbaren Funktionen verwenden, wenn sich die Eingaben nicht geändert haben. Weitere Informationen finden Sie unter Überspringen, wenn die Eingaben
nicht geändert.
Abhängig vom Ergebnis der Erstellung werden in der Editor-Benutzeroberfläche das Layout und die Zeichnung ausgeführt. Phasen. Diese Phasen werden möglicherweise übersprungen, wenn der Inhalt und die Größe gleich bleiben. und das Layout ändert sich nicht.
var padding by remember { mutableStateOf(8.dp) } Text( text = "Hello", // The `padding` state is read in the composition phase // when the modifier is constructed. // Changes in `padding` will invoke recomposition. modifier = Modifier.padding(padding) )
Phase 2: Layout
Die Layoutphase besteht aus zwei Schritten: Messung und Placement. Die
wird die Lambda-Messung ausgeführt, die an die zusammensetzbare Funktion Layout
übergeben wird.
MeasureScope.measure
der LayoutModifier
-Schnittstelle usw. Die
beim Placement-Schritt den Platzierungsblock der layout
-Funktion, der Lambda-Funktion,
Modifier.offset { … }
und so weiter.
Status-Reads während der einzelnen Schritte wirken sich auf das Layout und möglicherweise Zeichenphase. Wenn sich der Statuswert ändert, wird das Layout in der Benutzeroberfläche „Compose“ geplant . Außerdem wird die Zeichenphase ausgeführt, wenn sich Größe oder Position geändert hat.
Um genauer zu sein, haben die Schritte zur Messung und die Platzierung separate die Bereiche neu starten, d. h., die Statuslesevorgänge im Placement-Schritt werden nicht noch einmal aufgerufen noch einmal mit der Messung beginnen. Diese beiden Schritte sind jedoch häufig sind miteinander verknüpft, sodass sich ein im Placement-Schritt gelesener Status auf andere Neustarts auswirken kann. die zum Schritt der Messung gehören.
var offsetX by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.offset { // The `offsetX` state is read in the placement step // of the layout phase when the offset is calculated. // Changes in `offsetX` restart the layout. IntOffset(offsetX.roundToPx(), 0) } )
Phase 3: Zeichnen
Statuslesevorgänge während des Zeichencodes wirken sich auf die Zeichenphase aus. Gängige Beispiele
umfassen Canvas()
, Modifier.drawBehind
und Modifier.drawWithContent
. Wann?
Wenn sich der Statuswert ändert, wird nur die Zeichenphase ausgeführt.
var color by remember { mutableStateOf(Color.Red) } Canvas(modifier = modifier) { // The `color` state is read in the drawing phase // when the canvas is rendered. // Changes in `color` restart the drawing. drawRect(color) }
Statuslesevorgänge optimieren
Wenn Compose lokalisierte Status-Lesevorgänge durchführt, können wir die Anzahl der durchgeführt werden, indem jeder Zustand in einer geeigneten Phase gelesen wird.
Sehen wir uns ein Beispiel an. Hier haben wir ein Image()
, das den Offset verwendet.
, um die endgültige Layoutposition zu verschieben. Dies führt zu einem Parallaxe-Effekt,
wenn die Nutzenden scrollen.
Box { val listState = rememberLazyListState() Image( // ... // Non-optimal implementation! Modifier.offset( with(LocalDensity.current) { // State read of firstVisibleItemScrollOffset in composition (listState.firstVisibleItemScrollOffset / 2).toDp() } ) ) LazyColumn(state = listState) { // ... } }
Dieser Code funktioniert, aber die Leistung ist nicht optimal. Wie bereits erwähnt,
liest den Wert des firstVisibleItemScrollOffset
-Zustands und übergibt ihn an
die
Modifier.offset(offset: Dp)
. Wenn der Nutzer scrollt, wird der Wert firstVisibleItemScrollOffset
ändern können. Wie wir wissen, verfolgt „Compose“ alle Statuslesevorgänge, sodass es neu gestartet werden kann.
(re-invoke) des Lesecodes, der in unserem Beispiel der Inhalt der
Box
Dies ist ein Beispiel für einen Zustand, der innerhalb der composition-Phase gelesen wird. Dies ist nicht unbedingt eine schlechte Sache und tatsächlich die Grundlage der Neuzusammensetzung, sodass Datenänderungen eine neue Benutzeroberfläche ausgeben.
In diesem Beispiel ist sie jedoch nicht optimal, da jedes Scroll-Ereignis des gesamten zusammensetzbaren Inhalts, der neu bewertet und dann auch gemessen wird. und schließlich gezeichnet werden. Bei jedem Scrollen wird die Erstellungsphase ausgelöst Auch wenn sich die angezeigten Inhalte nicht geändert haben, nur wo sie zu sehen sind. Wir können unseren Zustandslesevorgang so optimieren, dass nur die Layoutphase neu ausgelöst wird.
Es ist eine weitere Version des Offset-Modifikators verfügbar:
Modifier.offset(offset: Density.() -> IntOffset)
Diese Version verwendet einen Lambda-Parameter, bei dem das resultierende Offset vom mit dem Lambda-Block. Aktualisieren wir nun unseren Code, um sie zu verwenden:
Box { val listState = rememberLazyListState() Image( // ... Modifier.offset { // State read of firstVisibleItemScrollOffset in Layout IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2) } ) LazyColumn(state = listState) { // ... } }
Warum ist das leistungsstärker? Der Lambdablock, den wir
für den Modifikator bereitstellen, ist
die während der Layoutphase aufgerufen werden, insbesondere während der
im Placement-Schritt). Das bedeutet, dass der Status firstVisibleItemScrollOffset
länger lesen können. Mit „Compose“ wird erfasst,
wann der Status gelesen wird,
bedeutet diese Änderung Folgendes: Wenn sich der Wert firstVisibleItemScrollOffset
ändert,
Mit der Funktion „Schreiben“ müssen nur die Layout- und Zeichenphasen neu gestartet werden.
Dieses Beispiel basiert auf den verschiedenen Offset-Modifikatoren, um den Generell gilt aber: Versuchen Sie, Statuslesevorgänge in der kürzesten Phase, sodass mit der Funktion „Compose“ so wenig wie möglich arbeiten.
Natürlich ist es oft absolut notwendig, die Stadien in der Komposition zu lesen. . Dennoch gibt es Fälle, in denen wir die Anzahl der indem Sie Statusänderungen filtern. Weitere Informationen hierzu Siehe RelatedStateOf: Konvertieren eines oder mehrerer Statusobjekte in ein anderes Bundesstaat.
Neuzusammensetzungsschleife (zyklische Phasenabhängigkeit)
Wir haben bereits erwähnt, dass die Phasen von „Compose“ immer im selben und dass es keine Möglichkeit gibt, im selben Frame rückwärts zu springen. Das verhindert jedoch nicht, dass Apps in Kompositionsschleifen geraten. in verschiedenen Frames zu sehen. Betrachten wir dieses Beispiel:
Box { var imageHeightPx by remember { mutableStateOf(0) } Image( painter = painterResource(R.drawable.rectangle), contentDescription = "I'm above the text", modifier = Modifier .fillMaxWidth() .onSizeChanged { size -> // Don't do this imageHeightPx = size.height } ) Text( text = "I'm below the image", modifier = Modifier.padding( top = with(LocalDensity.current) { imageHeightPx.toDp() } ) ) }
Hier haben wir (fehlerhaft) eine vertikale Spalte implementiert, bei der das Bild ganz oben steht,
und dann den Text darunter. Wir verwenden Modifier.onSizeChanged()
, um zu erfahren,
die Größe des Bildes aufgelöst und dann mit Modifier.padding()
für den Text
und verschieben es nach unten. Die unnatürliche Konvertierung von Px
zurück in Dp
ist bereits
gibt an, dass ein Problem mit dem Code vorliegt.
Das Problem bei diesem Beispiel ist, dass wir nicht zum „endgültigen“ Layout innerhalb von auf einen einzelnen Frame. Der Code basiert auf mehreren Frames, wodurch unnötige Arbeit und führt dazu, dass die Nutzenden auf dem Bildschirm herumspringen.
Gehen wir die einzelnen Frames durch, um zu sehen, was passiert:
In der Zusammensetzungsphase des ersten Frames hat imageHeightPx
den Wert 0,
und der Text wird mit Modifier.padding(top = 0)
angegeben. Dann wird das Layout
folgt, und der Callback für den onSizeChanged
-Modifikator wird aufgerufen.
Dabei wird imageHeightPx
auf die tatsächliche Höhe des Bildes aktualisiert.
Erstellt einen Plan für die Neuzusammensetzung des nächsten Frames. In der Zeichenphase
Text wird mit einem Abstand von 0 gerendert, da die Wertänderung nicht widergespiegelt wird
.
Compose startet dann den zweiten Frame, der durch die Wertänderung von
imageHeightPx
Der Status wird im Box-Inhaltsblock eingelesen und aufgerufen
in der Phase der Komposition. Dieses Mal wird für den Text ein Abstand
an die Bildhöhe anpassen. In der Layoutphase legt der Code den Wert
imageHeightPx
wieder, aber es ist keine Neuzusammensetzung geplant, da der Wert
bleibt gleich.
Am Ende erhalten wir den gewünschten Abstand für den Text, aber es ist nicht optimal, einen zusätzlichen Frame ausgeben, um den Padding-Wert an eine andere Phase zurückzugeben, führt dazu, dass ein Frame mit überlappendem Inhalt erstellt wird.
Dieses Beispiel mag konstruiert erscheinen, aber achten Sie auf dieses allgemeine Muster:
Modifier.onSizeChanged()
,onGloballyPositioned()
oder ein anderes Layout Betriebsabläufe- Bestimmten Status aktualisieren
- Verwenden Sie diesen Status als Eingabe für einen Layoutmodifikator (
padding()
,height()
oder ähnlich) - Mögliche Wiederholung
Die Lösung für das obige Beispiel besteht darin, die richtigen Layout-Primitive zu verwenden. Das Beispiel
lässt sich mit einem einfachen Column()
implementieren. Sie haben jedoch möglicherweise ein
komplexes Beispiel, bei dem ein benutzerdefinierter Vorgang erforderlich ist, der das Schreiben einer
benutzerdefiniertes Layout. Weitere Informationen finden Sie im Leitfaden zu benutzerdefinierten Layouts.
.
Das Prinzip ist hier im Allgemeinen, eine Single Source of Truth für mehrere Benutzeroberflächen Elemente, die gemessen und im Verhältnis zueinander platziert werden sollen. Mit ein geeignetes Layout-Primitive erstellen oder ein benutzerdefiniertes Layout erstellen, ein gemeinsames Elternteil dient als Informationsquelle, die die Beziehung koordinieren kann. zwischen mehreren Elementen. Der dynamische Zustand widerspricht diesem Prinzip.
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- State und Jetpack Compose
- Listen und Raster
- Kotlin für Jetpack Compose