Jetpack Compose Path will draw outside canvas bounds - android

I have a signature box, at the moment when you sign, it is possible to draw outside the canvas. I need the path to stay within the bounds of the canvas. I could manually do this when capturing the path but I figure there is probably an automatic way.
Canvas(modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.border(1.dp, MaterialTheme.colors.primaryVariant, shape = RoundedCornerShape(4.dp))
.pointerInput(Unit) {
detectDragGestures(onDragStart = {
touchMove(path, it.x, it.y, -1f, -1f, true)
}) { change, _ ->
change.consumeAllChanges()
touchMove(
path,
change.position.x,
change.position.y,
change.previousPosition.x,
change.previousPosition.y,
false
)
}
}) {
canvasWidth = size.width
canvasHeight = size.height
drawPath(path, color = Color.Blue, style = Stroke(width = 4f))
}

Dearie me. Figured it out minuets after writing.
Modifier.clipToBounds()
Strange this isn't mentioned in any of the examples. Its the opposite behaviour of normal android clipping I think.

Related

How to mirror a composable function made by canvas with Modifier?

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

Draw lines at an angle in jetpack compose without a canvas

so I wanted to draw a line at an angle from point A to point B in jetpack compose.
Is there a way to do it without a canvas, since a canvas won't really work with what I want to do
If you want to draw behind other composables you can use the drawBehind modifier.
If you want to draw both behind and in front you can use the drawWithContentmodifier.
If you also want to cache the result as much as possible you can use the drawWithCache modifier.
Example of a line behind the content and a line in front of the content using the drawWithCache modifier
#Composable
fun DrawWithCacheExample() {
val width = 400.dp
val height = 200.dp
var offsetX by remember { mutableStateOf(0f) }
Box(
modifier = Modifier
.size(width, height)
.border(1f.dp, Color.Black, RectangleShape)
.drawWithCache {
onDrawWithContent {
// draw behind the content
drawLine(Color.Black, Offset.Zero, Offset(width.toPx(), height.toPx()), 1f)
// draw the content
drawContent()
// draw in front of the content
drawLine(Color.Black, Offset(0f, height.toPx()), Offset(width.toPx(), 0f), 1f)
}
}
) {
Box(modifier = Modifier
.size(width / 2, height / 2)
.offset { IntOffset(offsetX.roundToInt(), (height / 4).roundToPx()) }
.background(Color.Yellow)
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
offsetX += delta
}
)
)
}
}

How can I draw a shadow for a path in canvas in jetpack compose

I am drawing a custom shape for a topbar in jetpack compose. I want to draw a shadow for the path.
val topBarShapePath = Path().apply {
moveTo(dpToPixels(leftPadding), 0f)
lineTo(dpToPixels(leftPadding), dpToPixels(dpValue = 110.dp))
arcTo(
Rect(
dpToPixels(leftPadding),
dpToPixels(dpValue = 110.dp),
dpToPixels(dpValue = 32.dp),
dpToPixels(dpValue = 135.dp)
), -180f, -90f, true)
lineTo(
dpToPixels(dpValue = triangleStartX),
dpToPixels(dpValue = rectHeight))
lineTo(
dpToPixels(dpValue = screenWidth),
dpToPixels(dpValue = triangleEndY)
)
lineTo(dpToPixels(dpValue = screenWidth), 0f)
lineTo(dpToPixels(dpValue = leftPadding), 0f)
}
Column(
modifier = Modifier
.fillMaxWidth()
.height(400.dp)
.drawBehind {
val finalWidth = 40.dp.toPx()
drawPath(
topBarShapePath,
color = topbarcolor)
drawOutline(
outline = Outline.Generic(
topBarShapePath),
brush = Brush.horizontalGradient(),
style = Stroke(
width = 1.dp.toPx(),
)
)
}
)
This is the code I am using to draw the shape, the "drawOutline" was to try and draw a shadow for the path, but I can't figure out how to blur the line.
Any help appreciated.
Here is a screenshot of the result I am looking for:
It's impossible to draw shadow in Canvas at the moment, but you can do it with Modifier.shadow, specifying the needed custom shape, like this:
class TopBarShape(/*some parameters*/): Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density,
) = Outline.Generic(Path().apply {
// your path code
})
}
Modifier.shadow(elevation = 10.dp, shape = TopBarShape(/*your parameters*/))
Sadly this modifier doesn't allow much modifications, it's one of the most starred Compose issues, so hopefully it'll change in future, but as it's not in the latest 1.1-beta I wouldn't expect it at least until 1.2.
If you still think that drawing shadow manually is a needed feature, you can create a feature request.

Curved text Jetpack compose

I want to create curved text in Jetpack Compose like it was in "Material You". But how?
Example:
You can do this using Canvas. Compose itself does not have a function to draw a curved text (afaik in rc-01). But using drawIntoCanvas function you can use the nativeCanvas which provides drawTextOnPath where you can draw a text in a Path. In this Path you add an arc, so your text is drawn in this path.
Canvas(
modifier = Modifier
.size(300.dp)
.background(Color.Gray)
) {
drawIntoCanvas {
val textPadding = 48.dp.toPx()
val arcHeight = 400.dp.toPx()
val arcWidth = 300.dp.toPx()
val path = Path().apply {
addArc(0f, textPadding, arcWidth, arcHeight, 180f, 180f)
}
it.nativeCanvas.drawTextOnPath(
"Curved Text with Jetpack Compose",
path,
0f,
0f,
Paint().apply {
textSize = 16.sp.toPx()
textAlign = Paint.Align.CENTER
}
)
}
}
Here's the result:

How to move a rectangle around a canvas in Jetpack Compose?

I have a canvas with a rectangle that I would like to be able to move around. With a Composable, I know how to use the dragging modifier as described here: https://developer.android.com/jetpack/compose/gestures#dragging
But my canvas rectangle has no modifier:
Canvas(modifier = Modifier.fillMaxSize()) {
drawRect(Color.Blue, topLeft = Offset(0f, 0f), size = Size(this.size.width, 55f))
So how can I drag it? Is there a way with Compose or is it better to just use the native way with a native canvas?
With 1.0.0-beta04 you can use the pointerInput modifier in the Canvas to control the dragging gesture through the detectDragGestures function and save the Offset and apply it in the topLeft parameter in the drawRect.
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Canvas(modifier = Modifier.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
offsetX += dragAmount.x
offsetY += dragAmount.y
}
}
){
val canvasQuadrantSize = size / 2F
drawRect(
topLeft = Offset(offsetX,offsetY),
color = Color.Green,
size = canvasQuadrantSize
)
}

Categories

Resources