Trying to Recreate Wordscapes circle input thingy in Jetpack-Compose - android

I'm trying to recreate Wordscapes' circle input thingy.
That guy.
I have about 95% of it recreated. The only thing I'm missing is the removal side of it. When you drag back to the previous circle and it's removed.
I've tried a few things already and I keep running into similar issues. The line wigs out because it's removing and adding constantly. I've tried keeping a check, the last dot variable, nothing I seem to do has the intended effect to match that moving back to the circle to remove that from the list.
I have my code here
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun <T : Any> PatternInput2(
options: List<T>,
modifier: Modifier = Modifier,
optionToString: (T) -> String = { it.toString() },
dotsColor: Color,
dotsSize: Float = 50f,
letterColor: Color = dotsColor,
sensitivity: Float = dotsSize,
linesColor: Color = dotsColor,
linesStroke: Float,
circleStroke: Stroke = Stroke(width = linesStroke),
animationDuration: Int = 200,
animationDelay: Long = 100,
onStart: (Dot<T>) -> Unit = {},
onDotConnected: (Dot<T>) -> Unit = {},
onResult: (List<Dot<T>>) -> Unit = {}
) {
val scope = rememberCoroutineScope()
val dotsList = remember(options) { mutableListOf<Dot<T>>() }
var previewLine by remember {
mutableStateOf(Line(Offset(0f, 0f), Offset(0f, 0f)))
}
val connectedLines = remember { mutableListOf<Line>() }
val connectedDots = remember { mutableListOf<Dot<T>>() }
Canvas(
modifier.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
for (dots in dotsList) {
if (
it.x in Range(dots.offset.x - sensitivity, dots.offset.x + sensitivity) &&
it.y in Range(dots.offset.y - sensitivity, dots.offset.y + sensitivity)
) {
connectedDots.add(dots)
onStart(dots)
scope.launch {
dots.size.animateTo(
(dotsSize * 1.8).toFloat(),
tween(animationDuration)
)
delay(animationDelay)
dots.size.animateTo(dotsSize, tween(animationDuration))
}
previewLine = previewLine.copy(start = Offset(dots.offset.x, dots.offset.y))
}
}
}
MotionEvent.ACTION_MOVE -> {
previewLine = previewLine.copy(end = Offset(it.x, it.y))
dotsList.find { dots ->
it.x in Range(
dots.offset.x - sensitivity,
dots.offset.x + sensitivity
) && it.y in Range(
dots.offset.y - sensitivity,
dots.offset.y + sensitivity
)
}
?.let { dots ->
if (dots !in connectedDots) {
if (previewLine.start != Offset(0f, 0f)) {
connectedLines.add(
Line(
start = previewLine.start,
end = dots.offset
)
)
}
connectedDots.add(dots)
onDotConnected(dots)
scope.launch {
dots.size.animateTo(
(dotsSize * 1.8).toFloat(),
tween(animationDuration)
)
delay(animationDelay)
dots.size.animateTo(dotsSize, tween(animationDuration))
}
previewLine = previewLine.copy(start = dots.offset)
}
}
}
MotionEvent.ACTION_UP -> {
previewLine = previewLine.copy(start = Offset(0f, 0f), end = Offset(0f, 0f))
onResult(connectedDots)
connectedLines.clear()
connectedDots.clear()
}
}
true
}
) {
drawCircle(
color = dotsColor,
radius = size.width / 2 - circleStroke.width,
style = circleStroke,
center = center
)
val radius = (size.width / 2) - (dotsSize * 2) - circleStroke.width
if (dotsList.size < options.size) {
options.forEachIndexed { index, t ->
val angleInDegrees = ((index.toFloat() / options.size.toFloat()) * 360.0) + 50.0
val x = -(radius * sin(Math.toRadians(angleInDegrees))).toFloat() + (size.width / 2)
val y = (radius * cos(Math.toRadians(angleInDegrees))).toFloat() + (size.height / 2)
dotsList.add(
Dot(
id = t,
offset = Offset(x = x, y = y),
size = Animatable(dotsSize)
)
)
}
}
if (previewLine.start != Offset(0f, 0f) && previewLine.end != Offset(0f, 0f)) {
drawLine(
color = linesColor,
start = previewLine.start,
end = previewLine.end,
strokeWidth = linesStroke,
cap = StrokeCap.Round
)
}
for (dots in dotsList) {
drawCircle(
color = dotsColor,
radius = dotsSize * 2,
style = Stroke(width = 2.dp.value),
center = dots.offset
)
drawIntoCanvas {
it.nativeCanvas.drawText(
optionToString(dots.id),
dots.offset.x,
dots.offset.y + (dots.size.value / 3),
Paint().apply {
color = letterColor.toArgb()
textSize = dots.size.value
textAlign = Paint.Align.CENTER
}
)
}
}
for (line in connectedLines) {
drawLine(
color = linesColor,
start = line.start,
end = line.end,
strokeWidth = linesStroke,
cap = StrokeCap.Round
)
}
}
}
data class Dot<T : Any>(
val id: T,
val offset: Offset,
val size: Animatable<Float, AnimationVector1D>
)
data class Line(
val start: Offset,
val end: Offset
)
#Preview
#Composable
fun PatternInput2Preview() {
var wordGuess by remember { mutableStateOf("") }
Column {
Text(wordGuess)
PatternInput2(
options = listOf("h", "e", "l", "l", "o", "!", "!"),
modifier = Modifier
.width(500.dp)
.height(1000.dp)
.background(Color.Black),
optionToString = { it },
dotsColor = Color.White,
dotsSize = 100f,
letterColor = Color.White,
sensitivity = 50.sp.value,
linesColor = Color.White,
linesStroke = 30f,
circleStroke = Stroke(width = 30f),
animationDuration = 200,
animationDelay = 100,
onStart = {
wordGuess = ""
wordGuess = it.id
},
onDotConnected = { wordGuess = "$wordGuess${it.id}" },
onResult = { /*Does a final thing*/ }
)
}
}
I have tried to put in:
onDotRemoved = { wordGuess = wordGuess.removeSuffix(it.id.toString()) },
The logic to the outside code is working as intended, but it's the component itself that's having the issue.
I've tried to do this in the ACTION_MOVE after the current code there, but this isn't working as it should:
val dots = connectedDots.lastOrNull()
if (removableDot != null && connectedDots.size >= 2) {
if (
it.x in Range(
removableDot!!.offset.x - sensitivity,
removableDot!!.offset.x + sensitivity
) &&
it.y in Range(
removableDot!!.offset.y - sensitivity,
removableDot!!.offset.y + sensitivity
)// && canRemove
) {
canRemove = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
connectedLines.removeIf { it.end == removableDot!!.offset }
}
connectedDots.removeLastOrNull()
onDotRemoved(removableDot!!)
removableDot = null
connectedDots.lastOrNull()?.let { previewLine = previewLine.copy(start = it.offset) }
} else if (
it.x !in Range(
removableDot!!.offset.x - sensitivity,
removableDot!!.offset.x + sensitivity
) &&
it.y !in Range(
removableDot!!.offset.y - sensitivity,
removableDot!!.offset.y + sensitivity
)
) {
canRemove = true
}
}

I actually found the solution to this!
I was doing a few things funky here. For starters:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
connectedLines.removeIf { it.end == removableDot!!.offset }
}
Was the cause of the funky line issues I was having. Replacing it with:
connectedLines.removeLastOrNull()
Was the perfect fix for it! I didn't even think of just removing the last one. I slightly over engineered this part.
The second issue I had ran into was the onDotRemoved not working correctly. Since I'm doing the removal side of this like onDotRemoved = { wordGuess = wordGuess.removeSuffix(it.id.toString()) }, so, I was curious as to why it wasn't working:
connectedDots.removeLastOrNull()
onDotRemoved(removableDot!!)
It didn't make sense to me why this wasn't working...BUT! removableDot SHOULD be the last item in connectedDots so instead of using removableDot, I ended up doing:
connectedDots.removeLastOrNull()?.let(onDotRemoved)
and it worked perfectly!

Related

How to add drag to custom layout and change the color of the dragged surface in jetpack compose

I am using this tutorial
https://danielrampelt.com/blog/jetpack-compose-custom-schedule-layout-part-1/ and part 2
to draw a custom schedule. How can I want to use draggable inside of it so I can change the color of the dragged surface in jetpack compose. My aim is to drag an time slot vertically. This is my composable function. I have put the draggable function but I don't know how to implement it. Now my screen freezes, I can't scroll vertically.
#Composable
fun DynamicSchedule(
viewModel: CalenderViewModel,
modifier: Modifier = Modifier,
appointmentContent: #Composable (appointment: Appointment) -> Unit = {
ScheduleCard(
appointment = it
)
},
minDate: LocalDate,
maxDate: LocalDate,
dayWidth: Dp,
hourHeight: Dp
) {
val numDays = ChronoUnit.DAYS.between(minDate, maxDate).toInt() + 1
val dividerColor = if (MaterialTheme.colors.isLight) Color.LightGray else Color.DarkGray
var offsetY by remember { mutableStateOf(0f) }
Layout(
content = {
viewModel.state.value.appointmentList.sortedBy { it.startDate }
.forEach { appointment ->
Box(modifier = Modifier.appointmentData(appointment)) {
appointmentContent(appointment)
}
}
},
modifier = modifier
.drawBehind {
repeat(23) {
drawLine(
dividerColor,
start = Offset(0f, (it + 1) * hourHeight.toPx()),
end = Offset(size.width, (it + 1) * hourHeight.toPx()),
strokeWidth = 1.dp.toPx()
)
}
repeat(numDays - 1) {
drawLine(
dividerColor,
start = Offset((it + 1) * dayWidth.toPx(), 0f),
end = Offset((it + 1) * dayWidth.toPx(), size.height),
strokeWidth = 1.dp.toPx()
)
}
}
.pointerInput(Unit) {
detectTapGestures {
val x = it.x.toDp()
val y = it.y.toDp()
val time = y.value.toInt() / hourHeight.value
val date = (x.value.toInt() / dayWidth.value)
println("X: $x, Y: $y")
println("Day: $date, Time: $time")
}
}
.draggable(
orientation = Orientation.Vertical,
state = rememberDraggableState { delta ->
offsetY += delta
println("Delta: $offsetY")
}
),
) { measurables, constraints ->
println("i got recomposed ======== ")
val height = hourHeight.roundToPx() * 24
val width = dayWidth.roundToPx() * numDays
val placeablesWithAppointment = measurables.map { measurable ->
val appointment = measurable.parentData as Appointment
val appointmentDurationInMinutes =
ChronoUnit.MINUTES.between(
appointment.startDate.time.toJavaLocalTime(),
appointment.endDate.time.toJavaLocalTime()
)
val appointmentHeight =
((appointmentDurationInMinutes / 60f) * hourHeight.toPx()).roundToInt()
val placeable = measurable.measure(
constraints.copy(
minWidth = dayWidth.roundToPx(),
maxWidth = dayWidth.roundToPx(),
minHeight = appointmentHeight,
maxHeight = appointmentHeight
)
)
Pair(placeable, appointment)
}
layout(width, height) {
placeablesWithAppointment.forEach { (placeable, appointment) ->
//appointment time - midnight
val appointmentOffsetMinutes =
ChronoUnit.MINUTES.between(
LocalTime.MIN,
appointment.startDate.time.toJavaLocalTime()
)
val appointmentY =
((appointmentOffsetMinutes / 60f) * hourHeight.toPx()).roundToInt()
val appointmentOffsetDays =
ChronoUnit.DAYS.between(
minDate,
appointment.startDate.date.toJavaLocalDate()
).toInt()
val appointmentX = appointmentOffsetDays * dayWidth.roundToPx()
placeable.place(appointmentX, appointmentY)
}
}
}
}

How to get transparent while erasing the canvas in Jetpack Compose , now I'm getting white color?

how can I make some parts of canvas transparent? I want user to be able to erase parts of an photo like this link shows to be transparent. my canvas code:
Canvas(
modifier = modifier
.background(Color.Transparent)
) {
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
drawImage(
image = bitmap,
srcSize = IntSize(bitmap.width, bitmap.height),
dstSize = IntSize(canvasWidth, canvasHeight)
)
drawPath(
path = erasePath,
style = Stroke(
width = 30f,
cap = StrokeCap.Round,
join = StrokeJoin.Round
),
blendMode = BlendMode.Clear,
color = Color.Transparent,
)
restoreToCount(checkPoint)
}
}
What you get as Transparent is Color(0x00000000), white you get is the color of your background, even if you Canvas has transparent background, color of your root or parent Composable is white.
You need to draw checker layout or checker image first, inside Layer you should draw your image and path with BlendMode.Clear
val width = this.size.width
val height = this.size.height
val checkerWidth = 10.dp.toPx()
val checkerHeight = 10.dp.toPx()
val horizontalSteps = (width / checkerWidth).toInt()
val verticalSteps = (height / checkerHeight).toInt()
for (y in 0..verticalSteps) {
for (x in 0..horizontalSteps) {
val isGrayTile = ((x + y) % 2 == 1)
drawRect(
color = if (isGrayTile) Color.LightGray else Color.White,
topLeft = Offset(x * checkerWidth, y * checkerHeight),
size = Size(checkerWidth, checkerHeight)
)
}
}
val space = 20.dp.roundToPx()
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
// Destination
drawImage(
image = dstBitmap,
dstOffset = IntOffset(
space / 2,
space / 2
),
dstSize = IntSize(
canvasWidth - space, canvasHeight - space
)
)
// Source
drawPath(
color = Color.Transparent,
path = erasePath,
style = Stroke(
width = 30f,
cap = StrokeCap.Round,
join = StrokeJoin.Round
),
blendMode = BlendMode.Clear
)
restoreToCount(checkPoint)
}
Full implementation
#Composable
private fun MyImageDrawer(modifier: Modifier) {
// This is the image to draw onto
val dstBitmap = ImageBitmap.imageResource(id = R.drawable.landscape1)
// Path used for erasing. In this example erasing is faked by drawing with canvas color
// above draw path.
val erasePath = remember { Path() }
var motionEvent by remember { mutableStateOf(MotionEvent.Idle) }
// This is our motion event we get from touch motion
var currentPosition by remember { mutableStateOf(Offset.Unspecified) }
// This is previous motion event before next touch is saved into this current position
var previousPosition by remember { mutableStateOf(Offset.Unspecified) }
val drawModifier = modifier
.pointerMotionEvents(Unit,
onDown = { pointerInputChange ->
motionEvent = MotionEvent.Down
currentPosition = pointerInputChange.position
pointerInputChange.consume()
},
onMove = { pointerInputChange ->
motionEvent = MotionEvent.Move
currentPosition = pointerInputChange.position
pointerInputChange.consume()
},
onUp = { pointerInputChange ->
motionEvent = MotionEvent.Up
pointerInputChange.consume()
}
)
Canvas(modifier = drawModifier) {
val canvasWidth = size.width.roundToInt()
val canvasHeight = size.height.roundToInt()
// Draw or erase depending on erase mode is active or not
when (motionEvent) {
MotionEvent.Down -> {
erasePath.moveTo(currentPosition.x, currentPosition.y)
previousPosition = currentPosition
}
MotionEvent.Move -> {
erasePath.quadraticBezierTo(
previousPosition.x,
previousPosition.y,
(previousPosition.x + currentPosition.x) / 2,
(previousPosition.y + currentPosition.y) / 2
)
previousPosition = currentPosition
}
MotionEvent.Up -> {
erasePath.lineTo(currentPosition.x, currentPosition.y)
currentPosition = Offset.Unspecified
previousPosition = currentPosition
motionEvent = MotionEvent.Idle
}
else -> Unit
}
val width = this.size.width
val height = this.size.height
val checkerWidth = 10.dp.toPx()
val checkerHeight = 10.dp.toPx()
val horizontalSteps = (width / checkerWidth).toInt()
val verticalSteps = (height / checkerHeight).toInt()
for (y in 0..verticalSteps) {
for (x in 0..horizontalSteps) {
val isGrayTile = ((x + y) % 2 == 1)
drawRect(
color = if (isGrayTile) Color.LightGray else Color.White,
topLeft = Offset(x * checkerWidth, y * checkerHeight),
size = Size(checkerWidth, checkerHeight)
)
}
}
val space = 20.dp.roundToPx()
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
// Destination
drawImage(
image = dstBitmap,
dstOffset = IntOffset(
space / 2,
space / 2
),
dstSize = IntSize(
canvasWidth - space, canvasHeight - space
)
)
// Source
drawPath(
color = Color.Transparent,
path = erasePath,
style = Stroke(
width = 30f,
cap = StrokeCap.Round,
join = StrokeJoin.Round
),
blendMode = BlendMode.Clear
)
restoreToCount(checkPoint)
}
}
}
Outcome

How to create timed Instagram story loading bar using Jetpack Compose animation?

I would like to create a composable component very similar to Instagram story loading bar with 10 seconds of duration.
I had an idea how to do it but I'm not how to executed. I was thinking about adding a static bar (grey color) using a BOX and then add another bar (white color) which animates from 0 to final of composable width in 10 seconds.
Do you have any idea how I can implement this component?
You can use this Composable to create a segmented progress bar
private const val BackgroundOpacity = 0.25f
private const val NumberOfSegments = 8
private val StrokeWidth = 4.dp
private val SegmentGap = 8.dp
#Composable
fun SegmentedProgressIndicator(
/*#FloatRange(from = 0.0, to = 1.0)*/
progress: Float,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.primary,
backgroundColor: Color = color.copy(alpha = BackgroundOpacity),
strokeWidth: Dp = StrokeWidth,
numberOfSegments: Int = NumberOfSegments,
segmentGap: Dp = SegmentGap
) {
val gap: Float
val stroke: Float
with(LocalDensity.current) {
gap = segmentGap.toPx()
stroke = strokeWidth.toPx()
}
Canvas(
modifier
.progressSemantics(progress)
.fillMaxWidth()
.height(strokeWidth)
.focusable()
) {
drawSegments(1f, backgroundColor, stroke, numberOfSegments, gap)
drawSegments(progress, color, stroke, numberOfSegments, gap)
}
}
private fun DrawScope.drawSegments(
progress: Float,
color: Color,
strokeWidth: Float,
segments: Int,
segmentGap: Float,
) {
val width = size.width
val start = 0f
val gaps = (segments - 1) * segmentGap
val segmentWidth = (width - gaps) / segments
val barsWidth = segmentWidth * segments
val end = barsWidth * progress + (progress * segments).toInt()* segmentGap
repeat(segments) { index ->
val offset = index * (segmentWidth + segmentGap)
if (offset < end) {
val barEnd = (offset + segmentWidth).coerceAtMost(end)
drawLine(
color,
Offset(start + offset, 0f),
Offset(barEnd, 0f),
strokeWidth
)
}
}
}
You use it like this
var running by remember { mutableStateOf(false) }
val progress: Float by animateFloatAsState(
if (running) 1f else 0f,
animationSpec = tween(
durationMillis = 10_000,
easing = LinearEasing
)
)
Surface(color = MaterialTheme.colors.background) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
SegmentedProgressIndicator(
progress = progress,
modifier = Modifier
.padding(top = 64.dp, start = 32.dp, end = 32.dp)
.fillMaxWidth(),
)
Button(
onClick = { running = !running },
modifier = Modifier.padding(top = 32.dp)
) {
Text(
text = if (running) "Reverse Animation" else "Start Animation"
)
}
}
}
This is the result
You can animate the progress of a LinearProgressIndicator.
Something like:
var enabled by remember { mutableStateOf(false) }
val progress: Float by animateFloatAsState(
if (enabled) 1f else 0.0f,
animationSpec = tween(
durationMillis = 10000,
delayMillis = 40,
easing = LinearOutSlowInEasing
)
)
LinearProgressIndicator(
color = White,
backgroundColor = LightGray,
progress = progress,
modifier = Modifier.width(100.dp)
)
Just set enabled=true to start the animation (it is just an example).
try this solution, it should work:
var progress by remember {mutableStateOf(0.0f)}
var enabled by remember { mutableStateOf(true) }
LaunchedEffect(key1 = progress, key2 = enabled) {
if(progress<1 && enabled) {
delay(100L)
progress += 0.01F
}
}
LinearProgressIndicator(
color = White,
backgroundColor = LightGray,
progress = progress,
modifier = Modifier.width(100.dp)
)
This is can be a full sample solution you can see and try to extend as well as refactor. The code is self-explanatory and you will see how it goes.
A story composable:
#OptIn(ExperimentalAnimationApi::class)
#Composable
fun Story(story: Stories) {
AnimatedContent(story) {
when (story) {
Stories.ONE -> {
StoryContent("1")
}
Stories.TWO -> {
StoryContent("2")
}
Stories.THREE -> {
StoryContent("3")
}
}
}
}
#Composable
private fun StoryContent(content: String) {
Text("Story $content")
}
Story progress indicator:
#Composable
fun StoryProgressIndicator(running: Boolean, modifier: Modifier = Modifier, onTenSecondsOnThisStory: () -> Unit) {
val progress: Float by animateFloatAsState(
if (running) 1f else 0f,
animationSpec = tween(
durationMillis = if (running) 10_000 else 0,
easing = LinearEasing
)
)
if (progress == 1f) {
onTenSecondsOnThisStory()
}
LinearProgressIndicator(
progress, modifier
)
}
And a screen that contains the navigation among stories and playing the animation.
#OptIn(ExperimentalAnimationApi::class)
#Composable
fun InstagramLikeStories() {
var screenWidth by remember { mutableStateOf(1) }
var currentStory by remember { mutableStateOf(Stories.ONE) }
var currentStoryPointer by remember { mutableStateOf(0) }
var runningStoryOne by remember { mutableStateOf(false) }
var runningStoryTwo by remember { mutableStateOf(false) }
var runningStoryThree by remember { mutableStateOf(false) }
val runStoryOne = { runningStoryOne = true; runningStoryTwo = false; runningStoryThree = false }
val runStoryTwo = { runningStoryOne = false; runningStoryTwo = true; runningStoryThree = false }
val runStoryThree = { runningStoryOne = false; runningStoryTwo = false; runningStoryThree = true }
val stories = Stories.values()
LaunchedEffect(Unit) { runStoryOne() }
Column(
Modifier.fillMaxSize().onGloballyPositioned {
screenWidth = it.size.width
}.pointerInput(Unit) {
detectTapGestures(onTap = {
if ((it.x / screenWidth) * 100 > 50) {
if (currentStoryPointer == stories.size - 1) {
currentStoryPointer = 0
} else {
currentStoryPointer++
}
currentStory = stories[currentStoryPointer]
} else {
if (currentStoryPointer != 0) {
currentStoryPointer--
currentStory = stories[currentStoryPointer]
}
}
runStoryIndicator(currentStory, runStoryOne, runStoryTwo, runStoryThree)
})
}
) {
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {
StoryProgressIndicator(runningStoryOne, Modifier.weight(1f), onTenSecondsOnThisStory = {
runStoryIndicator(currentStory, runStoryOne, runStoryTwo, runStoryThree)
currentStoryPointer = 1
currentStory = stories[currentStoryPointer]
})
Spacer(Modifier.weight(0.1f))
StoryProgressIndicator(runningStoryTwo, Modifier.weight(1f), onTenSecondsOnThisStory = {
runStoryIndicator(currentStory, runStoryOne, runStoryTwo, runStoryThree)
currentStoryPointer = 2
currentStory = stories[currentStoryPointer]
})
Spacer(Modifier.weight(0.1f))
StoryProgressIndicator(runningStoryThree, Modifier.weight(1f), onTenSecondsOnThisStory = {
runStoryIndicator(currentStory, runStoryOne, runStoryTwo, runStoryThree)
// go to first one
currentStoryPointer = 0
currentStory = stories[currentStoryPointer]
})
}
}
Story(currentStory)
Row {
Button(onClick = {
}) {
Text("button")
}
}
}
private fun runStoryIndicator(
currentStory: Stories,
runStoryOne: () -> Unit,
runStoryTwo: () -> Unit,
runStoryThree: () -> Unit
) {
when (currentStory) {
Stories.ONE -> runStoryOne()
Stories.TWO -> runStoryTwo()
Stories.THREE -> runStoryThree()
}
}
enum class Stories {
ONE, TWO, THREE
}

Animating drawLine using Jetpack Compose

I am animating drawLine as follows
val animateFloat = remember { Animatable(0f) }
LaunchedEffect(animateFloat) {
animateFloat.animateTo(
targetValue = 1f,
animationSpec = tween(durationMillis = 1000, easing = LinearEasing))
}
Canvas(modifier = Modifier.fillMaxSize() ) {
onDraw = {
drawLine(
color = Color.Black,
Offset(size.width / 4, size.height / 6)
Offset(size.width / 4, (size.height / 6 + SCAFFOLD_HEIGHT) * animateFloat.value),
strokeWidth = 2f
)
}
}
This works absolutely fine. But when I add this inside a condition check, as follows,
if(lives == 5){
drawLine(
color = Color.Black,
Offset(size.width / 4, size.height / 6)
Offset(size.width / 4, (size.height / 6 + SCAFFOLD_HEIGHT) * animateFloat.value),
strokeWidth = 2f
)
}
Line is drawn, but fails to animate. Please let me know what might be happening under the hood.
Compose_version = 1.0.0-beta04
I'm not sure what you want to achieve... But assuming that you want to animate just in case lives == 5, you just need to wrap the LaunchedEffect with your condition.
For instance:
#Composable
fun LineAnimation(lives: Int) {
val animVal = remember { Animatable(0f) }
if (lives > 5) {
LaunchedEffect(animVal) {
animVal.animateTo(
targetValue = 1f,
animationSpec = tween(durationMillis = 1000, easing = LinearEasing)
)
}
}
Canvas(modifier = Modifier.size(200.dp, 200.dp)) {
drawLine(
color = Color.Black,
start = Offset(0f, 0f),
end = Offset(animVal.value * size.width, animVal.value * size.height),
strokeWidth = 2f
)
}
}
and where you're calling this Composable, you can do the following:
#Composable
fun AnimationScreen() {
var count by remember {
mutableStateOf(0)
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
Button(onClick = { count++ }) {
Text("Count $count")
}
LineAnimation(count)
}
}
This is the result:

How to update position change of objects on canvas in Jetpack Compose?

The code below is working fine apart from the fact that once I have dragged my rectangles, I can only select them again by touching the area where they were before I moved them. I don't know how to update their position once I have dragged them. I couldn't find how to do it in the doc, but maybe I was not looking in the right place (androidx.compose.foundation.gestures).
So this is the code that I am using so far:
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
var offsetX2 by remember { mutableStateOf(0f) }
var offsetY2 by remember { mutableStateOf(0f) }
val rect1 = RectF(offsetX, offsetY, offsetX + 200f, offsetY + 300f)
val rect2 = RectF(offsetX2, offsetY2, offsetX2 + 300f, offsetY2 + 400f)
var selectedRect: RectF? = null
val collision = RectF.intersects(rect1, rect2)
val imageBitmap = ImageBitmap(
1000, 1000, ImageBitmapConfig.Argb8888, false,
Color.Black.colorSpace
)
val imageBitmapCanvas = Canvas(imageBitmap)
val canvas = Canvas(imageBitmapCanvas.nativeCanvas)
val paint = Paint()
val rectanglePaint = Paint().apply {
color = android.graphics.Color.BLUE
style = Paint.Style.STROKE
strokeWidth = 8f
}
Column(
modifier = Modifier
.background(color = Color.DarkGray)
.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally
) {
TextField(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 30.dp),
value = textState.value,
onValueChange = { textState.value = it }
)
CanvasDrawScope().draw(Density(1.0f), LayoutDirection.Ltr, canvas,
Size(1000f, 1000f), ) {
drawRect(
topLeft = Offset(0f, 0f), color = if (collision) Color.Red else Color.Green,
size = Size(1000f, 1000f)
)
}
canvas.nativeCanvas.drawRect(rect1, rectanglePaint)
canvas.nativeCanvas.drawRect(rect2, rectanglePaint)
Image(bitmap = imageBitmap, "New Image", Modifier
.pointerInput(Unit) {
detectTapGestures(
onPress = {
val x = it.x
val y = it.y
selectedRect = when {
rect1.contains(x, y) -> rect1
rect2.contains(x, y) -> rect2
else -> null
}
},
)
}
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
if (selectedRect == rect1) {
offsetX += dragAmount.x
offsetY += dragAmount.y
} else {
offsetX2 += dragAmount.x
offsetY2 += dragAmount.y
}
}
})
I would be grateful for any ideas.
I changed something in your code in order to use a Canvas Composable.
In the detectDragGestures I update also the Offset in the selected Rect. I would avoid it but I didn't find a better solution.
data class RectData(
var size: Size,
var offset: Offset
)
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
var offsetX2 by remember { mutableStateOf(250f) }
var offsetY2 by remember { mutableStateOf(300f) }
val rectList = mutableListOf<RectData>()
var rectA = RectData(Size(200f,300f), Offset(offsetX, offsetY))
var rectB = RectData(Size(500f,600f), Offset(offsetX2, offsetY2))
rectList.add(rectA)
rectList.add(rectB)
var selectedRect: RectData? by remember { mutableStateOf(null) }
Canvas(modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(
onPress = {
val x = it.x
val y = it.y
selectedRect = null
rectList.forEach(){
val rect = RectF(
it.offset.x,
it.offset.y,
it.offset.x+it.size.width,
it.offset.y + it.size.height
)
if (rect.contains(x,y)) selectedRect = it
}
},
)
}
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
when (selectedRect) {
rectA -> {
offsetX += dragAmount.x
offsetY += dragAmount.y
rectA.offset = Offset(offsetX,offsetY) //update the offset
}
rectB -> {
offsetX2 += dragAmount.x
offsetY2 += dragAmount.y
rectB.offset = Offset(offsetX2,offsetY2) //update the offset
}
}
}
}
){
val canvasQuadrantSize = size / 2F
drawRect(
topLeft = Offset(0f,0f),
color = Color.Green,
size = canvasQuadrantSize
)
rectList.forEach(){
drawRect(
brush = SolidColor(Color.Blue),
topLeft = it.offset,
size = it.size,
style = Stroke(width = 8f)
)
}
}

Categories

Resources