I have an image and I want to draw dark rectangle over it with a transparent circle, so the result will be something like this:
I have ended up with this code:
Box(modifier = Modifier
.clip(RectangleShape)
.fillMaxSize()
.background(Color.Black)
.pointerInput(Unit) {
detectTransformGestures { centroid, pan, zoom, rotation ->
scale *= zoom
}
}) {
Image(
modifier = Modifier
.align(Alignment.Center)
.graphicsLayer(
scaleX = maxOf(.2f, minOf(5f, scale)),
scaleY = maxOf(.2f, minOf(5f, scale))
),
bitmap = bitmap.asImageBitmap(),
contentDescription = null
)
Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
drawRect(Color.Black.copy(alpha = 0.8f))
drawCircle(
Color.Transparent,
style = Fill,
blendMode = BlendMode.Clear
)
})
}
But it seems like it just draws a dark circle on top of the image instead of clearing darken rectangle...
It would be also super handy if you would suggest how to crop image based on this circle coordinates.
You need to use clipPath in this case.
Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
val circlePath = Path().apply {
addOval(Rect(center, size.minDimension / 2))
}
clipPath(circlePath, clipOp = ClipOp.Difference) {
drawRect(SolidColor(Color.Black.copy(alpha = 0.8f)))
}
})
You don't need to necessarily use clipPath, you can use Porterduff(BlendMode) modes too, which is actually preferred way when you check sample codes or questions for clearing or removing some pixels or manipulating pixels.
It only requires small modifications to your code to work, you can check my answer how to apply Porterduff modes here.
1- adding graphics layer with alpha less than 1f
.graphicsLayer {
alpha = .99f
}
will fix the issue.
Full implementation
Box(
modifier = Modifier
.background(Color.Black)
.fillMaxSize()
) {
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.drawable.landscape),
contentScale = ContentScale.Crop,
contentDescription = null
)
Canvas(
modifier = Modifier
.fillMaxSize()
// ONLY ADD THIS
.graphicsLayer {
alpha = .99f
}
) {
// Destination
drawRect(Color.Black.copy(alpha = 0.8f))
// Source
drawCircle(
color = Color.Transparent,
blendMode = BlendMode.Clear
)
}
}
2- Saving to a layer
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
// Destination
drawRect(Color.Black.copy(alpha = 0.8f))
// Source
drawCircle(
color = Color.Transparent,
blendMode = BlendMode.Clear
)
restoreToCount(checkPoint)
}
Full Implementation
Canvas(modifier = Modifier
.fillMaxSize()
) {
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
// Destination
drawRect(Color.Black.copy(alpha = 0.8f))
// Source
drawCircle(
color = Color.Transparent,
blendMode = BlendMode.Clear
)
restoreToCount(checkPoint)
}
}
}
Result
Also you can check here to get familiar with BlendModes, path operations and more about canvas.
Related
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 add a left/start vertical border to view (Column), Not able to get the solution. as of now was trying to achieve using a divider inside the column it also need a height, but it depends on the contents inside the column, sometime it may grow.
Column(modifier = Modifier.padding(start = 34.dp)) {
Divider(
color = Color.Red,
modifier = Modifier
.height(100.dp)
.padding(end = 34.dp).width(2.dp)
)
You can use the drawWithCache modifier using the drawLine function.
Something like:
Column(modifier =
Modifier
.padding(start = 34.dp)
.size(100.dp, 75.dp)
.drawWithCache {
onDrawWithContent {
// draw behind the content the vertical line on the left
drawLine(
color = Color.Red,
start = Offset.Zero,
end = Offset(0f, this.size.height),
strokeWidth= 1f
)
// draw the content
drawContent()
}
}
){
//...content
}
If you want to use a Divider you can use fillMaxHeight() applying an intrinsic measurements to its parent container.
Something like:
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
Divider(
color = Color.Red,
modifier = Modifier
.fillMaxHeight() //important
.width(2.dp)
)
Box(Modifier.fillMaxWidth().height(100.dp).background(Yellow))
}
You can achive this with Modifier.drawBehind and drawLine
Code
TextButton(
onClick = {
//Click Functions
},
modifier = Modifier.drawBehind {
val strokeWidth = 1 * density
//Draw line function for left border
drawLine(
Color.LightGray,
Offset(0f, strokeWidth),
Offset(0f, size.height),
strokeWidth
)
}
)
{
Text("Left Border")
}
Output
How to clip or cut Composable content to have Image, Button or Composables to have custom shapes? This question is not about using Modifier.clip(), more like accomplishing task with alternative methods that allow outcomes that are not possible or when it's difficult to create a shape like cloud or Squircle.
This is share your knowledge, Q&A-style question which inspired by M3 BottomAppBar or BottomNavigation not having cutout shape, couldn't find question, and drawing a Squircle shape being difficult as in this question.
More and better ways of clipping or customizing shapes and Composables are more than welcome.
One of the ways for achieving cutting or clipping a Composable without the need of creating a custom Composable is using
Modifier.drawWithContent{} with a layer and a BlendMode or PorterDuff modes.
With Jetpack Compose for these modes to work you either need to set alpha less than 1f or use a Layer as in answer here.
I go with layer solution because i don't want to change content alpha
fun ContentDrawScope.drawWithLayer(block: ContentDrawScope.() -> Unit) {
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
block()
restoreToCount(checkPoint)
}
}
block lambda is the draw scope for Modifier.drawWithContent{} to do clipping
and another extension for simplifying further
fun Modifier.drawWithLayer(block: ContentDrawScope.() -> Unit) = this.then(
Modifier.drawWithContent {
drawWithLayer {
block()
}
}
)
Clip button at the left side
First let's draw the button that is cleared a circle at left side
#Composable
private fun WhoAteMyButton() {
val circleSize = LocalDensity.current.run { 100.dp.toPx() }
Box(
modifier = Modifier
.fillMaxWidth()
.drawWithLayer {
// Destination
drawContent()
// Source
drawCircle(
center = Offset(0f, 10f),
radius = circleSize,
blendMode = BlendMode.SrcOut,
color = Color.Transparent
)
}
) {
Button(
modifier = Modifier
.padding(horizontal = 10.dp)
.fillMaxWidth(),
onClick = { /*TODO*/ }) {
Text("Hello World")
}
}
}
We simply draw a circle but because of BlendMode.SrcOut intersection of destination is removed.
Clip button and Image with custom image
For squircle button i found an image from web
And clipped button and image using this image with
#Composable
private fun ClipComposables() {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
val imageBitmap = ImageBitmap.imageResource(id = R.drawable.squircle)
Box(modifier = Modifier
.size(150.dp)
.drawWithLayer {
// Destination
drawContent()
// Source
drawImage(
image = imageBitmap,
dstSize = IntSize(width = size.width.toInt(), height = size.height.toInt()),
blendMode = BlendMode.DstIn
)
}
) {
Box(
modifier = Modifier
.size(150.dp)
.clickable { }
.background(MaterialTheme.colorScheme.inversePrimary),
contentAlignment = Alignment.Center
) {
Text(text = "Squircle", fontSize = 20.sp)
}
}
Box(modifier = Modifier
.size(150.dp)
.drawWithLayer {
// Destination
drawContent()
// Source
drawImage(
image = imageBitmap,
dstSize = IntSize(width = size.width.toInt(), height = size.height.toInt()),
blendMode = BlendMode.DstIn
)
}
) {
Image(
painterResource(id = R.drawable.squirtle),
modifier = Modifier
.size(150.dp),
contentScale = ContentScale.Crop,
contentDescription = ""
)
}
}
}
There are 2 things to note here
1- Blend mode is BlendMode.DstIn because we want texture of Destination with shape of Source
2- Drawing image inside ContentDrawScope with dstSize to match Composable size. By default it's drawn with png size posted above.
Creating a BottomNavigation with cutout shape
#Composable
private fun BottomBarWithCutOutShape() {
val density = LocalDensity.current
val shapeSize = density.run { 70.dp.toPx() }
val cutCornerShape = CutCornerShape(50)
val outline = cutCornerShape.createOutline(
Size(shapeSize, shapeSize),
LocalLayoutDirection.current,
density
)
val icons =
listOf(Icons.Filled.Home, Icons.Filled.Map, Icons.Filled.Settings, Icons.Filled.LocationOn)
Box(
modifier = Modifier.fillMaxWidth()
) {
BottomNavigation(
modifier = Modifier
.drawWithLayer {
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
val width = size.width
val outlineWidth = outline.bounds.width
val outlineHeight = outline.bounds.height
// Destination
drawContent()
// Source
withTransform(
{
translate(
left = (width - outlineWidth) / 2,
top = -outlineHeight / 2
)
}
) {
drawOutline(
outline = outline,
color = Color.Transparent,
blendMode = BlendMode.Clear
)
}
restoreToCount(checkPoint)
}
},
backgroundColor = Color.White
) {
var selectedIndex by remember { mutableStateOf(0) }
icons.forEachIndexed { index, imageVector: ImageVector ->
if (index == 2) {
Spacer(modifier = Modifier.weight(1f))
BottomNavigationItem(
icon = { Icon(imageVector, contentDescription = null) },
label = null,
selected = selectedIndex == index,
onClick = {
selectedIndex = index
}
)
} else {
BottomNavigationItem(
icon = { Icon(imageVector, contentDescription = null) },
label = null,
selected = selectedIndex == index,
onClick = {
selectedIndex = index
}
)
}
}
}
// This is size fo BottomNavigationItem
val bottomNavigationHeight = LocalDensity.current.run { 56.dp.roundToPx() }
FloatingActionButton(
modifier = Modifier
.align(Alignment.TopCenter)
.offset {
IntOffset(0, -bottomNavigationHeight / 2)
},
shape = cutCornerShape,
onClick = {}
) {
Icon(imageVector = Icons.Default.Add, contentDescription = null)
}
}
}
This code is a bit long but we basically create a shape like we always and create an outline to clip
val cutCornerShape = CutCornerShape(50)
val outline = cutCornerShape.createOutline(
Size(shapeSize, shapeSize),
LocalLayoutDirection.current,
density
)
And before clipping we move this shape section up as half of the height to cut only with half of the outline
withTransform(
{
translate(
left = (width - outlineWidth) / 2,
top = -outlineHeight / 2
)
}
) {
drawOutline(
outline = outline,
color = Color.Transparent,
blendMode = BlendMode.Clear
)
}
Also to have a BottomNavigation such as BottomAppBar that places children on both side
i used a Spacer
icons.forEachIndexed { index, imageVector: ImageVector ->
if (index == 2) {
Spacer(modifier = Modifier.weight(1f))
BottomNavigationItem(
icon = { Icon(imageVector, contentDescription = null) },
label = null,
selected = selectedIndex == index,
onClick = {
selectedIndex = index
}
)
} else {
BottomNavigationItem(
icon = { Icon(imageVector, contentDescription = null) },
label = null,
selected = selectedIndex == index,
onClick = {
selectedIndex = index
}
)
}
}
Then we simply add a FloatingActionButton, i used offset but you can create a bigger parent and put our custom BottomNavigation and button inside it.
I've below composable with image inside box (which is inside LazyColumn). When I click on box, image scale is changing (which I've verified), but image on UI is not changing. That is, UI stays same, but it should change. What am I missing here?
#Composable
fun ImageContent(url: String) {
var scale by remember { mutableStateOf(1f) }
val state = rememberTransformableState { zoomChange, _, _ ->
scale = (scale * zoomChange).coerceIn(1f, 3f)
}
var expanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
.padding(start = 4.dp, end = 4.dp)
.clip(RoundedCornerShape(16.dp))
.graphicsLayer(scaleX = scale, scaleY = scale)
.transformable(state = state)
.clickable { expanded = !expanded },
) {
Image(
modifier = Modifier.fillMaxSize(),
painter = rememberImagePainter(data = url),
contentScale = if (expanded) ContentScale.Fit else ContentScale.Crop
)
}
}
I didn't mentioned it earlier, but I found the cause of issue and resolved it. transformations function was forcing the ui to not change when content scale change
Image(
modifier = Modifier.fillMaxSize(),
painter = rememberImagePainter(
data = url,
builder = {
placeholder(R.drawable.ic_filled_placeholder)
crossfade(300)
// This line was forcing the ui to not change when content scale change
transformations(RoundedCornersTransformation(16f))
}
),
contentScale = if (expanded) ContentScale.Fit else ContentScale.Crop,
contentDescription = null
)
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)