I'm trying to make a composable with a line shape. These lines will be connecting any two coordinates together, meaning it won't be a straight horizontal/vertical line, I know I can use divider for that.
I can do a line in drawWithCache (compose desktop's equivalent of Canvas), but these lines are neither hoverable nor clickable.
Afaik clipping a box with a custom shape won't be helping here because hover and click events will still work with a rectangle shape.
Is there any way at all that I can do this?
P.S. I would love it if I could extend a solution to make other shapes, such as a line with Bezier's curve applied.
I've found the solution.
Modifier.clipToBounds won't work, but Modifier.clip(shape) does.
#Composable
fun Line() {
var top by remember { mutableStateOf(false) }
val shape = if (top) {
LineShapeTop
} else {
LineShapeBottom
}
Box(
Modifier
.background(color = Color.Blue, shape = shape)
.clip(shape)
.fillMaxSize()
.clickable { top = !top }
)
}
object LineShapeTop : Shape {
const val strokeWidth = 16.0f
override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline {
val path = Path().apply {
moveTo(0f, 0f)
lineTo(size.width/2 + strokeWidth, 0f)
lineTo(size.width/2 + strokeWidth, size.height- strokeWidth)
lineTo(size.width, size.height- strokeWidth)
lineTo(size.width, size.height)
lineTo(size.width/2, size.height)
lineTo(size.width/2, strokeWidth)
lineTo(0f, strokeWidth)
close()
}
return Outline.Generic(path)
}
}
object LineShapeBottom : Shape {
const val strokeWidth = 16.0f
override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline {
val path = Path().apply {
moveTo(0f, size.height)
lineTo(size.width/2 + strokeWidth, size.height)
lineTo(size.width/2 + strokeWidth, strokeWidth)
lineTo(size.width, strokeWidth)
lineTo(size.width, 0f)
lineTo(size.width/2, 0f)
lineTo(size.width/2, size.height- strokeWidth)
lineTo(0f, size.height- strokeWidth)
close()
}
return Outline.Generic(path)
}
}
Related
I want to make a shape filling the circle according to given percentage. Since i suck at math I'm unable to achieve this with path Api of jetpack compose.
My custom class is looking like this:
class ProgressPie(val progress: Float) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
val path = Path().apply {
addArc(
Rect(0f, 0f, size.width , size.height ),
-90f,
360f * progress
)
close()
}
return Outline.Generic(path)
}
}
and when the progress parameter changed the result should be looking like this.
You can use Canvas or Modifier.draWithContent to draw a pie chart.
Column(modifier=Modifier.padding(10.dp)) {
var progress by remember { mutableStateOf(0f) }
Box(
modifier = Modifier
.background(Color.White)
.fillMaxWidth()
.aspectRatio(1f)
.drawBehind {
drawCircle(Color.LightGray, style = Stroke(1.dp.toPx()))
drawArc(
Color.Blue,
startAngle = -90f,
sweepAngle = progress,
useCenter = true
)
}
)
Slider(
value = progress,
onValueChange = {
progress = it
},
valueRange = 0f..360f
)
}
You can use shape with remember to create new shape on progress change but the issue with Path and arc is it's not drawn from center.
val shape = remember(progress) {
GenericShape { size: Size, _: LayoutDirection ->
if( progress == 360f){
addOval(
Rect(0f, 0f, size.width, size.height)
)
}else {
moveTo(size.width/2f, size.height/2f)
arcTo(
Rect(0f, 0f, size.width, size.height),
-90f,
progress,
forceMoveTo = false
)
}
close()
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color.White)
.clip(shape)
.background(Color.Blue)
.aspectRatio(1f)
)
This question already has an answer here:
Curved text Jetpack compose
(1 answer)
Closed 7 months ago.
I'm beginner in jetpack compose, especially in Canvas.
I want to create this with Canvas:
But I don't know how.
I could create curved Text but with fillMaxSize and it's not useful for me. Because I want to use this picture as a button in a page and I have some buttons like this one in one page. So I want to have this Canvas with wrapcontent size.
So how can I create that?
The answer mentioned in the comments above is mine. I did some adjustments in order to give you some direction. The code is listed below:
#Composable
fun ButtonWithCurvedText(
#DrawableRes icon: Int,
text: String,
iconSize: Dp,
onClick: () -> Unit,
) {
val density = LocalDensity.current
val imgSize = iconSize
val imageSizePx = with(density) { imgSize.toPx() }
val labelSize = 12.sp
val textToIconPadding = 4.dp
val textToIconPaddingPx = with(density) { textToIconPadding.toPx() }
val boxSize = imgSize + (textToIconPadding * 2) + with(density) { labelSize.roundToPx().toDp() }
val boxSizePx = with(density) { boxSize.toPx() }
val imageInset = ((boxSize - imgSize) / 2)
val imageInsetPx = with(density) { imageInset.toPx() }
val arcTop = imageInsetPx - (textToIconPaddingPx / 2f)
val arcLeft = imageInsetPx - (textToIconPaddingPx / 2f)
val arcBottom = boxSizePx - imageInsetPx + (textToIconPaddingPx / 2f)
val arcRight = boxSizePx - imageInsetPx + (textToIconPaddingPx / 2f)
val vectorPainter = rememberVectorPainter(
ImageVector.vectorResource(icon)
)
Canvas(
modifier = Modifier
.size(boxSize)
.background(Color.Gray)
.clickable {
onClick()
}
) {
drawCircle(Color.LightGray, radius = imageSizePx / 2f)
with(vectorPainter) {
inset(
horizontal = imageInsetPx,
vertical = imageInsetPx
) {
draw(Size(imageSizePx, imageSizePx))
}
}
drawIntoCanvas {
val path = Path().apply {
addArc(arcLeft, arcTop, arcRight, arcBottom, 270f, 180f)
}
it.nativeCanvas.drawTextOnPath(
text,
path,
0f,
0f,
Paint().apply {
textSize = labelSize.toPx()
textAlign = Paint.Align.CENTER
}
)
}
}
}
As you mentioned, you want something responsive. You just need to make some constants used here as parameters. The only "size" param required here is the icon size.
You can use like this:
Row() {
ButtonWithCurvedText(R.drawable.ic_share, "Share", 48.dp, {})
ButtonWithCurvedText(R.drawable.ic_cancel, "Cancel", 72.dp, {})
ButtonWithCurvedText(R.drawable.ic_check, "Check", 96.dp, {})
}
The result is like below:
I found that function to drag and drop component like in my code simple image
but now i need to found the x and y axis for that image on screen
like for example when i move it to new position put the new position in variable
#Composable
fun ImgLatterMoving(painter: Painter,contentDescription: String) {
val offsetX = remember { mutableStateOf(0F) }
val offsetY = remember { mutableStateOf(0F) }
Box(
modifier = Modifier
.offset {
IntOffset(
x = offsetX.value.roundToInt(),
y = offsetY.value.roundToInt()
)
}
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
offsetX.value += dragAmount.x
offsetY.value += dragAmount.y
}
}
.size(100.dp)
) {
Image(painter = painter,
contentDescription = contentDescription,
modifier = Modifier.clip(RoundedCornerShape(10.dp))
)
}
}
I'm learning Jetpack Compose Canvas.
I met the two different Code both Code A and Code B.
Code A invoke drawLine directly, and Code B invoke drawLine which is wrapped in drawIntoCanvas.
1: What are differents between Code A and Code B ?
2: Which one is the better between Code A and Code B ?
Code A
#Composable
fun setCanvas() {
Canvas(
modifier = Modifier
.fillMaxSize()
) {
val canvasWidth = size.width
val canvasHeight = size.height
drawLine(
start = Offset(x = 0f, y = canvasHeight),
end = Offset(x = canvasWidth, y = 0f),
color = Color.Blue
)
}
}
Code B
#Composable
fun setCanvas() {
val linePaint = Paint()
linePaint.isAntiAlias = true
linePaint.style = PaintingStyle.Stroke
linePaint.color = Color.Blue
Canvas(
modifier = Modifier
.fillMaxSize()
) {
val canvasWidth = size.width
val canvasHeight = size.height
drawIntoCanvas {
it.drawLine(
Offset(x = 0f, y = canvasHeight),
Offset(x = canvasWidth, y = 0f),
linePaint
)
}
}
}
Function drawIntoCanvas has access to the underlying Canvas with nativeCanvas (quite helpful if you can to reuse some draw logic you implemented in previous Android apps). Then, you can call all the methods related to the native Canvas like drawText() or drawVertices() for example.
This function draw figures on canvas but when I clicked on button its shows only latest clicked figure but I want to show all figures which was clicked previous.
I think drawScope state can be remembered as a mutable state but how to achieve it.
#Composable
fun PaintBrushLayout(
modifier: Modifier = Modifier,
) {
var clicked by remember {
mutableStateOf(false)
}
var figName by remember {
mutableStateOf("")
}
Column(
verticalArrangement = Arrangement.Bottom, modifier = modifier.fillMaxSize()
) {
Canvas(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(.9f)
) {
Log.d("in Paint", "PaintBrushLayout: $this")
if (clicked) {
Log.d("TAG", "PaintBrushLayout: $figName")
when (figName) {
"Circle" -> {
drawCircleFig(color = Color.Blue)
}
"Rectangle" -> {
drawRectangleFig()
}
"Oval" -> {
drawOvalFig(color = Color.Gray)
}
}
}
}
Row(
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween
) {
BottomLayout("Circle") { click, name ->
clicked = click
figName = name
}
BottomLayout("Rectangle") { click, name ->
clicked = click
figName = name
}
BottomLayout("Oval") { click, name ->
clicked = click
figName = name
}
BottomLayout("Choose Color") { click, name ->
clicked = click
// figName = name
}
}
}
}
Create Bottom Layout of Buttons
#Composable
fun BottomLayout(
btnName: String,
click: (Boolean, String) -> Unit
) {
Button(onClick = {
click(true, btnName)
}, modifier = Modifier.padding(4.dp)) {
Text(text = btnName)
}
}
Draw Circle Figure on canvas
fun DrawScope.drawCircleFig(color: Color = Color.Red) {
drawCircle(
color = color
)
}
Draw Rectangle Figure on canvas
fun DrawScope.drawRectangleFig(color: Color = Color.Red) {
drawRect(
color = color
)
}
Draw Oval Figure on canvas
fun DrawScope.drawOvalFig(color: Color = Color.Red) {
drawOval(
color = color
)
}
I don't know if it's possible to recompose a Canvas and maintain its state prior to what it was previously, allowing you to drawn on top of what is already there. One solution is to keep track of a queue of the operations that you want to perform and then each time the Canvas is recomposed, you iterate through the queue and draw each item. If this is a drawingn app, it has the benefit of allowing the user to undo the previous steps. I also noticed that the code you provided doesn't recompose when you click on a button. I've noticed this issue before. The solution is to assign the remembered value to another value. Here is the sample on using a queue:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val queue = mutableListOf<DrawingObject>()
PaintBrushLayout(queue = queue)
}
}
}
#Composable
fun PaintBrushLayout(
modifier: Modifier = Modifier,
queue: MutableList<DrawingObject>
) {
var currentDrawingObject by remember { mutableStateOf(DrawingObject.None) }
var c = currentDrawingObject // This fixes a bug in recomposition.
fun addToQueue(drawingObject: DrawingObject) {
queue.add(drawingObject)
currentDrawingObject = drawingObject
}
Column(
verticalArrangement = Arrangement.Bottom, modifier = modifier.fillMaxSize()
) {
Canvas(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(.9f)
) {
queue.forEach { drawingObject ->
when (drawingObject) {
DrawingObject.Circle -> {
drawCircleFig(color = Color.Blue)
}
DrawingObject.Rectangle -> {
drawRectangleFig()
}
DrawingObject.Oval -> {
drawOvalFig(color = Color.Gray)
}
}
}
}
Row(
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween
) {
BottomLayout("Circle") { click, name ->
addToQueue(DrawingObject.Circle)
}
BottomLayout("Rectangle") { click, name ->
addToQueue(DrawingObject.Rectangle)
}
BottomLayout("Oval") { click, name ->
addToQueue(DrawingObject.Oval)
}
BottomLayout("Choose Color") { click, name ->
}
}
}
}
#Composable
fun BottomLayout(
btnName: String,
click: (Boolean, String) -> Unit
) {
Button(onClick = {
click(true, btnName)
}, modifier = Modifier.padding(4.dp)) {
Text(text = btnName)
}
}
fun DrawScope.drawCircleFig(color: Color = Color.Red) {
drawCircle(
radius = 300f,
color = color
)
}
fun DrawScope.drawRectangleFig(color: Color = Color.Red) {
drawRect(
size = Size(200f, 200f),
color = color
)
}
fun DrawScope.drawOvalFig(color: Color = Color.Red) {
drawOval(
size = Size(100f, 100f),
color = color
)
}
enum class DrawingObject {
None,
Circle,
Rectangle,
Oval
}