Jetpack compose custom shape with pie effect - kotlin

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)
)

Related

How can to create a curved Text Button [duplicate]

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:

how to know position for component android jet-pack compose Kotlin

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))
)
}
}

Create vertical slider with label in Jetpack Compose

I made a vertical slider based on this answer, and now I need to add title and value for each sliders.
If I set a fixed width value in modifier like this modifier.width(180.dp), it looks fine like this
However I would like to let the slider height be responsive to the device screen size, so I set the width to modifier.fillMaxWidth(), the bottom text will disappear
Here is my vertical slider compose looks like, and I try to set the height in modifier here.
#Composable
fun VerticalSlider(value: MutableState<Float>, min: Int, max: Int, onFinished:(Int)->Unit) {
Slider(
modifier = Modifier
.graphicsLayer {
rotationZ = 270f
transformOrigin = TransformOrigin(0f, 0f)
}
.layout { measurable, constraints ->
val placeable = measurable.measure(
Constraints(
minWidth = constraints.minHeight,
maxWidth = constraints.maxHeight,
minHeight = constraints.minWidth,
maxHeight = constraints.maxWidth,
)
)
layout(placeable.height, placeable.width) {
placeable.place(-placeable.width, 0)
}
}
// .fillMaxWidth()
.width(180.dp)
.height(50.dp)
,
value = value.value,
valueRange = min.toFloat()..max.toFloat(),
onValueChange = {
value.value = it.toInt().toFloat()
},
onValueChangeFinished = {
onFinished(value.value.toInt())
},
)
}
Vertical slider with text.
#Composable
fun VerticalSliderWithText(sliderValue: MutableState<Float>, min: Int, max: Int, onFinished:(Int)->Unit) {
var sliderValue = remember { mutableStateOf(0f) }
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.SpaceAround,
horizontalAlignment = Alignment.CenterHorizontally
){
Text("slider title")
VerticalSlider(value = sliderValue, min = -6, max = 6,
) {
//println(" finish value: $it")
}
Text(modifier = Modifier.background(Green),
text="slider value")
}
}
Instead of Modifier.fillMaxWidth, you need to use Modifier.weight, which is available inside a Column. To do so you need to add a modifier parameter:
#Composable
fun VerticalSlider(value: MutableState<Float>, min: Int, max: Int, onFinished:(Int)->Unit, modifier: Modifier) {
Slider(
modifier = modifier
//...
So you can pass Modifier.weight like this:
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.SpaceAround,
horizontalAlignment = Alignment.CenterHorizontally
){
Text("slider title")
VerticalSlider(
value = sliderValue,
min = -6,
max = 6,
onFinished = {
//println(" finish value: $it")
},
modifier = Modifier.weight(1f)
)
Alternatively, you can declare VerticalSlider on ColumnScope, so Modifier.weight can be used directly from the function:
#Composable
fun ColumnScope.VerticalSlider(value: MutableState<Float>, min: Int, max: Int, onFinished:(Int)->Unit) {
Slider(
modifier = Modifier
.weight(1f)
//...

What are differents between drawLine directly and drawLine wrapped in drawIntoCanvas in Jetpack Compose?

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.

Recompose a Canvas while maintaining the previous drawing

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
}