if anyone can tell me how to draw this shape
with an inside text, id greatly appreciate it.
Is there a way to do it in regular xml or any android api
You can use the Canvas to draw any shapes. Below is a sample implementation as per your required shape which you can customize.
Box {
Canvas(
modifier = Modifier
.size(200.dp)
.padding(40.dp)
) {
val trianglePath = Path().let {
it.moveTo(this.size.width * .40f, 0f)
it.lineTo(this.size.width * .50f, -30f)
it.lineTo(this.size.width * .60f, 0f)
it.close()
it
}
drawRoundRect(
Color.LightGray,
size = Size(this.size.width, this.size.height * 0.95f),
cornerRadius = CornerRadius(60f)
)
drawPath(
path = trianglePath,
Color.LightGray,
)
}
}
Easiest way you can do it with shadow and border properties is to create a custom Shape.
If you draw to Canvas alone you will also need to draw shadow and border properties, and need to create offsets inside your Composables bounds.
With GenericShape you can create a Path with roundedRect and add a triangle to top with
fun getBubbleShape(
density: Density,
cornerRadius: Dp,
arrowWidth: Dp,
arrowHeight: Dp,
arrowOffset: Dp
): GenericShape {
val cornerRadiusPx: Float
val arrowWidthPx: Float
val arrowHeightPx: Float
val arrowOffsetPx: Float
with(density) {
cornerRadiusPx = cornerRadius.toPx()
arrowWidthPx = arrowWidth.toPx()
arrowHeightPx = arrowHeight.toPx()
arrowOffsetPx = arrowOffset.toPx()
}
return GenericShape { size: Size, layoutDirection: LayoutDirection ->
this.addRoundRect(
RoundRect(
rect = Rect(
offset = Offset(0f, arrowHeightPx),
size = Size(size.width, size.height - arrowHeightPx)
),
cornerRadius = CornerRadius(cornerRadiusPx, cornerRadiusPx)
)
)
moveTo(arrowOffsetPx, arrowHeightPx)
lineTo(arrowOffsetPx + arrowWidthPx / 2, 0f)
lineTo(arrowOffsetPx + arrowWidthPx, arrowHeightPx)
}
}
If you need to have borders instead of addRoundRect you will need arcs and lines to draw for connecting arrow and lines you can refer this answer for drawing rounded rectangle using arcs and lines. If you set border with current setup you will understand what i mean
Then use it as
#Composable
private fun BubbleShapeSample() {
val density = LocalDensity.current
val arrowHeight = 16.dp
val bubbleShape = remember {
getBubbleShape(
density = density,
cornerRadius = 12.dp,
arrowWidth = 20.dp,
arrowHeight = arrowHeight,
arrowOffset = 30.dp
)
}
Column(
modifier = Modifier
.shadow(5.dp, bubbleShape)
.background(Color.White)
.padding(8.dp)
) {
Spacer(modifier = Modifier.height(arrowHeight))
Row(modifier = Modifier.padding(12.dp)) {
Icon(
modifier = Modifier.size(60.dp),
imageVector = Icons.Default.NotificationsActive,
contentDescription = "",
tint = Color(0xffFFC107)
)
Spacer(modifier = Modifier.width(20.dp))
Text(
"Get updates\n" +
"on questions\n" +
"and answers",
fontSize = 20.sp
)
Spacer(modifier = Modifier.width(20.dp))
Icon(
imageVector = Icons.Default.Close,
contentDescription = ""
)
}
}
}
Related
Is there a way to have an Icon (with ImageVector) component with a shadow/elevation in Jetpack Compose?
I want to make an IconButton with an elevated Icon but there seems to be no solution available for this problem. Things like Modifier.shadow() will only draw a shadow box around my icon and the Icon component itself has no elevation parameter.
This ticket seems like a duplicate of How to add a shadow / border / elevation to an icon in Jetpack Compose at first glance, but that ticket is not referring to the Icon component in combination with an ImageVector. Also, the proposed solution does not work and it wasn't updated in 6 months.
To further clarify, I want my Icon to look like this:
What you require is a library that converts imageVectors or xml files into Path. As i know of there is no built-in library for this. There are probably few out there that converts into Path or Shape.
When you have a shape or path what you need to do is draw with this shape as Modifier or into Canvas
fun Modifier.vectorShadow(
path: Path,
x: Dp,
y: Dp,
radius: Dp
) = composed(
inspectorInfo = {
name = "vectorShadow"
value = path
value = x
value = y
value = radius
},
factory = {
val paint = remember {
Paint()
}
val frameworkPaint = remember {
paint.asFrameworkPaint()
}
val color = Color.DarkGray
val dx: Float
val dy: Float
val radiusInPx: Float
with(LocalDensity.current) {
dx = x.toPx()
dy = y.toPx()
radiusInPx = radius.toPx()
}
drawBehind {
this.drawIntoCanvas {
val transparent = color
.copy(alpha = 0f)
.toArgb()
frameworkPaint.color = transparent
frameworkPaint.setShadowLayer(
radiusInPx,
dx,
dy,
color
.copy(alpha = .7f)
.toArgb()
)
it.drawPath(path, paint)
}
}
}
)
Usage
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
) {
val center = with(LocalDensity.current) {
150.dp.toPx()
}
val path1 = createPolygonPath(center, center, 6, center)
val path2 = createPolygonPath(center, center, 5, center)
Canvas(
modifier = Modifier
.size(300.dp)
.vectorShadow(path1, 0.dp, 0.dp, 6.dp)
.border(3.dp, Color.Green)
) {
drawPath(path1, Color.White)
}
Spacer(modifier = Modifier.height(10.dp))
Canvas(
modifier = Modifier
.size(300.dp)
.vectorShadow(path2, 3.dp, 3.dp, 10.dp)
.border(3.dp, Color.Green)
) {
drawPath(path2, Color.White)
}
}
Result
createPolygonPath is a sample function to create Path. If you manage to convert your vector to Path rest is simple.
fun createPolygonPath(cx: Float, cy: Float, sides: Int, radius: Float): Path {
val angle = 2.0 * Math.PI / sides
return Path().apply {
moveTo(
cx + (radius * cos(0.0)).toFloat(),
cy + (radius * sin(0.0)).toFloat()
)
for (i in 1 until sides) {
lineTo(
cx + (radius * cos(angle * i)).toFloat(),
cy + (radius * sin(angle * i)).toFloat()
)
}
close()
}
}
It's not exactly what you want but for elevating an icon you can simply do this:
Icon(
Icons.Outlined.Refresh, contentDescription = "back",
modifier = Modifier
.size(300.dp)
.offset(10.dp, 10.dp), tint = Color(0, 0, 0, 40)
)
Icon(
Icons.Outlined.Refresh, contentDescription = "front",
modifier = Modifier.size(300.dp), tint = Color(0xFFb6d7a8)
)
The problem is that it is lacking the blurring effect.
I had a BadgeView written with View using onMeasure, onLayout and OnDraw
I'm trying to migrate this View to Jetpack Compose.
Since drawing shapes is easier with compose i thought there is no need to use canvas or Layout functions at all, but size of Text or Surface wrapping it is not set properly before text size is calculated, and circle is not drawn properly.
Also checked out Badge component, it uses static sizes BadgeWithContentRadius, since in my design size depends on text size it's not possible to set a static size.
Surface(
shape = CircleShape,
contentColor = Color.White,
color = Color.Red
) {
Text(
text = "0",
modifier = Modifier.padding(4.dp),
fontSize = 34.sp,
)
}
Then tried using
var size: Dp by remember { mutableStateOf(40.dp) }
val density = LocalDensity.current
Surface(
shape = CircleShape,
modifier = Modifier.size(size),
contentColor = Color.Yellow,
color = Color.Red
){
Text(
text = "0",
modifier = Modifier.padding(4.dp),
fontSize = 24.sp,
onTextLayout = { textLayoutResult: TextLayoutResult ->
val textSize = textLayoutResult.size
val circleRadius = textSize.width.coerceAtLeast(textSize.height)
size = with(density) {
circleRadius.toDp()
}
println("Size: $size")
}
)
}
Both of the implementations are not working, then tried doing it with Layout
#Composable
private fun Badge(text: String, badgeState: BadgeState, modifier: Modifier = Modifier) {
Surface(shape = CircleShape, color = Color.Red, contentColor = Color.White) {
BadgeLayout(text = text, badgeState = badgeState, modifier = modifier)
}
}
#Composable
private fun BadgeLayout(text: String, badgeState: BadgeState, modifier: Modifier = Modifier) {
var circleRadius = 0
var size: IntSize by remember {
mutableStateOf(IntSize(0, 0))
}
val content = #Composable {
Text(
text = text,
modifier = Modifier.padding(4.dp),
fontSize = 34.sp,
onTextLayout = { textLayoutResult: TextLayoutResult ->
size = textLayoutResult.size
circleRadius = size.width.coerceAtLeast(size.height)
},
)
}
Layout(
modifier = modifier,
content = content
) { measurables: List<Measurable>, constraints: Constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
println("🔥 Badge: $circleRadius, size: $size")
layout(width = circleRadius, height = circleRadius) {
placeables.first().placeRelative(0, 0)
}
}
}
Shape seems to be applied correctly but couldn't find exact way to get text size to set number to center of Surface or Text.
How can a component, should have circle shape when it's one or digit number then turning it into RoundedCornerShape can be implemented with considering performance be implemented?
I've made the following modifier using Modifier.layout:
fun Modifier.badgeLayout() =
layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
// based on the expectation of only one line of text
val minPadding = placeable.height / 4
val width = maxOf(placeable.width + minPadding, placeable.height)
layout(width, placeable.height) {
placeable.place((width - placeable.width) / 2, 0)
}
}
Usage:
Text(
text,
modifier = Modifier
.background(MaterialTheme.colors.error, shape = CircleShape)
.badgeLayout()
)
Result:
I would look into using the Material Badge that is already available for Compose:
Material Badge for Compose
Solution is to use textHeight with onTextLayout callback of Text. Since placeable.height returns full text height with font padding
onTextLayout = { textLayoutResult: TextLayoutResult ->
textSize = textLayoutResult.size
// 🔥🔥 This is text height without padding, result size returns height with font padding
textHeight = textLayoutResult.firstBaseline.toInt()
}
Layout implementation is as
#Composable
fun Badge(
modifier: Modifier = Modifier,
badgeState: BadgeState = rememberBadgeState(),
) {
BadgeComponent(badgeState = badgeState, modifier = modifier)
}
#Composable
private fun BadgeComponent(badgeState: BadgeState, modifier: Modifier = Modifier) {
// TODO Question: Why does this not survive recompositions without mutableState?
var textSize = remember { IntSize(0, 0) }
var textHeight = remember(badgeState) { 0 }
var badgeHeight = remember { 0 }
val density = LocalDensity.current
val text = badgeState.text
val isCircleShape = badgeState.isCircleShape
val shape =
if (isCircleShape) CircleShape else RoundedCornerShape(badgeState.roundedRadiusPercent)
println(
"✅ BadgeComponent: text: $text, " +
"isCircleShape: $isCircleShape, " +
"textHeight: $textHeight, " +
"badgeHeight: $badgeHeight, " +
"textSize: $textSize"
)
val content = #Composable {
Text(
text = badgeState.text,
color = badgeState.textColor,
fontSize = badgeState.fontSize,
lineHeight = badgeState.fontSize,
onTextLayout = { textLayoutResult: TextLayoutResult ->
textSize = textLayoutResult.size
// 🔥🔥 This is text height without padding, result size returns height with font padding
textHeight = textLayoutResult.firstBaseline.toInt()
println("✏️ BadgeComponent textHeight: $textHeight, textSize: $textSize")
},
)
}
val badgeModifier = modifier
.materialShadow(badgeState = badgeState)
.then(
badgeState.borderStroke?.let { borderStroke ->
modifier.border(borderStroke, shape = shape)
} ?: modifier
)
.background(
badgeState.backgroundColor,
shape = shape
)
Layout(
modifier = badgeModifier,
content = content
) { measurables: List<Measurable>, constraints: Constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
val placeable = placeables.first()
if (badgeHeight == 0) {
// Space above and below text, this is drawing area + empty space
val verticalSpaceAroundText = with(density) {
textHeight * .12f + 6 + badgeState.verticalPadding.toPx()
}
badgeHeight = (textHeight + 2 * verticalSpaceAroundText).toInt()
if (isCircleShape) {
// Use bigger dimension to have circle that covers 2 digit counts either
badgeHeight = textSize.width.coerceAtLeast(badgeHeight)
layout(width = badgeHeight, height = badgeHeight) {
placeable.placeRelative(
(badgeHeight - textSize.width) / 2,
(badgeHeight - textSize.height) / 2
)
}
} else {
// Space left and right of the text, this is drawing area + empty space
val horizontalSpaceAroundText = with(density) {
textHeight * .12f + 6 + badgeState.horizontalPadding.toPx()
}
val width = (textSize.width + 2 * horizontalSpaceAroundText).toInt()
layout(width = width, height = badgeHeight) {
placeable.placeRelative(
x = (width - textSize.width) / 2,
y = (-textSize.height + badgeHeight) / 2
)
}
}
}
}
Also used custom Modifier and rememberable to set colored shadow, paddings, font properties, shapes and more which full implementation can be found in github repository.
Final result
I'm trying to achieve this custom shape with Compose
But for some reason the separator offseted circle is drawn with a dotted line and here is the code
#Preview
#Composable
private fun ReceiptSeparator () {
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp) ,
verticalAlignment = Alignment.CenterVertically ,) {
Box(
modifier = Modifier
.requiredSize(50.dp)
.background(Color.White)
.offset(-40.dp)
.clip(CircleShape)
.border(BorderStroke(2.dp, Color.Gray))
){}
Box(
Modifier
.height(1.dp)
.requiredWidth(250.dp)
.weight(3f)
.background(Color.Gray, shape = DottedShape(step = 20.dp))
){}
Box(
modifier = Modifier
.offset(40.dp)
.clip(CircleShape)
.border(BorderStroke(2.dp, Color.Gray))
.background(Color.White)
.size(50.dp)
){}
}
}
Why the circle is drawn with a dotted line and how to achieve this shape correctly?
Your circle is not drawn correctly, because Modifier.border draws a rectangle border by default, and then you clip it with your Modifier.clip. Instead, if you need to apply shape to the border, you need to pass the shape into Modifier.border, like this:
.border(BorderStroke(2.dp, Color.Gray), shape = CircleShape)
But this won't solve your problem. To draw the shadow correctly like shown in your image, you need to apply a custom Shape to your container.
You can use Modifier.onGloballyPositioned to get position of your cutoffs:
var separatorOffsetY by remember { mutableStateOf<Float?>(null) }
val cornerRadius = 20.dp
Card(
shape = RoundedCutoutShape(separatorOffsetY, cornerRadius),
backgroundColor = Color.White,
modifier = Modifier.padding(10.dp)
) {
Column {
Box(modifier = Modifier.height(200.dp))
Box(
Modifier
.padding(horizontal = cornerRadius)
.height(1.dp)
.requiredWidth(250.dp)
// DottedShape is taken from this answer:
// https://stackoverflow.com/a/68789205/3585796
.background(Color.Gray, shape = DottedShape(step = 20.dp))
.onGloballyPositioned {
separatorOffsetY = it.boundsInParent().center.y
}
)
Box(modifier = Modifier.height(50.dp))
}
}
Using this information you can create a shape like following:
class RoundedCutoutShape(
private val offsetY: Float?,
private val cornerRadiusDp: Dp,
) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density,
) = Outline.Generic(run path#{
val cornerRadius = with(density) { cornerRadiusDp.toPx() }
val rect = Rect(Offset.Zero, size)
val mainPath = Path().apply {
addRoundRect(RoundRect(rect, CornerRadius(cornerRadius)))
}
if (offsetY == null) return#path mainPath
val cutoutPath = Path().apply {
val circleSize = Size(cornerRadius, cornerRadius) * 2f
val visiblePart = 0.25f
val leftOval = Rect(
offset = Offset(
x = 0 - circleSize.width * (1 - visiblePart),
y = offsetY - circleSize.height / 2
),
size = circleSize
)
val rightOval = Rect(
offset = Offset(
x = rect.width - circleSize.width * visiblePart,
y = offsetY - circleSize.height / 2
),
size = circleSize
)
addOval(leftOval)
addOval(rightOval)
}
return#path Path().apply {
op(mainPath, cutoutPath, PathOperation.Difference)
}
})
}
Result:
Get rid of:
shape = DottedShape(step = 20.dp)
I want to add border on bottom of the layout. I know i can use Divider composable but i just want to learn how to draw a border.
Currently, I can add border for all sides which is not what I want.
Row(
modifier = Modifier
.border(border = BorderStroke(width = 1.dp, Color.LightGray))
) {
TextField(value = "", onValueChange = {}, modifier = Modifier.weight(1f))
Switch(checked = true, onCheckedChange = {})
Icon(Icons.Filled.Close, "Remove", tint = Color.Gray)
}
You can use the drawBehind modifier to draw a line.
Something like:
Row(
modifier = Modifier
.drawBehind {
val strokeWidth = indicatorWidth.value * density
val y = size.height - strokeWidth / 2
drawLine(
Color.LightGray,
Offset(0f, y),
Offset(size.width, y),
strokeWidth
)
}){
//....
}
If you prefer you can build your custom Modifier with the same code above
fun Modifier.bottomBorder(strokeWidth: Dp, color: Color) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
Modifier.drawBehind {
val width = size.width
val height = size.height - strokeWidthPx/2
drawLine(
color = color,
start = Offset(x = 0f, y = height),
end = Offset(x = width , y = height),
strokeWidth = strokeWidthPx
)
}
}
)
and then just apply it:
Row(
modifier = Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth()
.bottomBorder(1.dp, DarkGray)
){
//Row content
}
You can draw a line in a draw scope. In my opinion, a divider looks cleaner in code.
Row(modifier = Modifier
.drawWithContent {
drawContent()
clipRect { // Not needed if you do not care about painting half stroke outside
val strokeWidth = Stroke.DefaultMiter
val y = size.height // - strokeWidth
// if the whole line should be inside component
drawLine(
brush = SolidColor(Color.Red),
strokeWidth = strokeWidth,
cap = StrokeCap.Square,
start = Offset.Zero.copy(y = y),
end = Offset(x = size.width, y = y)
)
}
}
) {
Text("test")
}
Yeah this oughta do it:-
#Suppress("UnnecessaryComposedModifier")
fun Modifier.topRectBorder(width: Dp = Dp.Hairline, brush: Brush = SolidColor(Color.Black)): Modifier = composed(
factory = {
this.then(
Modifier.drawWithCache {
onDrawWithContent {
drawContent()
drawLine(brush, Offset(width.value, 0f), Offset(size.width - width.value, 0f))
}
}
)
},
inspectorInfo = debugInspectorInfo {
name = "border"
properties["width"] = width
if (brush is SolidColor) {
properties["color"] = brush.value
value = brush.value
} else {
properties["brush"] = brush
}
properties["shape"] = RectangleShape
}
)
You can define a rectangular Shape on the bottom of your element, using the bottom line thickness as parameter:
private fun getBottomLineShape(bottomLineThickness: Float) : Shape {
return GenericShape { size, _ ->
// 1) Bottom-left corner
moveTo(0f, size.height)
// 2) Bottom-right corner
lineTo(size.width, size.height)
// 3) Top-right corner
lineTo(size.width, size.height - bottomLineThickness)
// 4) Top-left corner
lineTo(0f, size.height - bottomLineThickness)
}
}
And then use it in the border modifier like this:
val lineThickness = with(LocalDensity.current) {[desired_thickness_in_dp].toPx()}
Row(
modifier = Modifier
.height(rowHeight)
.border(width = lineThickness,
color = Color.Black,
shape = getBottomLineShape(lineThickness))
) {
// Stuff in the row
}
Using a "Divider" worked for me,
Column {
Divider (
color = Color.White,
modifier = Modifier
.height(1.dp)
.fillMaxHeight()
.fillMaxWidth()
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
) {
// Something else
}
}
I can easily create a normal border using the Modifier.border() but how to create a dashed border as shown in the image below.
There isn't a parameter in Modifier.border() to achieve a dashed path.
However you can use a DrawScope to draw a dashed Path using PathEffect.dashPathEffect.
Something like:
val stroke = Stroke(width = 2f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
)
You can draw it using the drawBehind modifier:
Box(
Modifier
.size(250.dp,60.dp)
.drawBehind {
drawRoundRect(color = Color.Red, style = stroke)
},
contentAlignment = Alignment.Center
) {
Text(textAlign = TextAlign.Center,text = "Tap here to introduce yourseft")
}
If you want rounded corner just use the cornerRadius attribute in the drawRoundRect method:
drawRoundRect(color = Color.Red,style = stroke, cornerRadius = CornerRadius(8.dp.toPx()))
If you prefer you can build your custom Modifier with the same code above. Something like:
fun Modifier.dashedBorder(strokeWidth: Dp, color: Color, cornerRadiusDp: Dp) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
val cornerRadiusPx = density.run { cornerRadiusDp.toPx() }
this.then(
Modifier.drawWithCache {
onDrawBehind {
val stroke = Stroke(
width = strokeWidthPx,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
)
drawRoundRect(
color = color,
style = stroke,
cornerRadius = CornerRadius(cornerRadiusPx)
)
}
}
)
}
)
and then just apply it:
Box(
Modifier
.size(250.dp,60.dp)
.dashedBorder(1.dp, Red, 8.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "Tap here to introduce yourself",
textAlign = TextAlign.Center,
)
}
After some digging in the normal border modifier, I found out that it uses Stroke object which can take a parameter PathEffect that can make it dashed, here is a modified version of the normal border function that takes this parameter.
https://gist.github.com/DavidIbrahim/236dadbccd99c4fd328e53587df35a21
I wrote this extension for the Modifier you can simply use it or modify it.
fun Modifier.dashedBorder(width: Dp, radius: Dp, color: Color) =
drawBehind {
drawIntoCanvas {
val paint = Paint()
.apply {
strokeWidth = width.toPx()
this.color = color
style = PaintingStyle.Stroke
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
}
it.drawRoundRect(
width.toPx(),
width.toPx(),
size.width - width.toPx(),
size.height - width.toPx(),
radius.toPx(),
radius.toPx(),
paint
)
}
}