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
I'm trying to make a photo editor that can be scale, rotate and drag to any place.
But I want to control the scale and rotate behavior via the corner rotate icon
not by the image itself.
I know it must to be using pointerInput from the modifier, but not sure how to implement it.
Can anybody help with that and give some example?
From example here: Android Touch System Gesture-Handling Modifiers in Jetpack Compose
#Composable
fun TransformableDemo() {
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
var offset by remember { mutableStateOf(Offset.Zero) }
val state = rememberTransformableState {
zoomChange, offsetChange, rotationChange ->
scale *= zoomChange
rotation += rotationChange
offset += offsetChange
}
Box(
modifier = Modifier
.graphicsLayer(
scaleX = scale,
scaleY = scale,
rotationZ = rotation,
translationX = offset.x,
translationY = offset.y
)
.transformable(state = state)
.background(Color.Blue)
.fillMaxSize()
)
}
Can't resolve the problem with scrolling a Custom View in Compose.
I have a Canvas nested in a Column with fillMaxSize() modifier.
In Canvas I also use fillMaxSize() modifier (or size() with fixed value, doesn't matter)
I draw a vertical line that can be higher than screen size. And want, if this vertical line is higher, make canvas scrollable, but I can't. Any methods like verticalScroll(), scrollable() has no effects. I'm trying to make height of Canvas higher and use scrollable modifier, but it does no effect also (when I check "size" variable of DrawScope, it's always 1630 on Pixel 4).
Please help.
My code (Simplified and this method will be nested in Column):
#Composable
fun DrawTimeLine(list: List<Unit>){
Canvas(modifier = Modifier
.fillMaxSize()
.scrollable(rememberScrollState(), Orientation.Vertical)
.padding(
top = Dimens.DEFAULT_MARGIN,
bottom = Dimens.BOTTOM_BAR_SIZE,
start = Dimens.DEFAULT_MARGIN
)){
var offsetY = 0f
val offsetX = 0f
val increaseValue = 150f
fun drawTimePoint() {
drawCircle(
color = Color.White,
radius = 15.dp.value,
center = Offset(offsetX, offsetY)
)
}
fun drawTimeLine() {
drawLine(
color = Color.White,
start = Offset(offsetX, offsetY),
end = Offset(offsetX, offsetY + increaseValue),
strokeWidth = 5.dp.value
)
offsetY += increaseValue
}
list.forEach {
drawTimePoint()
drawTimeLine()
}
}
}
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.
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
)
}