Zusätzlich zu den Canvas
-Kompositionen bietet Compose mehrere nützliche GrafikenModifiers
, die beim Zeichnen benutzerdefinierter Inhalte helfen. Diese Modifikatoren sind nützlich, da sie auf jedes Kompositionen angewendet werden können.
Zeichenmodifikatoren
Alle Zeichenbefehle werden in Compose mit einem Zeichenmodifikator ausgeführt. In Compose gibt es drei Hauptmodifikatoren für das Zeichnen:
Der Basis-Modifikator für das Zeichnen ist drawWithContent
. Hier können Sie die Zeichnungsreihenfolge Ihres Composables und die Zeichnungsbefehle festlegen, die im Modifikator ausgegeben werden. drawBehind
ist ein praktischer Wrapper um drawWithContent
, bei dem die Zeichenreihenfolge hinter dem Inhalt des Composeables festgelegt ist. drawWithCache
ruft entweder onDrawBehind
oder onDrawWithContent
auf und bietet einen Mechanismus zum Caching der darin erstellten Objekte.
Modifier.drawWithContent
: Zeichnungsreihenfolge auswählen
Mit Modifier.drawWithContent
können Sie DrawScope
-Vorgänge vor oder nach dem Inhalt des Composeables ausführen. Rufen Sie drawContent
auf, um den Inhalt des Composeables zu rendern. Mit diesem Modifikator können Sie die Reihenfolge der Vorgänge festlegen, wenn Ihre Inhalte vor oder nach Ihren benutzerdefinierten Zeichenvorgängen gezeichnet werden sollen.
Wenn Sie beispielsweise einen radialen Farbverlauf über Ihren Inhalt rendern möchten, um einen Taschenlampen-Schlüssellocheffekt auf der Benutzeroberfläche zu erzeugen, gehen Sie so vor:
var pointerOffset by remember { mutableStateOf(Offset(0f, 0f)) } Column( modifier = Modifier .fillMaxSize() .pointerInput("dragging") { detectDragGestures { change, dragAmount -> pointerOffset += dragAmount } } .onSizeChanged { pointerOffset = Offset(it.width / 2f, it.height / 2f) } .drawWithContent { drawContent() // draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI. drawRect( Brush.radialGradient( listOf(Color.Transparent, Color.Black), center = pointerOffset, radius = 100.dp.toPx(), ) ) } ) { // Your composables here }
Modifier.drawBehind
: Hinter einem Composeable zeichnen
Mit Modifier.drawBehind
können Sie DrawScope
-Vorgänge hinter den zusammensetzbaren Inhalten ausführen, die auf dem Bildschirm angezeigt werden. Wenn Sie sich die Implementierung von Canvas
ansehen, stellen Sie möglicherweise fest, dass es sich dabei nur um eine praktische Ummantelung von Modifier.drawBehind
handelt.
So zeichnen Sie ein abgerundetes Rechteck hinter Text
:
Text( "Hello Compose!", modifier = Modifier .drawBehind { drawRoundRect( Color(0xFFBBAAEE), cornerRadius = CornerRadius(10.dp.toPx()) ) } .padding(4.dp) )
Das führt zu folgendem Ergebnis:
Modifier.drawWithCache
: Zeichnen und Caching von Zeichnungsobjekten
Modifier.drawWithCache
speichert die darin erstellten Objekte im Cache. Die Objekte werden im Cache gespeichert, solange die Größe des Zeichenbereichs gleich bleibt oder sich die gelesenen Statusobjekte nicht geändert haben. Dieser Modifikator ist nützlich, um die Leistung von Zeichenaufrufen zu verbessern, da Objekte, die bei „draw“ erstellt werden (z. B. Brush, Shader, Path
), nicht neu zugewiesen werden müssen.
Alternativ können Sie Objekte auch mit remember
außerhalb des Modifikators im Cache speichern. Das ist jedoch nicht immer möglich, da Sie nicht immer Zugriff auf die Komposition haben. Die Verwendung von drawWithCache
kann leistungsfähiger sein, wenn die Objekte nur zum Zeichnen verwendet werden.
Wenn Sie beispielsweise ein Brush
erstellen, um einen Farbverlauf hinter einem Text
zu zeichnen, wird das Brush
-Objekt mit drawWithCache
im Cache gespeichert, bis sich die Größe des Zeichenbereichs ändert:
Text( "Hello Compose!", modifier = Modifier .drawWithCache { val brush = Brush.linearGradient( listOf( Color(0xFF9E82F0), Color(0xFF42A5F5) ) ) onDrawBehind { drawRoundRect( brush, cornerRadius = CornerRadius(10.dp.toPx()) ) } } )
Grafikmodifikatoren
Modifier.graphicsLayer
: Transformationen auf Elemente anwenden
Modifier.graphicsLayer
ist ein Modifikator, mit dem der Inhalt des Composeables in eine Zeichenebene gezeichnet wird. Eine Schicht bietet verschiedene Funktionen, z. B.:
- Isolation für die Zeichenanweisungen (ähnlich wie bei
RenderNode
). Zeichenanweisungen, die als Teil einer Ebene erfasst wurden, können von der Rendering-Pipeline effizient neu ausgegeben werden, ohne dass der Anwendungscode noch einmal ausgeführt werden muss. - Transformationen, die auf alle Zeichnungsanweisungen in einer Ebene angewendet werden.
- Rasterisierung für Kompositionsmöglichkeiten Wenn eine Ebene gerastert wird, werden die zugehörigen Zeichenanweisungen ausgeführt und die Ausgabe wird in einem Offscreen-Puffer erfasst. Das Compositing eines solchen Buffers für nachfolgende Frames ist schneller als die Ausführung der einzelnen Anweisungen. Er verhält sich jedoch wie eine Bitmap, wenn Transformationen wie Skalierung oder Drehung angewendet werden.
Transformation
Modifier.graphicsLayer
sorgt für eine Isolation der Zeichenanweisungen. So können beispielsweise verschiedene Transformationen mit Modifier.graphicsLayer
angewendet werden.
Sie können animiert oder geändert werden, ohne dass das Zeichnen-Lambda neu ausgeführt werden muss.
Modifier.graphicsLayer
ändert weder die Größe noch die Platzierung des Composeables, da es sich nur auf die Zeichenphase auswirkt. Das bedeutet, dass sich Ihr Composeable über andere Elemente legen kann, wenn es außerhalb seiner Layoutgrenzen gezeichnet wird.
Mit diesem Modifikator können die folgenden Transformationen angewendet werden:
Skalierung – Größe erhöhen
Mit scaleX
und scaleY
können Sie Inhalte horizontal oder vertikal vergrößern oder verkleinern. Ein Wert von 1.0f
bedeutet keine Änderung der Skalierung, ein Wert von 0.5f
bedeutet die Hälfte der Dimension.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.scaleX = 1.2f this.scaleY = 0.8f } )
Übersetzung
translationX
und translationY
können durch graphicsLayer
ersetzt werden. Mit translationX
wird das Element nach links oder rechts verschoben. Mit translationY
kannst du das Composeable nach oben oder unten verschieben.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.translationX = 100.dp.toPx() this.translationY = 10.dp.toPx() } )
Ausrichtung
Legen Sie rotationX
für die horizontale, rotationY
für die vertikale und rotationZ
für die Drehung um die Z‑Achse (Standarddrehung) fest. Dieser Wert wird in Grad (0–360) angegeben.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
Origin
Ein transformOrigin
kann angegeben werden. Er wird dann als Ausgangspunkt für Transformationen verwendet. In allen bisherigen Beispielen wurde TransformOrigin.Center
verwendet, das sich unter (0.5f, 0.5f)
befindet. Wenn Sie den Ursprung bei (0f, 0f)
angeben, beginnen die Transformationen links oben im Composeable.
Wenn Sie den Ursprung mit einer rotationZ
-Transformation ändern, sehen Sie, dass sich das Element oben links im Composeable dreht:
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.transformOrigin = TransformOrigin(0f, 0f) this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
Zuschneiden und Formen
Mit „Shape“ (Form) wird der Umriss angegeben, an den die Inhalte zugeschnitten werden, wenn clip = true
. In diesem Beispiel haben wir zwei Boxen mit zwei verschiedenen Clips eingerichtet – eine mit der Clipvariablen graphicsLayer
und die andere mit dem praktischen Wrapper Modifier.clip
.
Column(modifier = Modifier.padding(16.dp)) { Box( modifier = Modifier .size(200.dp) .graphicsLayer { clip = true shape = CircleShape } .background(Color(0xFFF06292)) ) { Text( "Hello Compose", style = TextStyle(color = Color.Black, fontSize = 46.sp), modifier = Modifier.align(Alignment.Center) ) } Box( modifier = Modifier .size(200.dp) .clip(CircleShape) .background(Color(0xFF4DB6AC)) ) }
Der Inhalt des ersten Felds (der Text „Hallo Compose“) wird auf die Kreisform zugeschnitten:
Wenn Sie dann einen translationY
auf den oberen rosa Kreis anwenden, sehen Sie, dass die Begrenzungen des Composed-Elements gleich bleiben, der Kreis aber unter dem unteren Kreis (und außerhalb seiner Begrenzung) gezeichnet wird.
Wenn Sie das Composeable auf den Bereich zuschneiden möchten, in dem es gezeichnet wird, können Sie am Anfang der Modifikatorkette eine weitere Modifier.clip(RectangleShape)
hinzufügen. Der Inhalt bleibt dann innerhalb der ursprünglichen Grenzen.
Column(modifier = Modifier.padding(16.dp)) { Box( modifier = Modifier .clip(RectangleShape) .size(200.dp) .border(2.dp, Color.Black) .graphicsLayer { clip = true shape = CircleShape translationY = 50.dp.toPx() } .background(Color(0xFFF06292)) ) { Text( "Hello Compose", style = TextStyle(color = Color.Black, fontSize = 46.sp), modifier = Modifier.align(Alignment.Center) ) } Box( modifier = Modifier .size(200.dp) .clip(RoundedCornerShape(500.dp)) .background(Color(0xFF4DB6AC)) ) }
Alpha
Mit Modifier.graphicsLayer
können Sie eine alpha
(Deckkraft) für die gesamte Ebene festlegen. 1.0f
ist vollständig undurchsichtig und 0.0f
ist unsichtbar.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "clock", modifier = Modifier .graphicsLayer { this.alpha = 0.5f } )
Compositing-Strategie
Die Arbeit mit Alpha und Transparenz ist möglicherweise nicht so einfach wie das Ändern eines einzelnen Alphawerts. Neben dem Ändern eines Alphakanals können Sie auch einen CompositingStrategy
für einen graphicsLayer
festlegen. Mit einer CompositingStrategy
wird festgelegt, wie die Inhalte des Composeables mit den anderen Inhalten kombiniert werden, die bereits auf dem Bildschirm dargestellt werden.
Die verschiedenen Strategien sind:
Automatisch (Standard)
Die Kompositionsstrategie wird durch die übrigen graphicsLayer
-Parameter bestimmt. Die Ebene wird in einen Offscreen-Puffer gerendert, wenn der Alphawert kleiner als 1,0 f ist oder RenderEffect
festgelegt ist. Wenn der Alphawert unter 1f liegt, wird automatisch eine Kompositionierungsebene erstellt, um den Inhalt zu rendern und dann diesen Offscreen-Puffer mit dem entsprechenden Alphawert an das Ziel zu zeichnen. Wenn du RenderEffect
oder Overscroll festlegst, werden Inhalte unabhängig von der festgelegten CompositingStrategy
immer in einem Offscreen-Puffer gerendert.
Nicht sichtbar
Der Inhalt des Composeables wird immer in eine Offscreen-Textur oder -Bitmap gerastert, bevor er an das Ziel gerendert wird. Das ist nützlich, um BlendMode
-Vorgänge zum Maskieren von Inhalten anzuwenden und die Leistung beim Rendern komplexer Zeichenanweisungen zu verbessern.
Ein Beispiel für die Verwendung von CompositingStrategy.Offscreen
ist BlendModes
. Im folgenden Beispiel möchten Sie Teile einer Image
-Komposition entfernen, indem Sie einen Zeichenbefehl mit BlendMode.Clear
ausführen. Wenn Sie compositingStrategy
nicht auf CompositingStrategy.Offscreen
festlegen, interagiert BlendMode
mit dem gesamten Inhalt darunter.
Image( painter = painterResource(id = R.drawable.dog), contentDescription = "Dog", contentScale = ContentScale.Crop, modifier = Modifier .size(120.dp) .aspectRatio(1f) .background( Brush.linearGradient( listOf( Color(0xFFC5E1A5), Color(0xFF80DEEA) ) ) ) .padding(8.dp) .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } .drawWithCache { val path = Path() path.addOval( Rect( topLeft = Offset.Zero, bottomRight = Offset(size.width, size.height) ) ) onDrawWithContent { clipPath(path) { // this draws the actual image - if you don't call drawContent, it wont // render anything this@onDrawWithContent.drawContent() } val dotSize = size.width / 8f // Clip a white border for the content drawCircle( Color.Black, radius = dotSize, center = Offset( x = size.width - dotSize, y = size.height - dotSize ), blendMode = BlendMode.Clear ) // draw the red circle indication drawCircle( Color(0xFFEF5350), radius = dotSize * 0.8f, center = Offset( x = size.width - dotSize, y = size.height - dotSize ) ) } } )
Wenn du CompositingStrategy
auf Offscreen
setzt, wird eine Offscreen-Textur erstellt, auf die die Befehle ausgeführt werden. BlendMode
wird dann nur auf den Inhalt dieses Composeables angewendet. Dieser wird dann über dem bereits gerenderten Inhalt auf dem Bildschirm gerendert, ohne dass sich dies auf den bereits gezeichneten Inhalt auswirkt.
Wenn Sie CompositingStrategy.Offscreen
nicht verwendet haben, werden durch die Anwendung von BlendMode.Clear
alle Pixel im Ziel gelöscht, unabhängig davon, was bereits festgelegt wurde. Der Rendering-Puffer des Fensters (schwarz) bleibt sichtbar. Viele der BlendModes
-Funktionen, die Alpha beinhalten, funktionieren ohne Offscreen-Puffer nicht wie erwartet. Beachten Sie den schwarzen Ring um den roten Kreis:
Zur Verdeutlichung: Wenn die App einen halbtransparenten Fensterhintergrund hat und Sie die CompositingStrategy.Offscreen
nicht verwenden, interagiert die BlendMode
mit der gesamten App. Alle Pixel werden gelöscht, um die App oder den Hintergrund darunter anzuzeigen, wie in diesem Beispiel:
Hinweis: Wenn Sie CompositingStrategy.Offscreen
verwenden, wird eine Offscreen-Textur erstellt, die der Größe des Zeichenbereichs entspricht, und wieder auf dem Bildschirm gerendert. Alle Zeichnungsbefehle, die mit dieser Strategie ausgeführt werden, werden standardmäßig auf diese Region zugeschnitten. Das folgende Code-Snippet veranschaulicht die Unterschiede beim Wechsel zu Offscreen-Texturen:
@Composable fun CompositingStrategyExamples() { Column( modifier = Modifier .fillMaxSize() .wrapContentSize(Alignment.Center) ) { // Does not clip content even with a graphics layer usage here. By default, graphicsLayer // does not allocate + rasterize content into a separate layer but instead is used // for isolation. That is draw invalidations made outside of this graphicsLayer will not // re-record the drawing instructions in this composable as they have not changed Canvas( modifier = Modifier .graphicsLayer() .size(100.dp) // Note size of 100 dp here .border(2.dp, color = Color.Blue) ) { // ... and drawing a size of 200 dp here outside the bounds drawRect(color = Color.Magenta, size = Size(200.dp.toPx(), 200.dp.toPx())) } Spacer(modifier = Modifier.size(300.dp)) /* Clips content as alpha usage here creates an offscreen buffer to rasterize content into first then draws to the original destination */ Canvas( modifier = Modifier // force to an offscreen buffer .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) .size(100.dp) // Note size of 100 dp here .border(2.dp, color = Color.Blue) ) { /* ... and drawing a size of 200 dp. However, because of the CompositingStrategy.Offscreen usage above, the content gets clipped */ drawRect(color = Color.Red, size = Size(200.dp.toPx(), 200.dp.toPx())) } } }
ModulateAlpha
Bei dieser Kompositionsstrategie wird der Alphawert für jede der im graphicsLayer
aufgezeichneten Zeichenanweisungen moduliert. Es wird kein Offscreen-Puffer für Alphawerte unter 1.0f erstellt, es sei denn, RenderEffect
ist festgelegt. Das kann für das Alpha-Rendering effizienter sein. Bei sich überschneidenden Inhalten kann es jedoch zu unterschiedlichen Ergebnissen kommen. Bei Anwendungsfällen, bei denen im Voraus bekannt ist, dass sich Inhalte nicht überschneiden, kann dies eine bessere Leistung als CompositingStrategy.Auto
mit Alphawerten unter 1 bieten.
Unten sehen Sie ein weiteres Beispiel für unterschiedliche Kompositionierungsstrategien: Unterschiedliche Alphas werden auf verschiedene Teile der Composeables angewendet und eine Modulate
-Strategie wird verwendet:
@Preview @Composable fun CompositingStrategy_ModulateAlpha() { Column( modifier = Modifier .fillMaxSize() .padding(32.dp) ) { // Base drawing, no alpha applied Canvas( modifier = Modifier.size(200.dp) ) { drawSquares() } Spacer(modifier = Modifier.size(36.dp)) // Alpha 0.5f applied to whole composable Canvas( modifier = Modifier .size(200.dp) .graphicsLayer { alpha = 0.5f } ) { drawSquares() } Spacer(modifier = Modifier.size(36.dp)) // 0.75f alpha applied to each draw call when using ModulateAlpha Canvas( modifier = Modifier .size(200.dp) .graphicsLayer { compositingStrategy = CompositingStrategy.ModulateAlpha alpha = 0.75f } ) { drawSquares() } } } private fun DrawScope.drawSquares() { val size = Size(100.dp.toPx(), 100.dp.toPx()) drawRect(color = Red, size = size) drawRect( color = Purple, size = size, topLeft = Offset(size.width / 4f, size.height / 4f) ) drawRect( color = Yellow, size = size, topLeft = Offset(size.width / 4f * 2f, size.height / 4f * 2f) ) } val Purple = Color(0xFF7E57C2) val Yellow = Color(0xFFFFCA28) val Red = Color(0xFFEF5350)
Inhalt eines Composeables in eine Bitmap schreiben
Ein häufiger Anwendungsfall ist das Erstellen einer Bitmap
aus einem Composeable. Wenn Sie den Inhalt Ihres Composeables in ein Bitmap
kopieren möchten, erstellen Sie mit rememberGraphicsLayer()
ein GraphicsLayer
.
Leiten Sie die Zeichenbefehle mit drawWithContent()
und graphicsLayer.record{}
an die neue Ebene weiter. Zeichnen Sie dann die Ebene mit drawLayer
auf dem sichtbaren Canvas:
val coroutineScope = rememberCoroutineScope() val graphicsLayer = rememberGraphicsLayer() Box( modifier = Modifier .drawWithContent { // call record to capture the content in the graphics layer graphicsLayer.record { // draw the contents of the composable into the graphics layer this@drawWithContent.drawContent() } // draw the graphics layer on the visible canvas drawLayer(graphicsLayer) } .clickable { coroutineScope.launch { val bitmap = graphicsLayer.toImageBitmap() // do something with the newly acquired bitmap } } .background(Color.White) ) { Text("Hello Android", fontSize = 26.sp) }
Sie können die Bitmap auf dem Laufwerk speichern und freigeben. Weitere Informationen finden Sie im vollständigen Beispiel-Snippet. Prüfen Sie die Berechtigungen auf dem Gerät, bevor Sie versuchen, auf dem Laufwerk zu speichern.
Modifikator für benutzerdefinierte Zeichnungen
Wenn Sie einen eigenen benutzerdefinierten Modifikator erstellen möchten, implementieren Sie die DrawModifier
-Schnittstelle. So erhalten Sie Zugriff auf einen ContentDrawScope
, der mit dem identisch ist, der bei Verwendung von Modifier.drawWithContent()
freigegeben wird. Sie können dann häufig verwendete Zeichenvorgänge in benutzerdefinierte Zeichenmodifikatoren extrahieren, um den Code zu bereinigen und praktische Wrapper bereitzustellen. Modifier.background()
ist beispielsweise eine praktische DrawModifier
.
Wenn Sie beispielsweise eine Modifier
implementieren möchten, die Inhalte vertikal dreht, können Sie sie so erstellen:
class FlippedModifier : DrawModifier { override fun ContentDrawScope.draw() { scale(1f, -1f) { this@draw.drawContent() } } } fun Modifier.flipped() = this.then(FlippedModifier())
Verwenden Sie dann diesen umgekehrten Modifier, der auf Text
angewendet wird:
Text( "Hello Compose!", modifier = Modifier .flipped() )
Weitere Informationen
Weitere Beispiele für die Verwendung von graphicsLayer
und benutzerdefinierten Zeichnungen finden Sie in den folgenden Ressourcen:
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Grafiken in Compose
- Bild anpassen {:#customize-image}
- Kotlin für Jetpack Compose