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.
Related
Problem description
I'm trying to create a component on android using Compose and Canvas that simulates a 7-segment display like this:
For that, I adopted a strategy of creating only half of this component and mirroring this part that I created downwards, so I would have the entire display.
This is the top part of the 7-segment display:
But the problem is when "mirror" the top to bottom. It turns out that when I add the Modifier.rotate(180f) the figure rotates around the origin of the canvas clockwise, and so it doesn't appear on the screen (it would if it were counterclockwise).
I don't want to do this solution using a font for this, I would like to solve this problem through the canvas and compose itself. If there is a smarter way to do this on canvas without necessarily needing a mirror I would like to know.
My code
Below is my code that I'm using to draw this:
DisplayComponent.kt
#Composable
fun DisplayComponent(
modifier: Modifier = Modifier,
size: Int = 1000,
color: Color = MaterialTheme.colors.primary,
) {
Column(modifier = modifier) {
HalfDisplayComponent(size, color)
HalfDisplayComponent(
modifier = Modifier.rotate(180f),
size = size,
color = color
)
}
}
#Composable
private fun HalfDisplayComponent(
size: Int,
color: Color,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
LedModel.values().forEach {
LedComponent(
ledModel = it,
size = size,
color = color
)
}
}
}
LedModel.kt
enum class LedModel(val coordinates: List<Pair<Float, Float>>) {
HorizontalTop(
listOf(
Pair(0.04f, 0.03f), // Point A
Pair(0.07f, 0f), // Point B
Pair(0.37f, 0f), // Point C
Pair(0.4f, 0.03f), // Point D
Pair(0.34f, 0.08f), // Point E
Pair(0.1f, 0.08f), // Point F
)
),
VerticalRight(
listOf(
Pair(0.41f, 0.04f), // Point A
Pair(0.44f, 0.07f), // Point B
Pair(0.44f, 0.37f), // Point C
Pair(0.41f, 0.4f), // Point D
Pair(0.35f, 0.35f), // Point E
Pair(0.35f, 0.09f), // Point F
)
),
VerticalLeft(
listOf(
Pair(0.03f, 0.4f), // Point A
Pair(0f, 0.37f), // Point B
Pair(0f, 0.07f), // Point C
Pair(0.03f, 0.04f), // Point D
Pair(0.09f, 0.09f), // Point E
Pair(0.09f, 0.35f), // Point F
)
),
HorizontalBottom(
listOf(
Pair(0.1f, 0.36f), // Point A
Pair(0.34f, 0.36f), // Point B
Pair(0.39f, 0.4f), // Point C
Pair(0.05f, 0.4f), // Point D
)
),
}
LedComponent.kt
#Composable
fun LedComponent(
modifier: Modifier = Modifier,
size: Int = 30,
color: Color = MaterialTheme.colors.primary,
ledModel: LedModel = LedModel.HorizontalTop
) = getPath(ledModel.coordinates).let { path ->
Canvas(modifier = modifier.scale(size.toFloat())) {
drawPath(path, color)
}
}
private fun getPath(coordinates: List<Pair<Float, Float>>): Path = Path().apply {
coordinates.map {
transformPointCoordinate(it)
}.forEachIndexed { index, point ->
if (index == 0) moveTo(point.x, point.y) else lineTo(point.x, point.y)
}
}
private fun transformPointCoordinate(point: Pair<Float, Float>) =
Offset(point.first.dp.value, point.second.dp.value)
My failed attempt
As described earlier, I tried adding a Modifier by rotating the composable of the display but it didn't work. I did it this way:
#Composable
fun DisplayComponent(
modifier: Modifier = Modifier,
size: Int = 1000,
color: Color = MaterialTheme.colors.primary,
) {
Column(modifier = modifier) {
DisplayFABGComponent(size, color)
DisplayFABGComponent(
modifier = Modifier.rotate(180f),
size = size,
color = color
)
}
}
There are many things wrong with the code you posted above.
First of all in Jetpack Compose even if your Canvas has 0.dp size you can still draw anywhere which is the first issue in your question. Your Canvas has no size modifier, which you can verify by printing DrawScope.size as below.
fun LedComponent(
modifier: Modifier = Modifier,
size: Int = 1000,
color: Color = MaterialTheme.colorScheme.primary,
ledModel: LedModel = LedModel.HorizontalTop
) = getPath(ledModel.coordinates).let { path ->
Canvas(
modifier = modifier.scale(size.toFloat())
) {
println("CANVAS size: ${this.size}")
drawPath(path, color)
}
}
any value you enter makes no difference other than Modifier.scale(0f), also this is not how you should build or scale your drawing either.
If you set size for your Canvas such as
#Composable
fun DisplayComponent(
modifier: Modifier = Modifier,
size: Int = 1000,
color: Color = MaterialTheme.colorScheme.primary,
) {
Column(modifier = modifier) {
HalfDisplayComponent(
size,
color,
Modifier
.size(200.dp)
.border(2.dp,Color.Red)
)
HalfDisplayComponent(
modifier = Modifier
.size(200.dp)
.border(2.dp, Color.Cyan)
.rotate(180f),
size = size,
color = color
)
}
}
Rotation works but what you draw is not symmetric as in image in your question.
point.first.dp.value this snippet does nothing. What it does is adds dp to float then gets float. This is not how you do float/dp conversions and which is not necessary either.
You can achieve your goal with one Canvas or using Modifier.drawBehind{}. Create a Path using Size as reference for half component then draw again and rotate it or create a path that contains full led component. Or you can have paths for each sections if you wish show LED digits separately.
This is a simple example to build only one diamond shape, then translate and rotate it to build hourglass like shape using half component. You can use this sample as demonstration for how to create Path using Size as reference, translate and rotate.
fun getHalfPath(path: Path, size: Size) {
path.apply {
val width = size.width
val height = size.height / 2
moveTo(width * 0f, height * .5f)
lineTo(width * .3f, height * 0.3f)
lineTo(width * .7f, height * 0.3f)
lineTo(width * 1f, height * .5f)
lineTo(width * .5f, height * 1f)
lineTo(width * 0f, height * .5f)
}
}
You need to use aspect ratio of 1/2f to be able to have symmetric drawing. Green border is to show bounds of Box composable.
val path = remember {
Path()
}
Box(modifier = Modifier
.border(3.dp, Color.Green)
.fillMaxWidth(.4f)
.aspectRatio(1 / 2f)
.drawBehind {
if (path.isEmpty) {
getHalfPath(path, size)
}
drawPath(
path = path,
color = Color.Red,
style = Stroke(2.dp.toPx())
)
withTransform(
{
translate(0f, size.height / 2f)
rotate(
degrees = 180f,
pivot = Offset(center.x, center.y / 2)
)
}
) {
drawPath(
path = path,
color = Color.Black,
style = Stroke(2.dp.toPx())
)
}
}
Result
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 = ""
)
}
}
}
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)
Since Jetpack Compose has this limitation I'm looking for workaround solution for this problem?
Maybe Canvas could do the trick? I really appreciate if someone can provide code snippets of how to render shadow in Jetpack Compose for Card, Box, Column, Row etc utilising additional parameters like X and Y offset, blur and opacity with custom implementation (Canvas or something else)?
I managed to find solution thanks to this code snippets
fun Modifier.advancedShadow(
color: Color = Color.Black,
alpha: Float = 1f,
cornersRadius: Dp = 0.dp,
shadowBlurRadius: Dp = 0.dp,
offsetY: Dp = 0.dp,
offsetX: Dp = 0.dp
) = drawBehind {
val shadowColor = color.copy(alpha = alpha).toArgb()
val transparentColor = color.copy(alpha = 0f).toArgb()
drawIntoCanvas {
val paint = Paint()
val frameworkPaint = paint.asFrameworkPaint()
frameworkPaint.color = transparentColor
frameworkPaint.setShadowLayer(
shadowBlurRadius.toPx(),
offsetX.toPx(),
offsetY.toPx(),
shadowColor
)
it.drawRoundRect(
0f,
0f,
this.size.width,
this.size.height,
cornersRadius.toPx(),
cornersRadius.toPx(),
paint
)
}
}
Based on the post above, I've changed the implementation to match the parameters of the site below
https://html-css-js.com/css/generator/box-shadow/
My current implementation is the following
internal fun Modifier.coloredShadow(
color: Color = Color.Black,
borderRadius: Dp = 0.dp,
blurRadius: Dp = 0.dp,
offsetY: Dp = 0.dp,
offsetX: Dp = 0.dp,
spread: Float = 0f,
modifier: Modifier = Modifier,
) = this.then(
modifier.drawBehind {
this.drawIntoCanvas {
val paint = Paint()
val frameworkPaint = paint.asFrameworkPaint()
val spreadPixel = spread.dp.toPx()
val leftPixel = (0f - spreadPixel) + offsetX.toPx()
val topPixel = (0f - spreadPixel) + offsetY.toPx()
val rightPixel = (this.size.width + spreadPixel)
val bottomPixel = (this.size.height + spreadPixel)
if (blurRadius != 0.dp) {
/*
The feature maskFilter used below to apply the blur effect only works
with hardware acceleration disabled.
*/
frameworkPaint.maskFilter =
(BlurMaskFilter(blurRadius.toPx(), BlurMaskFilter.Blur.NORMAL))
}
frameworkPaint.color = color.toArgb()
it.drawRoundRect(
left = leftPixel,
top = topPixel,
right = rightPixel,
bottom = bottomPixel,
radiusX = borderRadius.toPx(),
radiusY = borderRadius.toPx(),
paint
)
}
}
)
Feel free to add comments and help to evolve this gist
https://gist.github.com/hernandazevedo/dfd41b39d0156c740a195f6f5866ce20
I've recently created shadow modifier allowing to set both radius and offsets.
#Immutable
data class Shadow(
#Stable val offsetX: Dp,
#Stable val offsetY: Dp,
#Stable val radius: Dp,
#Stable val color: Color,
)
fun Modifier.withShadow(
shadow: Shadow,
shape: Shape,
) = drawBehind {
drawIntoCanvas { canvas ->
val paint = Paint()
paint.asFrameworkPaint().apply {
this.color = Color.Transparent.toArgb()
setShadowLayer(
radius = shadow.radius.toPx(),
dx = shadow.offsetX.toPx(),
dy = shadow.offsetY.toPx(),
shadowColor = shadow.color,
)
}
val outline = shape.createOutline(size, layoutDirection, this)
canvas.drawOutline(outline, paint)
}
}
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
)
}
}