At work recently we needed to have a semi-circular “cutout” from a rectangular box containing content. This was surprisingly tricky, so here’s an easy way to cut out arbitrary shapes from a Jetpack Compose Box!
drawWithContent lets you customise how your Box (or other @Composable) content is drawn to screen, so that a circle (or any other shape) can be drawn on top of the content.
The colour doesn’t matter, only the shape / position does.
As this new shape is being drawn on top, a blendMode of BlendMode.DstOut (a Porter/Duff blend mode1) states that the new shape should be subtracted from the original content2.
Setting the compositingStrategy to Offscreen forces the content & any shapes to be drawn to a buffer then placed on screen at once3. This is required to allow the blendMode to function.
Available since 1.4.0, previous versions can use alpha=0.99f instead to force the behaviour.
That’s it! Draw the content, then draw a shape to cut away from the content.
Whilst the repo itself provides all of these examples in one large preview, standalone examples may be helpful.
Obviously the colours, shapes, background and contents are completely up to you, these are just simple samples!
Code
Output
@ComposablefunTopLeftCircle(){Box(modifier=Modifier.size(100.dp).graphicsLayer{compositingStrategy=CompositingStrategy.Offscreen}.drawWithContent{drawContent()drawCircle(color=Color(0xFFFFFFFF),center=Offset(x=0f,y=0f),radius=100f,blendMode=BlendMode.DstOut)}.background(color=Color(0xFFFFFFFF))){Text("There is text and other content here!")}}
@ComposablefunMiddleLeftCircle(){Box(modifier=Modifier.size(100.dp).graphicsLayer{compositingStrategy=CompositingStrategy.Offscreen}.drawWithContent{drawContent()drawCircle(color=Color(0xFFFFFFFF),center=Offset(x=0f,y=size.height/2),radius=50f,blendMode=BlendMode.DstOut)}.background(color=Color(0xFFFFFFFF))){Text("There is text and other content here!")}}
@ComposablefunMultipleCircles(){Box(modifier=Modifier.size(100.dp).graphicsLayer{compositingStrategy=CompositingStrategy.Offscreen}.drawWithContent{drawContent()drawCircle(color=Color(0xFFFFFFFF),center=Offset(x=50f,y=30f),radius=70f,blendMode=BlendMode.DstOut)drawCircle(color=Color(0xFFFFFFFF),center=Offset(x=size.width,y=size.height),radius=70f,blendMode=BlendMode.DstOut)drawCircle(color=Color(0xFFFFFFFF),center=Offset(x=size.width/2,y=size.height/2),radius=45f,blendMode=BlendMode.DstOut)}.background(color=Color(0xFFFFFFFF))){Text("There is text and other content here!")}}
@ComposablefunArbitraryShapes(){Box(modifier=Modifier.size(100.dp).graphicsLayer{compositingStrategy=CompositingStrategy.Offscreen}.drawWithContent{drawContent()drawArc(color=Color(0xFFFFFFFF),startAngle=0f,sweepAngle=300f,useCenter=true,topLeft=Offset(x=30f,y=30f),size=size/3f,blendMode=BlendMode.DstOut)drawLine(color=Color(0xFFFFFFFF),start=Offset(x=0f,y=0f),end=Offset(x=size.width,y=size.height),strokeWidth=30f,blendMode=BlendMode.DstOut)drawPoints(points=listOf(Offset(size.width,0f),Offset(150f,30f),Offset(170f,100f)),pointMode=androidx.compose.ui.graphics.PointMode.Points,color=Color(0xFFFFFFFF),strokeWidth=30f,cap=androidx.compose.ui.graphics.StrokeCap.Butt,blendMode=BlendMode.DstOut)}.background(color=Color(0xFFFFFFFF))){Text("There is text and other content here!")}}