Immagini del profilo sovrapposte con Jetpack Compose Modifier
Perché un'immagine dipinge più di mille parole, immagino che con questa capirai cosa volevo ottenere quando ho iniziato a lavorare su una riga dell'immagine del profilo:
Vorrei visualizzare le immagini del profilo in modo che si sovrappongano al precedente. Non è una tendenza del design recente, ma è comunque elegante per il 2023.
Attributi principali:
- in azzurro, la dimensione dell'immagine con margine
- in rosa, la spaziatura negativa tra le immagini
- in verde: margine di un'immagine
Ovviamente potrebbe essere fantastico rendere il risultato personalizzabile con quegli attributi.
Spaziatura negativa
Prima di iniziare questo piccolo POC in Jetpack Compose, sapevo che Rowle Columnfunzioni componibili consentono la disposizione negativa.
Usiamolo e iniziamo un ProfilePicturecomponibile allo stesso tempo:
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
)
}
}
Le immagini del profilo sono sovrapposte e l'ultima è sopra le altre. Grande.
Ma finora potresti non aver imparato nulla e non c'è spazio vuoto tra gli elementi.
Proviamo ad aggiungere...
…Margine dell'immagine
Usando un bordo
Il mio primo tentativo di aggiungere margine è stato quello di incapsulare ogni immagine all'interno di un Boxfile 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 ) Il bordo aggiunge margine tra gli elementi. Non è un grosso problema perché posso ricalcolare il risultato con la spaziatura negativa.
Ma aggiunge anche margine allo Rowstesso.
Ho provato a spostare l'offset e ad aggiornare i limiti di Rowma non ha avuto successo.
2) Il bordo è semplice, non escluso. Significa che il colore del bordo viene visualizzato ogni volta.
Nel caso in cui lo sfondo dietro Rownon sia noto, o non sia solido ma sfumato, questo può essere fastidioso.
Scelgo di saltare questa soluzione tecnica.
Disegno nel DrawScope
La seconda opzione è eliminare una parte negativa di ogni immagine, tranne l'ultima.
Con i tre parametri (dimensione, margine, spaziatura), posso posizionare il cerchio perfettamente dimensionato da escludere.
A tale scopo, posso utilizzare drawWithCacheModifier e onDrawWithContentmethod. Offrono strumenti per visualizzare il contenuto sopra o sotto il componibile modificato.
In questo modo, posso escludere la differenza tra l'attuale immagine del profilo e una seconda in cui:
center x = size.width * 1.5 — negativeSpacingPx
center y = size.height / 2
radius = size.width / 2 + marginPx
Il risultato è la combinazione di graphicalLayer Modifiere drawWithCachefunziona come:
.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,
)
}
}
Grazie alla documentazione ufficiale per i suggerimenti.
Fallo con un modificatore
Infine, posso estrarlo in un'estensione Modifier per renderlo riutilizzabile. Di conseguenza, ho ottenuto 3 elementi principali.
La Rowstessa con pochi parametri
- La spaziatura (negativa) tra le immagini
- Il margine, che rappresenta il confine tra due immagini del profilo
- La dimensione dell'immagine
Utilizzerà i tre parametri + l'url + un valore booleano per applicare l'esclusione del cerchio. Come accennato in precedenza, l'ultimo elemento non verrà ritagliato.
Sto lavorando con Coil per visualizzare l'immagine dall'URL.
Perché incateno Modifiers? Per renderlo più leggibile.
E poiché, ancora una volta, dell'ultimo elemento utilizzerà solo l'estensione initialModifier.
Il modificatore applyIf
Funzione utile per applicare un Modifierin determinate condizioni.
Il modificatore circleMask
In sostanza, troverai gli elementi di ritorno visti poche righe fa.
Si noti che l' sizeusato all'interno di onDrawingWithContentè correlato all'ambiente di disegno corrente. Vedere drawWithCachee CacheDrawScopedocumentazione per ulteriori informazioni.
Per favore, goditi tutto ciò che è scritto proprio qui ↓
// 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,
)
}
},
)
Puoi contribuire nella sezione dei commenti.
Potrei aggiornare questa storia più tardi se la miglioriamo insieme.
- Twitter: @bapness
- YouTube: @baptistemobiledev

![Che cos'è un elenco collegato, comunque? [Parte 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































