Neben der zusammensetzbaren Funktion Canvas bietet Compose mehrere nützliche Grafik
Modifiers, die beim Zeichnen benutzerdefinierter Inhalte helfen. Diese Modifikatoren sind nützlich, da sie auf jede zusammensetzbare Funktion angewendet werden können.
Zeichnungsmodifikatoren
Alle Zeichenbefehle werden in Compose mit einem Zeichnungsmodifikator ausgeführt. Es gibt drei Hauptzeichnungsmodifikatoren in Compose:
Der Basismodifikator für das Zeichnen ist drawWithContent. Hier können Sie die Zeichenreihenfolge Ihrer zusammensetzbaren Funktion und die Zeichenbefehle festlegen, die innerhalb des Modifikators ausgegeben werden. drawBehind ist ein praktischer Wrapper um drawWithContent, bei dem die Zeichenreihenfolge auf „hinter dem Inhalt der zusammensetzbaren Funktion“ festgelegt ist. drawWithCache
ruft entweder onDrawBehind oder onDrawWithContent auf und bietet einen Mechanismus zum Speichern der darin erstellten Objekte im Cache.
Modifier.drawWithContent: Zeichenreihenfolge auswählen
Modifier.drawWithContent ermöglicht es Ihnen,
DrawScope-Vorgänge vor oder nach dem Inhalt der
zusammensetzbaren Funktion auszuführen. Rufen Sie drawContent auf, um den tatsächlichen Inhalt der zusammensetzbaren Funktion zu rendern. Mit diesem Modifikator können Sie die Reihenfolge der Vorgänge festlegen, wenn der Inhalt vor oder nach den benutzerdefinierten Zeichenvorgängen gezeichnet werden soll.
Wenn Sie beispielsweise einen radialen Farbverlauf über dem Inhalt rendern möchten, um einen Taschenlampen-Schlüsselloch-Effekt auf der Benutzeroberfläche zu erzeugen, können Sie so vorgehen:
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 einer zusammensetzbaren Funktion zeichnen
Modifier.drawBehind ermöglicht Ihnen die Ausführung von
DrawScope-Vorgängen hinter dem Inhalt der zusammensetzbaren Funktion, der auf dem Bildschirm gezeichnet wird. Wenn
Sie sich die Implementierung von Canvas ansehen, werden Sie feststellen, dass es
sich nur um einen praktischen Wrapper um 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 Ergebnis sieht so aus:
Modifier.drawWithCache: Zeichenobjekte zeichnen und im Cache speichern
Modifier.drawWithCache speichert die darin erstellten Objekte im Cache. Die Objekte werden so lange im Cache gespeichert, wie die Größe des Zeichenbereichs gleich ist oder sich keine der gelesenen Statusobjekte geändert hat. Dieser Modifikator ist nützlich, um die Leistung von Zeichenaufrufen zu verbessern, da Objekte (z. B. Brush, Shader, Path usw.), die beim Zeichnen erstellt werden, 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. Es kann leistungsfähiger sein, drawWithCache zu verwenden, wenn die Objekte nur zum Zeichnen verwendet werden.
Wenn Sie beispielsweise mit drawWithCache einen Brush erstellen, um einen Farbverlauf hinter einem Text zu zeichnen, wird das Brush-Objekt 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 zusammensetzbare Funktionen anwenden
Modifier.graphicsLayer
ist ein Modifikator, mit dem der Inhalt der zusammensetzbaren Funktion in eine Zeichenebene gezeichnet wird. Eine Ebene bietet einige verschiedene Funktionen, z. B.:
- Isolation für die Zeichenanweisungen (ähnlich wie
RenderNode). Zeichen anweisungen, die als Teil einer Ebene erfasst werden, können von der Rendering-Pipeline effizient neu ausgegeben werden, ohne dass Anwendungscode neu ausgeführt werden muss. - Transformationen, die auf alle Zeichenanweisungen angewendet werden, die in einer Ebene enthalten sind.
- Rasterisierung für Kompositionsfunktionen. Wenn eine Ebene rasterisiert wird, werden die Zeichenanweisungen ausgeführt und die Ausgabe wird in einem Offscreen-Puffer erfasst. Das Zusammensetzen eines solchen Puffers 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 Rotation angewendet werden.
Transformationen
Modifier.graphicsLayer bietet Isolation für die Zeichenanweisungen. So können beispielsweise verschiedene Transformationen mit Modifier.graphicsLayer angewendet werden.
Diese können animiert oder geändert werden, ohne dass das Zeichen-Lambda neu ausgeführt werden muss.
Modifier.graphicsLayer ändert nicht die gemessene Größe oder Platzierung der zusammensetzbaren Funktion, da er sich nur auf die Zeichenphase auswirkt. Das bedeutet, dass sich die zusammensetzbare Funktion mit anderen überschneiden kann, wenn sie außerhalb ihrer Layoutgrenzen gezeichnet wird.
Die folgenden Transformationen können mit diesem Modifikator angewendet werden:
Skalieren – Größe erhöhen
Mit scaleX und scaleY wird der Inhalt in horizontaler bzw. vertikaler Richtung vergrößert oder verkleinert. 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 mit graphicsLayer geändert werden. Mit translationX wird die zusammensetzbare Funktion nach links oder rechts verschoben. Mit translationY wird die zusammensetzbare Funktion nach oben oder unten verschoben.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.translationX = 100.dp.toPx() this.translationY = 10.dp.toPx() } )
Rotation
Legen Sie rotationX für die horizontale Rotation, rotationY für die vertikale Rotation und rotationZ für die Rotation auf der Z-Achse (Standardrotation) fest. Dieser Wert wird in Grad angegeben (0–360).
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
Ursprung
Ein transformOrigin kann angegeben werden. Er wird dann als Punkt verwendet, von dem aus Transformationen stattfinden. In allen bisherigen Beispielen wurde TransformOrigin.Center verwendet, das sich bei (0.5f, 0.5f) befindet. Wenn Sie den Ursprung bei (0f, 0f) angeben, beginnen die Transformationen in der linken oberen Ecke der zusammensetzbaren Funktion.
Wenn Sie den Ursprung mit einer rotationZ-Transformation ändern, sehen Sie, dass sich das Element um die linke obere Ecke der zusammensetzbaren Funktion 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 } )
Clip und Form
„Shape“ gibt die Kontur an, an die der Inhalt angeclipst wird, wenn clip = true. In diesem Beispiel legen wir für zwei Felder zwei verschiedene Clips fest. Einer verwendet die graphicsLayer-Clipvariable und der andere den 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 „Hello Compose“) wird an die Kreisform angeclipst:
Wenn Sie dann eine translationY auf den oberen rosa Kreis anwenden, sehen Sie, dass die Grenzen der zusammensetzbaren Funktion gleich bleiben, der Kreis aber unter dem unteren Kreis (und außerhalb seiner Grenzen) gezeichnet wird.
Wenn Sie die zusammensetzbare Funktion auf den Bereich beschränken möchten, in dem sie gezeichnet wird, können Sie am Anfang der Modifikatorkette einen weiteren 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 kann ein alpha-Wert (Deckkraft) für die gesamte Ebene festgelegt werden. 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 } )
CompositingStrategy
Die Arbeit mit Alpha und Transparenz ist möglicherweise nicht so einfach wie das Ändern eines einzelnen Alpha-Werts. Neben dem Ändern eines Alpha-Werts besteht auch die Möglichkeit, eine
CompositingStrategy für eine graphicsLayer festzulegen. Eine CompositingStrategy bestimmt, wie der Inhalt der zusammensetzbaren Funktion mit dem anderen Inhalt zusammengesetzt wird, der bereits auf dem Bildschirm gezeichnet wurde.
Die verschiedenen Strategien sind:
Automatisch (Standard)
Die Compositing-Strategie wird durch die übrigen graphicsLayer
Parameter bestimmt. Die Ebene wird in einen Offscreen-Puffer gerendert, wenn der Alpha-Wert unter 1.0f liegt oder ein RenderEffect festgelegt ist. Wenn der Alpha-Wert unter 1f liegt, wird automatisch eine Compositing-Ebene erstellt, um den Inhalt zu rendern und diesen Offscreen-Puffer dann mit dem entsprechenden Alpha-Wert in das Ziel zu zeichnen. Wenn Sie einen
RenderEffect oder Overscroll festlegen, wird der Inhalt immer in einen Offscreen
Puffer gerendert, unabhängig von der CompositingStrategy festgelegt.
Nicht sichtbar
Der Inhalt der zusammensetzbaren Funktion wird immer in eine Offscreen-Textur oder -Bitmap rasterisiert, bevor er im Ziel gerendert wird. Dies 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 sind BlendModes. Nehmen wir an, Sie möchten Teile einer zusammensetzbaren Funktion vom Typ Image entfernen, indem Sie einen Zeichenbefehl ausgeben, der BlendMode.Clear verwendet. Wenn Sie compositingStrategy nicht auf CompositingStrategy.Offscreen festlegen, interagiert BlendMode mit allen Inhalten 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 Sie CompositingStrategy auf Offscreen festlegen, wird eine Offscreen-Textur erstellt, um die Befehle auszuführen (wobei BlendMode nur auf den Inhalt dieser zusammensetzbaren Funktion angewendet wird). Anschließend wird sie über dem bereits auf dem Bildschirm gerenderten Inhalt gerendert, ohne den bereits gezeichneten Inhalt zu beeinflussen.
Wenn Sie CompositingStrategy.Offscreen nicht verwenden, werden durch die Anwendung von BlendMode.Clear alle Pixel im Ziel gelöscht, unabhängig davon, was bereits festgelegt war. Der Rendering-Puffer des Fensters (schwarz) bleibt sichtbar. Viele der BlendModes, die Alpha verwenden, funktionieren ohne Offscreen-Puffer nicht wie erwartet. Beachten Sie den schwarzen Ring um die rote Kreisanzeige:
Ein weiteres Beispiel: Wenn die App einen durchscheinenden Fensterhintergrund hat und Sie CompositingStrategy.Offscreen nicht verwenden, interagiert BlendMode mit der gesamten App. Alle Pixel werden gelöscht, um die App oder das Hintergrundbild darunter anzuzeigen, wie in diesem Beispiel:
Wenn Sie CompositingStrategy.Offscreen verwenden, wird eine Offscreen-Textur erstellt, die die Größe des Zeichenbereichs hat und wieder auf dem Bildschirm gerendert wird. Alle Zeichenbefehle, die mit dieser Strategie ausgeführt werden, werden standardmäßig auf diesen Bereich beschränkt. Das folgende Code-Snippet veranschaulicht die Unterschiede beim Wechsel zur Verwendung von 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
Diese Compositing-Strategie moduliert den Alpha-Wert für jede der Zeichen
anweisungen, die in der graphicsLayer aufgezeichnet wurden. Für Alpha-Werte unter 1.0f wird kein Offscreen-Puffer erstellt, es sei denn, ein RenderEffect ist festgelegt. Daher kann diese Strategie für das Alpha-Rendering effizienter sein. Bei überlappenden Inhalten kann sie jedoch zu unterschiedlichen Ergebnissen führen. In Anwendungsfällen, in denen im Voraus bekannt ist, dass sich Inhalte nicht überlappen, kann diese Strategie eine bessere Leistung als CompositingStrategy.Auto mit Alpha-Werten unter 1 bieten.
Ein weiteres Beispiel für verschiedene Compositing-Strategien: Hier werden unterschiedliche Alpha-Werte auf verschiedene Teile der zusammensetzbaren Funktionen angewendet und eine Modulate-Strategie 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 einer zusammensetzbaren Funktion in eine Bitmap schreiben
Ein häufiger Anwendungsfall ist das Erstellen einer Bitmap aus einer zusammensetzbaren Funktion. Wenn Sie den
Inhalt der zusammensetzbaren Funktion in eine Bitmap kopieren möchten, erstellen Sie mit
rememberGraphicsLayer() eine GraphicsLayer.
Leiten Sie die Zeichenbefehle mit drawWithContent() und graphicsLayer.record{} an die neue Ebene weiter. Zeichnen Sie dann die Ebene mit drawLayer auf die sichtbare 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 der Festplatte speichern und freigeben. Weitere Informationen finden Sie im vollständigen Beispiel-Snippet. Prüfen Sie, ob On-Device-Berechtigungen vorhanden sind, bevor Sie versuchen, die Bitmap auf dem Laufwerk zu speichern.
Benutzerdefinierter Zeichnungsmodifikator
Wenn Sie einen eigenen benutzerdefinierten Modifikator erstellen möchten, implementieren Sie die DrawModifier-Schnittstelle. Dadurch erhalten Sie Zugriff auf einen ContentDrawScope, der mit dem identisch ist, der bei Verwendung von Modifier.drawWithContent() verfügbar gemacht wird. Sie können dann allgemeine Zeichenvorgänge in benutzerdefinierte Zeichnungsmodifikatoren extrahieren, um den Code zu bereinigen und praktische Wrapper bereitzustellen. Modifier.background() ist beispielsweise ein praktischer DrawModifier.
Wenn Sie beispielsweise einen Modifier implementieren möchten, der den Inhalt vertikal spiegelt, können Sie ihn so erstellen:
class FlippedModifier : DrawModifier { override fun ContentDrawScope.draw() { scale(1f, -1f) { this@draw.drawContent() } } } fun Modifier.flipped() = this.then(FlippedModifier())
Wenden Sie diesen gespiegelten Modifikator dann auf Text an:
Text( "Hello Compose!", modifier = Modifier .flipped() )
Zusätzliche Ressourcen
Weitere Beispiele für die Verwendung von graphicsLayer und benutzerdefinierten Zeichnungen finden Sie in den folgenden Ressourcen:
Empfehlungen für Sie
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist
- Grafiken in Compose
- Bild anpassen {:#customize-image}
- Kotlin für Jetpack Compose