According to the designs, our image views are supposed to add a label on top of the view. This label should contain text, such as '50%' to indicate that the item within the image has been discounted by 50%.
How would one draw such a label / ribbon on top of an existing image view within Jetpack Compose?
EDIT: The image used is an image from an URL, which is currently being loaded using 'AsyncImage' from Coil, if this matters
Thanks in advance!
Solving this requires simple math, Drawscope with rotation, TextMeasurer to measure text width and height. And composed Modifier if you wish to have these as Modifier. Built one with solid background another with shimmer effect to. You can check if painter.state is success before applying these modifiers.
Result
Modifier Implementations
fun Modifier.drawDiagonalLabel(
text: String,
color: Color,
style: TextStyle = TextStyle(
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White
),
labelTextRatio: Float = 7f
) = composed(
factory = {
val textMeasurer = rememberTextMeasurer()
val textLayoutResult: TextLayoutResult = remember {
textMeasurer.measure(text = AnnotatedString(text), style = style)
}
Modifier
.clipToBounds()
.drawWithContent {
val canvasWidth = size.width
val canvasHeight = size.height
val textSize = textLayoutResult.size
val textWidth = textSize.width
val textHeight = textSize.height
val rectWidth = textWidth * labelTextRatio
val rectHeight = textHeight * 1.1f
val rect = Rect(
offset = Offset(canvasWidth - rectWidth, 0f),
size = Size(rectWidth, rectHeight)
)
val sqrt = sqrt(rectWidth / 2f)
val translatePos = sqrt * sqrt
drawContent()
withTransform(
{
rotate(
degrees = 45f,
pivot = Offset(
canvasWidth - rectWidth / 2,
translatePos
)
)
}
) {
drawRect(
color = color,
topLeft = rect.topLeft,
size = rect.size
)
drawText(
textMeasurer = textMeasurer,
text = text,
style = style,
topLeft = Offset(
rect.left + (rectWidth - textWidth) / 2f,
rect.top + (rect.bottom - textHeight) / 2f
)
)
}
}
}
)
fun Modifier.drawDiagonalShimmerLabel(
text: String,
color: Color,
style: TextStyle = TextStyle(
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White
),
labelTextRatio: Float = 7f,
) = composed(
factory = {
val textMeasurer = rememberTextMeasurer()
val textLayoutResult: TextLayoutResult = remember {
textMeasurer.measure(text = AnnotatedString(text), style = style)
}
val transition = rememberInfiniteTransition()
val progress by transition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(3000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Modifier
.clipToBounds()
.drawWithContent {
val canvasWidth = size.width
val canvasHeight = size.height
val textSize = textLayoutResult.size
val textWidth = textSize.width
val textHeight = textSize.height
val rectWidth = textWidth * labelTextRatio
val rectHeight = textHeight * 1.1f
val rect = Rect(
offset = Offset(canvasWidth - rectWidth, 0f),
size = Size(rectWidth, rectHeight)
)
val sqrt = sqrt(rectWidth / 2f)
val translatePos = sqrt * sqrt
val brush = Brush.linearGradient(
colors = listOf(
color,
style.color,
color,
),
start = Offset(progress * canvasWidth, progress * canvasHeight),
end = Offset(
x = progress * canvasWidth + rectHeight,
y = progress * canvasHeight + rectHeight
),
)
drawContent()
withTransform(
{
rotate(
degrees = 45f,
pivot = Offset(
canvasWidth - rectWidth / 2,
translatePos
)
)
}
) {
drawRect(
brush = brush,
topLeft = rect.topLeft,
size = rect.size
)
drawText(
textMeasurer = textMeasurer,
text = text,
style = style,
topLeft = Offset(
rect.left + (rectWidth - textWidth) / 2f,
rect.top + (rect.bottom - textHeight) / 2f
)
)
}
}
}
)
Usage
Column(
modifier = Modifier
.background(backgroundColor)
.fillMaxSize()
.padding(20.dp)
) {
val painter1 = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.data("https://www.techtoyreviews.com/wp-content/uploads/2020/09/5152094_Cover_PS5.jpg")
.size(coil.size.Size.ORIGINAL) // Set the target size to load the image at.
.build()
)
Image(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(4 / 3f)
.then(
if (painter1.state is AsyncImagePainter.State.Success) {
Modifier.drawDiagonalLabel(
text = "50%",
color = Color.Red
)
} else Modifier
),
painter = painter1,
contentScale = ContentScale.FillBounds,
contentDescription = null
)
Spacer(modifier = Modifier.height(10.dp))
val painter2 = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.data("https://i02.appmifile.com/images/2019/06/03/03ab1861-42fe-4137-b7df-2840d9d3a7f5.png")
.size(coil.size.Size.ORIGINAL) // Set the target size to load the image at.
.build()
)
Image(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(4 / 3f)
.then(
if (painter2.state is AsyncImagePainter.State.Success) {
Modifier.drawDiagonalShimmerLabel(
text = "40% OFF",
color = Color(0xff4CAF50),
labelTextRatio = 5f
)
} else Modifier
),
painter = painter2,
contentScale = ContentScale.FillBounds,
contentDescription = null
)
}
@Composable
private fun RibbonSample() {
val text = "50%"
val textMeasurer = rememberTextMeasurer()
val style = TextStyle(
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White
)
val textLayoutResult: TextLayoutResult = remember {
textMeasurer.measure(text = AnnotatedString(text), style = style)
}
Box(
modifier = Modifier
.clipToBounds()
.drawWithContent {
val canvasWidth = size.width
val textSize = textLayoutResult.size
val textWidth = textSize.width
val textHeight = textSize.height
val rectWidth = textWidth * 7f
val rectHeight = textHeight * 1.1f
val rect = Rect(
offset = Offset(canvasWidth - rectWidth, 0f),
size = Size(rectWidth, rectHeight)
)
val translatePos = sqrt(rectWidth / 2f) * sqrt(rectWidth / 2f)
drawContent()
withTransform(
{
rotate(
degrees = 45f,
pivot = Offset(
canvasWidth - rectWidth / 2,
translatePos
)
)
}
) {
drawRect(
Color.Red,
topLeft = rect.topLeft,
size = rect.size
)
drawText(
textMeasurer = textMeasurer,
text = text,
style = style,
topLeft = Offset(
rect.left + (rectWidth - textWidth) / 2f,
rect.top + (rect.bottom - textHeight) / 2f
)
)
}
}
) {
Image(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(4 / 3f),
painter = painterResource(id = R.drawable.landscape1),
contentScale = ContentScale.FillBounds,
contentDescription = null
)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With