Animating drawLine using Jetpack Compose - android

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:

Related

How to make vertical dash/dotted in jetpack compose

I want to make dashed/dotted vertical line in canvas. I tried to use this answer 1. It works but not perfect. I'll show you my code, what I am tried.
#Composable
fun DrawProgressBar() {
val rangeComposition = RangeComposition()
val itemLst = rangeComposition.bpExplained
val boxSize = 30.dp
Box(
modifier = Modifier
.background(Color.White)
.height(height = boxSize)
) {
Canvas(
modifier = Modifier.fillMaxSize()
) {
val pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f, 10f), 0f)
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,
)
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 = 2.dp.toPx(),
pathEffect = pathEffect,
)
}
}
}
}
}
Actual Output
Expected Outout
You can use:
val pathEffect = PathEffect.dashPathEffect
(floatArrayOf(canvasHeight/19, canvasHeight/19), 0f)

Compose - Draw arc with gradient

I'm trying to implement the following component in compose
This is what I have so far
#Composable
fun CircularLoader(
size: Dp = 40.dp,
strokeWidth: Dp = 4.dp
) {
val stroke = with(LocalDensity.current) {
Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)
}
// draw on canvas
Canvas(
Modifier
.progressSemantics()
.size(size)
.padding(strokeWidth / 2)
) {
drawArc(
startAngle = 0f,
sweepAngle = 300f,
useCenter = false,
brush = Brush.linearGradient(listOf(
Color(0xFFEF7B7B),
Color(0x00EF7B7B)
)),
style = stroke
)
}
}
As Gabriele Mariotti said, try to use sweepGradient:
#Composable
#Preview
fun CircularLoaderPreview() {
Box(modifier = Modifier.background(Color(0xFF0F1A30))) {
Box(modifier = Modifier.padding(16.dp)) {
CircularLoader(size = 120.dp, strokeWidth = 18.dp)
}
}
}
#Composable
fun CircularLoader(
size: Dp,
strokeWidth: Dp
) {
val stroke = with(LocalDensity.current) {
Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)
}
// draw on canvas
Canvas(
modifier = Modifier
.progressSemantics()
.size(size)
.padding(strokeWidth / 2)
) {
drawArc(
startAngle = 0f,
sweepAngle = 300f,
useCenter = false,
brush = Brush.sweepGradient( // !!! that what
0f to Color(0x00EF7B7B),
0.9f to Color(0xFFEF7B7B),
0.91f to Color(0x00EF7B7B), // there was a problem with start of the gradient, maybe there way to solve it better
1f to Color(0x00EF7B7B)
),
style = stroke
)
}
}
Result:

Trying to Recreate Wordscapes circle input thingy in Jetpack-Compose

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!

implement a spinning activity indicator with Jetpack Compose

This is a share your knowledge, Q&A-style inspired by this question on Reddit and the one linked to stackoverflow
Result is
This is just a sample to show to build spinning progress indicator. Item count(8 or 12), animation duration, spinning item color or color of static items can be customized based on preference.
#Composable
private fun SpinningProgressBar(modifier: Modifier = Modifier) {
val count = 12
val infiniteTransition = rememberInfiniteTransition()
val angle by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = count.toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(count * 80, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
Canvas(modifier = modifier.size(48.dp)) {
val canvasWidth = size.width
val canvasHeight = size.height
val width = size.width * .3f
val height = size.height / 8
val cornerRadius = width.coerceAtMost(height) / 2
for (i in 0..360 step 360 / count) {
rotate(i.toFloat()) {
drawRoundRect(
color = Color.LightGray.copy(alpha = .7f),
topLeft = Offset(canvasWidth - width, (canvasHeight - height) / 2),
size = Size(width, height),
cornerRadius = CornerRadius(cornerRadius, cornerRadius)
)
}
}
val coefficient = 360f / count
for (i in 1..4) {
rotate((angle.toInt() + i) * coefficient) {
drawRoundRect(
color = Color.Gray.copy(alpha = (0.2f + 0.2f * i).coerceIn(0f, 1f)),
topLeft = Offset(canvasWidth - width, (canvasHeight - height) / 2),
size = Size(width, height),
cornerRadius = CornerRadius(cornerRadius, cornerRadius)
)
}
}
}
}
I created a library that contains other type of Spinners as can be seen in gif below is available here

value not updating (Jetpack Compose)

I'm trying to adapt a video tutorial to my own needs. Basically, I have a list of boxes and I want each one to animate with a delay of 1 second after the other. I don't understand why my code does not work. The
delay.value
does not appear to update. Any ideas?
#Composable
fun Rocket(
isRocketEnabled: Boolean,
maxWidth: Dp,
maxHeight: Dp
) {
val modifier: Modifier
val delay = remember { mutableStateOf(0) }
val tileSize = 50.dp
if (!isRocketEnabled) {
Modifier.offset(
y = maxHeight - tileSize,
)
} else {
val infiniteTransition = rememberInfiniteTransition()
val positionState = infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 2000,
delayMillis = delay.value,
easing = LinearEasing
)
)
)
modifier = Modifier.offset(
x = (maxWidth - tileSize) * positionState.value,
y = (maxHeight - tileSize) - (maxHeight - tileSize) * positionState.value,
)
listOf(
Color(0xffDFFF00),
Color(0xffFFBF00),
Color(0xffFF7F50),
Color(0xffDE3163),
Color(0xff9FE2BF),
Color(0xff40E0D0),
Color(0xff6495ED),
Color(0xffCCCCFF),
).forEachIndexed { index, color ->
Box(
modifier = modifier
.width(tileSize)
.height(tileSize)
.background(color = color)
)
delay.value += 1000
}
}
}
When a state remembered in a composable is changed , the entire composable gets re-composed.
So to achieve the given requirement,
Instead of using a delay as a mutableState we can simply use an Int delay and update its value in the forEach loop and create an animation with the updated delay.
.forEachIndexed { index, color ->
Box(
modifier = createModifier(maxWidth, maxHeight, tileSize, createAnim(delay = delay))
.width(tileSize)
.height(tileSize)
.background(color = color)
)
delay += 1000
}
Create the modifier with animation:-
fun createModifier(maxWidth: Dp, maxHeight: Dp, tileSize: Dp, positionState: State<Float>): Modifier {
return Modifier.offset(
x = ((maxWidth - tileSize) * positionState.value),
y = ((maxHeight - tileSize) - (maxHeight - tileSize) * positionState.value),
)
}
#Composable
fun createAnim(delay: Int): State<Float> {
val infiniteTransition = rememberInfiniteTransition()
return infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 2000,
delayMillis = delay,
easing = LinearEasing
)
)
)
}

Categories

Resources