Related
I'm learning Kotlin and Jetpack Compose desktop. I've made a Sudoku solver and am now trying to make it graphical. My window dimensions are 800 X 800. I have 20.dp padding around all sides. My formula for placing the horizontal lines works perfect. I would think it would be the same for the vertical lines but they are offset. I found it interesting that the canvas width and height are less than what I declared the window dimensions to be. I played around with the height of the columns and it always drew the line as desired.
#Composable
fun displayPuzzle(answer: Array<Array<IntArray>>) {
var list = mutableStateListOf<String>()
for (x in answer[0]) list.addAll(x.map { it.toString() })
var columnHeighty by remember { mutableStateOf(0F) }
var columnWidthx by remember { mutableStateOf(0f) }
var pad = 20
LazyVerticalGrid(
columns = GridCells.Fixed(9),
contentPadding = PaddingValues(
start = pad.dp,
top = pad.dp,
end = pad.dp,
bottom = pad.dp
)
) {
items(list.size) { index ->
Card(
backgroundColor = Color.Red,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.onGloballyPositioned { coordinates ->
columnWidthx = coordinates.size.width.toFloat()
columnHeighty = coordinates.size.height.toFloat()
},
border = BorderStroke(width = 1.dp, color = Color.White)
) {
Text(
text = list[index],
fontWeight = FontWeight.Bold,
fontSize = 30.sp,
color = Color(0xFF000000),
textAlign = TextAlign.Center,
modifier = Modifier.padding(23.dp)
)
}
}
}
Canvas(modifier = Modifier.fillMaxSize()) {
val canvasWidth = size.width
val canvasHeight = size.height
val strokeWidth = 5.0F
println("Canvas Width $canvasWidth")
println("Canvas Height $canvasHeight")
println("Column Width $columnWidthx")
println("Column Height $columnHeighty")
//Draw 1st vertical separator
drawLine(
start = Offset(x = columnWidthx * 3 + pad.toFloat(), y = pad.toFloat()),
end = Offset(x = columnWidthx * 3 + pad.toFloat(), y = canvasHeight - pad.toFloat()),
color = Color.Black,
strokeWidth = strokeWidth
)
//Draw 2nd vertical separator
drawLine(
start = Offset(x = columnWidthx * 6 + pad.toFloat(), y = pad.toFloat()),
end = Offset(x = columnWidthx * 6 + pad.toFloat(), y = canvasHeight - pad.toFloat()),
color = Color.Black,
strokeWidth = strokeWidth
)
//Draw 1st horizontal separator
drawLine(
start = Offset(x = pad.toFloat(), y = columnHeighty * 3 + pad.toFloat()),
end = Offset(x = canvasWidth - pad.toFloat() , y = columnHeighty * 3 + pad.toFloat()),
color = Color.Black,
strokeWidth = strokeWidth
)
//Draw 2nd horizontal seperator
drawLine(
start = Offset(x = pad.toFloat(), y = columnHeighty * 6 + pad.toFloat()),
end = Offset(x = canvasWidth - pad.toFloat() , y = columnHeighty * 6 + pad.toFloat()),
color = Color.Black,
strokeWidth = strokeWidth
)
//Draw top border
drawLine(
start = Offset(x = pad.toFloat(), y = pad.toFloat() + strokeWidth / 2),
end = Offset(x = canvasWidth - pad.toFloat() , y = pad.toFloat() + strokeWidth / 2),
color = Color.Black,
strokeWidth = strokeWidth
)
//Draw bottom border
drawLine(
start = Offset(x = pad.toFloat(), y = canvasHeight - pad.toFloat() - strokeWidth / 2),
end = Offset(x = canvasWidth - pad.toFloat() , y = canvasHeight - pad.toFloat() - strokeWidth / 2),
color = Color.Black,
strokeWidth = strokeWidth
)
//Draw left border
drawLine(
start = Offset(x = pad.toFloat(), y = pad.toFloat()),
end = Offset(x = pad.toFloat() , y = canvasHeight - pad.toFloat()),
color = Color.Black,
strokeWidth = strokeWidth
)
//Draw right border
drawLine(
start = Offset(x = canvasWidth - pad.toFloat(), y = pad.toFloat()),
end = Offset(x = canvasWidth - pad.toFloat() , y = canvasHeight - pad.toFloat()),
color = Color.Black,
strokeWidth = strokeWidth
)
}
}
Window(onCloseRequest = ::exitApplication) {
window.minimumSize = Dimension(800,800)
displayPuzzle(finalAnswer[0])
}
I figured out the width of the cells was varying by 1 dp. I used Modifier.requiredWidth and it works as desired.
I want to draw transparent arc after color gradient radius end. It sound confusing. I have progress bar in which I have end the line at X position. After that I want to show transparent arc space at X position with Line Gradient. I am trying to use drawArc, but it's not working correctly.
#Composable
fun DrawProgressBar() {
val activity = LocalContext.current as AppCompatActivity
val rangeComposition = RangeComposition()
val itemLst = rangeComposition.bpExplained
val boxSize = 30.dp
val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue))
val progressBarPointer = rangeComposition.findReadingWithPointer(142, 90).second
Box(
modifier = Modifier
.background(Color.White)
.height(height = boxSize)
) {
Canvas(
modifier = Modifier.fillMaxSize()
) {
val strokeWidth = 8.dp
val canvasWidth = size.width
val canvasHeight = size.height
val strokeWidthPx = density.run { strokeWidth.toPx() }
drawLine(
start = Offset(x = 0f, y = canvasHeight / 2),
end = Offset(x = canvasWidth, y = canvasHeight / 2),
color = Color.Gray,
strokeWidth = strokeWidthPx,
cap = StrokeCap.Round,
)
val progressBarPointerInPixel = (progressBarPointer / 100f) * canvasWidth
activity.logE("progressBarPointerInPixel $progressBarPointerInPixel")
drawLine(
brush = brush,
start = Offset(x = 0f, y = canvasHeight / 2),
end = Offset(x = progressBarPointerInPixel, y = canvasHeight / 2),
strokeWidth = strokeWidthPx,
cap = StrokeCap.Round,
)
drawArc(
topLeft = Offset(x = progressBarPointerInPixel, y = canvasHeight / 2),
size = Size(8.dp.toPx(), strokeWidthPx),
color = Color.Cyan,
startAngle = -90f,
sweepAngle = 180f,
useCenter = true
)
itemLst.forEachIndexed { index, rangeItem ->
val endPointInPixel = (rangeItem.endPoint / 100f) * canvasWidth
if (index != itemLst.lastIndex) {
drawLine(
start = Offset(x = endPointInPixel, y = 0F),
end = Offset(x = endPointInPixel, y = boxSize.toPx()),
color = Color.Black,
strokeWidth = 4.dp.toPx(),
)
}
}
}
}
}
Actual Output
Expected Output
You can find the source code of RangeComposition.kt.
UPDATE
After #GabrieleMariotti mention code I tried with white color and I see that there is white tiny vertical bar is there
fun DrawProgressBar() {
val activity = LocalContext.current as AppCompatActivity
val rangeComposition = RangeComposition()
val itemLst = rangeComposition.bpExplained
val boxSize = 30.dp
val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue))
val progressBarPointer = rangeComposition.findReadingWithPointer(142, 90).second
Box(
modifier = Modifier
.background(Color.White)
.height(height = boxSize)
) {
Canvas(
modifier = Modifier.fillMaxSize()
) {
val strokeWidth = 8.dp
val canvasWidth = size.width
val canvasHeight = size.height
val strokeWidthPx = density.run { strokeWidth.toPx() }
val pathEffect = PathEffect.dashPathEffect(floatArrayOf(canvasHeight / 19, canvasHeight / 19), 0f)
drawLine(
start = Offset(x = 0f, y = canvasHeight / 2),
end = Offset(x = canvasWidth, y = canvasHeight / 2),
color = Color.Gray,
strokeWidth = strokeWidthPx,
cap = StrokeCap.Round,
)
val progressBarPointerInPixel = (progressBarPointer / 100f) * canvasWidth
drawLine(
color = Color.White,
start = Offset(x = progressBarPointerInPixel, y = canvasHeight / 2),
end = Offset(x = progressBarPointerInPixel + strokeWidthPx / 2, y = canvasHeight / 2),
strokeWidth = strokeWidthPx,
)
drawLine(
brush = brush,
start = Offset(x = 0f, y = canvasHeight / 2),
end = Offset(x = progressBarPointerInPixel, y = canvasHeight / 2),
strokeWidth = strokeWidthPx,
cap = StrokeCap.Round,
)
drawArc(
topLeft = Offset(x = progressBarPointerInPixel, y = canvasHeight / 2 - strokeWidthPx / 2),
size = Size(strokeWidthPx, strokeWidthPx),
color = Color.White,
startAngle = -90f,
sweepAngle = 180f,
useCenter = true
)
itemLst.forEachIndexed { index, rangeItem ->
val endPointInPixel = (rangeItem.endPoint / 100f) * canvasWidth
if (index != itemLst.lastIndex) {
drawLine(
start = Offset(x = endPointInPixel, y = 0F),
end = Offset(x = endPointInPixel, y = boxSize.toPx()),
color = Color.Black,
strokeWidth = 1.2.dp.toPx(),
pathEffect = pathEffect
)
}
}
}
}
}
Result
In your arc you have to change the topLeft offset considering also the height offset due to the strokeWidthPx. Something like:
topLeft = Offset(x = progressBarPointerInPixel, y = canvasHeight / 2 - strokeWidthPx/2),
Also you should add also a line from progressBarPointerInPixel to progressBarPointerInPixel + strokeWidthPx/2 due to the rounded corners.
Something like:
drawLine(
//gray line
)
drawLine(
color = Color.Cyan,
start = Offset(x = progressBarPointerInPixel , y = canvasHeight / 2),
end = Offset(x = progressBarPointerInPixel + strokeWidthPx/2, y = canvasHeight / 2),
strokeWidth = strokeWidthPx,
)
drawLine(
brush = brush,
start = Offset(x = 0f, y = canvasHeight / 2),
end = Offset(x = progressBarPointerInPixel, y = canvasHeight / 2),
strokeWidth = strokeWidthPx,
cap = StrokeCap.Round,
)
drawArc(
topLeft = Offset(x = progressBarPointerInPixel, y = canvasHeight / 2 - strokeWidthPx/2),
size = Size(strokeWidthPx,strokeWidthPx),
color = Color.Cyan,
startAngle = -90f,
sweepAngle = 180f,
useCenter = true
)
To achieve a transparent arc you can add the blendMode = BlendMode.DstOut to line+arc.It also requires to apply an alpha !=1F to the Canvas with graphicsLayer(alpha = 0.99f)
Check the doc for more details about the blendMode.
Canvas(
modifier = Modifier.fillMaxSize().graphicsLayer(alpha = 0.99f)
) {
drawLine(
//gray line
)
drawLine(
//...
color = Color.Cyan,
blendMode = BlendMode.DstOut
)
drawLine(
//gradient
)
drawArc(
blendMode = BlendMode.DstOut
)
}
There are items() {} sections inside LazyColumn. So I would like to draw a border with rounded corners around each section. Is there any method?
// need to draw a border around the items
LazyColumn {
items(10) {
Row {
// content
}
}
items(5) {
Row {
// content
}
}
}
If you want to add a border to single item just add in your item content a Composable with a border modifier:
items(10) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(2.dp)
.border(width = 1.dp, color = Blue200, shape = RoundedCornerShape(8.dp))
.padding(2.dp)
){ /** ... */ }
}
If you want to add a border around all the items block you can create different border modifiers to apply to each items.
Something like:
//border
val strokeWidth: Dp = 2.dp
val strokeColor: Color = Blue500
val cornerRadius: Dp = 8.dp
//background shape
val topShape = RoundedCornerShape(topStart = cornerRadius, topEnd = cornerRadius)
val bottomShape = RoundedCornerShape(bottomStart = cornerRadius, bottomEnd = cornerRadius)
LazyColumn {
val itemCount = 10
var shape : Shape
var borderModifier : Modifier
items(itemCount) { index ->
when (index) {
0 -> {
//First item. Only top border
shape = topShape
borderModifier = Modifier.topBorder(strokeWidth,strokeColor,cornerRadius)
}
itemCount -1 -> {
//last item. Only bottom border
shape = bottomShape
borderModifier = Modifier.bottomBorder(strokeWidth,strokeColor,cornerRadius)
}
else -> {
//Other items. Only side border
shape = RectangleShape
borderModifier = Modifier.sideBorder(strokeWidth,strokeColor,cornerRadius)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.clip(shape)
.background(Teal200)
.then(borderModifier)
.padding(4.dp)
) {
Text(text = "Item: $index")
}
}
}
where:
fun Modifier.topBorder(strokeWidth: Dp, color: Color, cornerRadiusDp: Dp) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
val cornerRadiusPx = density.run { cornerRadiusDp.toPx() }
Modifier.drawBehind {
val width = size.width
val height = size.height
drawLine(
color = color,
start = Offset(x = 0f, y = height),
end = Offset(x = 0f, y = cornerRadiusPx),
strokeWidth = strokeWidthPx
)
drawArc(
color = color,
startAngle = 180f,
sweepAngle = 90f,
useCenter = false,
topLeft = Offset.Zero,
size = Size(cornerRadiusPx * 2, cornerRadiusPx * 2),
style = Stroke(width = strokeWidthPx)
)
drawLine(
color = color,
start = Offset(x = cornerRadiusPx, y = 0f),
end = Offset(x = width - cornerRadiusPx, y = 0f),
strokeWidth = strokeWidthPx
)
drawArc(
color = color,
startAngle = 270f,
sweepAngle = 90f,
useCenter = false,
topLeft = Offset(x = width - cornerRadiusPx * 2, y = 0f),
size = Size(cornerRadiusPx * 2, cornerRadiusPx * 2),
style = Stroke(width = strokeWidthPx)
)
drawLine(
color = color,
start = Offset(x = width, y = height),
end = Offset(x = width, y = cornerRadiusPx),
strokeWidth = strokeWidthPx
)
}
}
)
fun Modifier.bottomBorder(strokeWidth: Dp, color: Color, cornerRadiusDp: Dp) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
val cornerRadiusPx = density.run { cornerRadiusDp.toPx() }
Modifier.drawBehind {
val width = size.width
val height = size.height
drawLine(
color = color,
start = Offset(x = 0f, y = 0f),
end = Offset(x = 0f, y = height-cornerRadiusPx),
strokeWidth = strokeWidthPx
)
drawArc(
color = color,
startAngle = 90f,
sweepAngle = 90f,
useCenter = false,
topLeft = Offset(x = 0f, y = height - cornerRadiusPx * 2),
size = Size(cornerRadiusPx * 2, cornerRadiusPx * 2),
style = Stroke(width = strokeWidthPx)
)
drawLine(
color = color,
start = Offset(x = cornerRadiusPx, y = height),
end = Offset(x = width - cornerRadiusPx, y = height),
strokeWidth = strokeWidthPx
)
drawArc(
color = color,
startAngle = 0f,
sweepAngle = 90f,
useCenter = false,
topLeft = Offset(x = width - cornerRadiusPx * 2, y = height - cornerRadiusPx * 2),
size = Size(cornerRadiusPx * 2, cornerRadiusPx * 2),
style = Stroke(width = strokeWidthPx)
)
drawLine(
color = color,
start = Offset(x = width, y = 0f),
end = Offset(x = width, y = height - cornerRadiusPx),
strokeWidth = strokeWidthPx
)
}
}
)
fun Modifier.sideBorder(strokeWidth: Dp, color: Color, cornerRadiusDp: Dp) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
val cornerRadiusPx = density.run { cornerRadiusDp.toPx() }
Modifier.drawBehind {
val width = size.width
val height = size.height
drawLine(
color = color,
start = Offset(x = 0f, y = 0f),
end = Offset(x = 0f, y = height),
strokeWidth = strokeWidthPx
)
drawLine(
color = color,
start = Offset(x = width, y = 0f),
end = Offset(x = width, y = height),
strokeWidth = strokeWidthPx
)
}
}
)
You can draw a border around the whole list, using the modifier border and a RoundedCornerShape:
LazyColumn(modifier.border(width = 1.dp, color = Color.Red, shape = RoundedCornerShape(1.dp)))
Or around every item by applying the same to the rows:
Row(modifier.border(width = 1.dp, color = Color.Green, shape = RoundedCornerShape(1.dp)))
I am trying to add a seekbar on Canvas where blue line is showing in the image blow. When I try to add material component on canvas it says:
#Composable invocations can only happen from the context of a #Composable function
and if I do it in Composable function I do not know how can I get the coordinates of line and draw Seekbar on it.
here is my code:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.myComposable.setContent {
graphPoints(maxOnX = 20f, maOnY = 10, 4.0f,6.0f,0f,0f)
}
}
#Composable
fun graphPoints(maxOnX: Float, maOnY: Int, timeStart: Float, timeEnd: Float,
concentrationStart: Float, concentrationEnd: Float) {
Canvas(modifier = Modifier.fillMaxSize()) {
val canvasWidth = size.width
val canvasHeight = size.height
val unitX: Float = canvasWidth / maxOnX
val nextAfterX: Float = canvasWidth / maxOnX
for (i in 0 until canvasWidth.toInt() step nextAfterX.toInt() * 2) {
if (i == 0) {
continue
}
drawLine(
start = Offset(x = i.toFloat(), y = canvasHeight),
end = Offset(x = i.toFloat(), y = canvasHeight - 10),
color = Color.Black,
strokeWidth = 2F
)
}
val nextAfterY: Float = canvasHeight / maOnY.toFloat()
for (i in 0 until canvasHeight.toInt() step nextAfterY.toInt()) {
if (i == 0) {
continue
}
drawLine(
start = Offset(x = 0f, y = i.toFloat()),
end = Offset(x = 10f, y = i.toFloat()),
color = Color.Black,
strokeWidth = 2F
)
}
drawLine(
start = Offset(x = 0f, y = 0f),
end = Offset(x = 0f, y = canvasHeight),
color = Color.Black,
strokeWidth = 5F
)
drawLine(
start = Offset(x = 0f, y = 0f),
end = Offset(x = 0f, y = canvasHeight),
color = Color.Black,
strokeWidth = 5F
)
drawLine(
start = Offset(x = 0f, y = canvasHeight),
end = Offset(x = canvasWidth, y = canvasHeight),
color = Color.Black,
strokeWidth = 5F
)
if (timeStart > 0 && timeEnd > 0) {
drawCircle(
color = Color.Black,
radius = 10f,
center = Offset(timeStart * unitX, canvasHeight / 2)
)
drawLine(
start = Offset(timeStart * unitX, 0f),
end = Offset(x = timeStart * unitX, y = canvasHeight),
color = Color.Black,
strokeWidth = 2F
)
drawCircle(
color = Color.Black,
radius = 10f,
center = Offset(timeEnd * unitX, canvasHeight - (canvasHeight / 4))
)
drawLine(
start = Offset(timeEnd * unitX, 0f),
end = Offset(x = timeEnd * unitX, y = canvasHeight),
color = Color.Black,
strokeWidth = 2F
)
drawLine(
start = Offset(timeStart * unitX, canvasHeight - (canvasHeight / 2)),
end = Offset(x = timeEnd * unitX, y = canvasHeight - (canvasHeight / 4)),
color = Color.Blue,
strokeWidth = 2F
)
}
}
}
#Preview
#Composable
fun PreviewMessageCard() {
graphPoints(maxOnX = 20f, maOnY = 10, 4.0f,6.0f,0f,0f)
}
I'm not sure how can I do the calculation in order to center this arc on the canvas? Can someone point me in the proper direction?
Canvas(modifier = Modifier
.background(Color.LightGray)
.fillMaxWidth()
.height(300.dp)
) {
drawArc(
color = Color.Blue,
startAngle = 30f,
sweepAngle = 300f,
useCenter = false,
style = Stroke(width = 50f, cap = StrokeCap.Round),
size = size/2.25F
)
}
Use the topLeft parameter in the drawArc method.
Something like:
val sizeArc = size/2.25F
drawArc(
color = Color.Blue,
startAngle = 30f,
sweepAngle = 300f,
topLeft = Offset((size.width - sizeArc.width)/2f,(size.height - sizeArc.height)/2f),
useCenter = false,
style = Stroke(width = 50f, cap = StrokeCap.Round),
size = sizeArc
)
#Composable
fun CustomArc() {
Canvas(modifier = Modifier.fillMaxSize()) {
val arcRadius = 200f
val canvasWidth = size.width
val canvasHeight = size.height
drawArc(
color = Color.Red,
startAngle = -90f, //start angle is always in clockwise direction
sweepAngle = 270f, // angle formed between the start angle
useCenter = false,
size = Size(arcRadius, arcRadius),
topLeft = Offset(
(canvasWidth / 2) - (arcRadius / 2),
canvasHeight / 2 - (arcRadius / 2)
),
style = Stroke(width = 10f, cap = StrokeCap.Round)
)
}
}