Überlappende Profilbilder mit Jetpack Compose Modifier
Da ein Bild mehr sagt als tausend Worte, werden Sie mit diesem wohl verstehen, was ich erreichen wollte, als ich mit der Arbeit an einer Profilbildreihe begann:
Ich möchte Profilbilder so anzeigen, dass sie das vorherige überlappen. Es ist kein neuer Designtrend, aber für 2023 immer noch elegant.
Hauptmerkmale:
- in Hellblau die Bildgröße mit Rand
- in Rosa der negative Abstand zwischen Bildern
- in Grün: Rand eines Bildes
Natürlich könnte es großartig sein, das Ergebnis mit diesen Attributen anpassbar zu machen.
Negativer Abstand
Bevor ich mit diesem kleinen POC in Jetpack Compose begann, wusste ich, dass RowComposable Column-Funktionen eine negative Anordnung ermöglichen.
Lassen Sie es uns verwenden und ProfilePicturegleichzeitig ein Composable starten:
val size = 48.dp
val margin = 6.dp
val spacing = 12.dp
// Easy solution
Row(horizontalArrangement = Arrangement.spacedBy(-spacing.dp)) {
list.forEachIndexed { index, item ->
val enable = index != (list.size - 1)
ProfilePicture(
url = item.url,
imageSize = size,
margin = margin.dp,
negativeSpacing = spacing.dp,
cropped = enable
)
}
}
Die Profilbilder überlappen sich und das letzte Bild liegt über den anderen. Großartig.
Aber bisher haben Sie möglicherweise noch nichts gelernt, und es gibt keine Leerzeichen zwischen den Elementen.
Versuchen wir hinzuzufügen ...
…Bildrand
Einen Rahmen verwenden
Mein erster Versuch, einen Rand hinzuzufügen, bestand darin, alle Bilder in einem Boxmit einem zu kapseln Modifier.border(…).
@Composable
fun WithBorder() {
// Hard coded stuff
val size = 48.dp
val strokeSize = 4
val containerColor = MaterialTheme.colorScheme.tertiary
val avatarBackgroundColor = MaterialTheme.colorScheme.secondaryContainer
Box(
modifier = Modifier
.border(
border = BorderStroke(strokeSize.dp, containerColor),
shape = CircleShape
)
) {
// This box represent the profile picture
Box(
modifier = Modifier
.clip(CircleShape)
.padding(strokeSize.dp)
.background(
color = avatarBackgroundColor,
shape = CircleShape
)
.size(size)
) {}
}
}
@Preview
@Composable
fun WithBorderPreview() {
MaterialTheme {
Row(
horizontalArrangement = Arrangement.spacedBy(-16.dp),
) {
(0..5).forEach {
WithBorder()
}
}
}
}
1) Der Rand fügt einen Abstand zwischen den Elementen hinzu. Das ist kein großes Problem, da ich das Ergebnis mit dem negativen Abstand neu berechnen kann.
Aber es erhöht auch den Spielraum für Rowsich.
Ich habe versucht, den Offset zu verschieben und die Grenzen zu aktualisieren, Rowaber es war nicht erfolgreich.
2) Der Rand ist schlicht, nicht ausschließend. Das bedeutet, dass die Rahmenfarbe jedes Mal angezeigt wird.
Falls der Hintergrund dahinter Rownicht bekannt ist oder kein fester Hintergrund, sondern ein Farbverlauf ist, kann dies ärgerlich sein.
Ich entscheide mich, diese technische Lösung zu überspringen.
Zeichnen in das DrawScope
Die zweite Möglichkeit besteht darin, einen negativen Teil jedes Bildes außer dem letzten zu löschen.
Mit den drei Parametern (Größe, Rand, Abstand) kann ich den auszuschließenden Kreis in perfekter Größe platzieren.
Zu diesem Zweck kann ich drawWithCacheModifikator und onDrawWithContentMethode verwenden. Sie bieten Tools zum Anzeigen von Inhalten über oder unter dem geänderten Composable.
Auf diese Weise kann ich den Unterschied zwischen dem aktuellen Profilbild und einem zweiten ausschließen, bei dem:
Mitte x = size.width * 1,5 – negativeSpacingPx
Mitte y = size.height / 2
Radius = size.width / 2 + marginPx
Das Ergebnis ist die Kombination von graphicalLayer Modifierund drawWithCacheeiner Funktion wie:
.graphicsLayer {
// Ensure BlendMode.Clear strategy works
compositingStrategy = CompositingStrategy.Offscreen
}
.drawWithCache {
val path = Path().apply {
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 won't render anything
[email protected]()
}
val marginRadius = size.width / 2f + marginPx
val offset = size.width * 1.5f - negativeSpacingPx
drawCircle(
color = Color.Black,
radius = marginRadius,
center = Offset(x = offset, y = (size.height / 2f)),
blendMode = BlendMode.Clear,
)
}
}
Vielen Dank an die offizielle Dokumentation für die Tipps.
Machen Sie es mit einem Modifikator
Schließlich kann ich dies in eine Modifier-Erweiterung extrahieren, um es wiederverwendbar zu machen. Als Ergebnis habe ich 3 Hauptelemente erhalten.
Das Rowselbst mit wenigen Parametern
- Der (negative) Abstand zwischen Bildern
- Der Rand, der die Grenze zwischen zwei Profilbildern darstellt
- Die Bildgröße
Es werden die drei Parameter + die URL + ein boolescher Wert verwendet, um den Kreisausschluss anzuwenden. Wie bereits erwähnt, wird das letzte Element nicht beschnitten.
Ich arbeite mit Coil, um Bilder von der URL anzuzeigen.
Warum verkette ich Modifiers? Um es besser lesbar zu machen.
Und weil beim letzten Element wiederum nur das verwendet wird initialModifier.
Der applyIf-Modifikator
Nützliche Funktion zum Anwenden eines Modifierunter bestimmten Bedingungen.
Der CircleMask-Modifikator
Grundsätzlich finden Sie wieder Elemente, die vor ein paar Zeilen angezeigt wurden.
Beachten Sie, dass sich die sizeVerwendung im Inneren onDrawingWithContentauf die aktuelle Zeichenumgebung bezieht. Weitere Informationen finden Sie drawWithCachein der Dokumentation.CacheDrawScope
Viel Spaß mit allem, was hier geschrieben steht ↓
// Usage :
val spacing = 4.dp
val margin = 2.dp
val imageSize = 32.dp
Row(horizontalArrangement = Arrangement.spacedBy(-spacing.dp)) {
list.forEachIndexed { index, item ->
val enable = index != (list.size - 1)
val url = item.thumbUrl
ProfilePicture(
url = url,
imageSize = imageSize,
margin = margin,
negativeSpacing = spacing,
cropped = enable,
)
}
}
@Composable
fun ProfilePicture(
url: String,
imageSize: Dp,
margin: Dp = 0.dp,
negativeSpacing: Dp = 0.dp,
cropped: Boolean = false,
) {
val initialModifier = Modifier
.clip(CircleShape)
.size(imageSize)
.aspectRatio(1f)
val marginPx = margin.dpToPx()
val negativeSpacingPx = negativeSpacing.dpToPx()
AsyncImage(
modifier = initialModifier.applyIf(cropped) {
circleMask(marginPx, negativeSpacingPx)
},
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.crossfade(true)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
)
}
fun Modifier.applyIf(
condition: Boolean,
modifierFunction: Modifier.() -> Modifier,
) = this.run {
if (condition) {
this.modifierFunction()
} else {
this
}
}
fun Modifier.circleMask(
marginPx: Float,
negativeSpacingPx: Float,
) = then(
Modifier
.graphicsLayer {
// Ensure BlendMode.Clear strategy works
compositingStrategy = CompositingStrategy.Offscreen
}
.drawWithCache {
val path = Path().apply {
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 won't render anything
[email protected]()
}
val marginRadius = size.width / 2f + marginPx
val offset = size.width * 1.5f - negativeSpacingPx
drawCircle(
color = Color.Black,
radius = marginRadius,
center = Offset(x = offset, y = (size.height / 2f)),
blendMode = BlendMode.Clear,
)
}
},
)
Sie können im Kommentarbereich einen Beitrag leisten.
Ich kann diese Geschichte später aktualisieren, wenn wir sie gemeinsam verbessern.
- Twitter: @bapness
- YouTube: @baptistemobiledev

![Was ist überhaupt eine verknüpfte Liste? [Teil 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































