I'm trying to rewrite the looping animation(Snippet One) using keyframe animations in Jetpack Compose.
//Snippet One
#Composable
fun CircularLoadingAnimation() {
val startAngle = remember { Animatable(0F) }
Arc(startAngle = startAngle.value)
LaunchedEffect(Unit) {
launch {
while (true) {
startAngle.apply {
animateTo(targetValue = 240F, animationSpec = tweenSpec(500))
animateTo(targetValue = 360F, animationSpec = tweenSpec(500))
snapTo(0F)
}
}
}
}
Here's the solution I came up with(Snippet Two). This doesn't work.
I'm I thinking of this the wrongway?
//Snippet Two
#Composable
fun CircularLoadingAnimationAlt() {
val startAngle = remember { Animatable(0F) }
Arc(startAngle = startAngle.value)
LaunchedEffect(Unit) {
launch {
while (true) {
startAngle.animateTo(
targetValue = 360F,
animationSpec = keyframes {
240F at 500
360F at 1000
0F at 1001
}
)
}
}
}
}
Related
I have an application with 2 screens: (ScreenA and ScreenB)
I created a translation animation that launches at the start of the app on screenA.
val horizontalBias = remember { Animatable(0f) }
val alignment by derivedStateOf { BiasAlignment.Horizontal(horizontalBias.value) }
LaunchedEffect(key1 = 1) {
horizontalBias.animateTo(
targetValue = -1f,
animationSpec = tween(
durationMillis = durationMillis,
delayMillis = delayMillis
)
)
}
Box(modifier = modifier)
{
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = alignment)
{
...
}
}
I would like this animation to play when launching the application but not when navigating up from screenB
I would also like the animation not play when the orientation of device change
Edit
I don't know if this is a good way but I found a solution that works:
var horizontalBias by rememberSaveable {
mutableStateOf(0f)
}
val horizontalAnimatable = remember { Animatable(horizontalBias) }
val alignment by derivedStateOf { BiasAlignment.Horizontal(horizontalAnimatable.value) }
LaunchedEffect(true) {
if (horizontalBias > -1f) {
horizontalAnimatable.animateTo(
targetValue = -1f,
animationSpec = tween(
durationMillis = durationMillis,
delayMillis = delayMillis
)
)
horizontalBias = -1f
}
}
For animation to run once you can keep a value in VieModel and
LaunchedEffect(key1 = viewModel.isAnimationRun) {
if(!vieModel.isAnimationRun)
horizontalBias.animateTo(
targetValue = -1f,
animationSpec = tween(
durationMillis = durationMillis,
delayMillis = delayMillis
)
)
viewModel.animationRun = true
}
}
I'm trying to animate the movement of multiple circles on canvas at once. So far I managed to animate one, that moves to a random spot on canvas on every user click using Animatable. Now I want to add another 2 circles that do the same but move to another, also randomly chosen spot. Is there a way to achieve it easily without launching multiple coroutines?
My code so far:
#Composable
fun CanvasScreen(){
val animationScope = rememberCoroutineScope()
val animationX = remember{Animatable(0f)}
val animationY = remember{Animatable(0f)}
val randomColor = Color((Math.random() * 16777215).toInt() or (0xFF shl 24))
Canvas(
modifier = Modifier
.fillMaxSize()
.clickable {
animationScope.launch {
launch {
animationX.animateTo(
targetValue = (90..1000)
.random()
.toFloat()
)
}
launch {
animationY.animateTo(
targetValue = (90..1500)
.random()
.toFloat()
)
}
}
}
){
drawCircle(
color = randomColor,
radius = 90f,
center = Offset(animationX.value, animationY.value),
)
}
}
it took me a while but i finally got it done, there may be a simpler way of course but this can work.
#Composable
fun CanvasScreen(){
val circleNumber = 10
val animationScope = rememberCoroutineScope()
val randomColorList = remember { arrayListOf<Color>()}
val animationXList = remember { arrayListOf<Animatable<Float, AnimationVector1D>>() }
val animationYList = remember { arrayListOf<Animatable<Float, AnimationVector1D>>() }
for(i in 0 until circleNumber){
randomColorList.add(Color((Math.random() * 16777215).toInt() or (0xFF shl 24)))
animationXList.add(Animatable(0f))
animationYList.add(Animatable(0f))
}
Canvas(
modifier = Modifier
.fillMaxSize()
.clickable {
animationXList.forEach {
animationScope.launch {
launch {
it.animateTo(
targetValue = (90..1000)
.random()
.toFloat()
)
}
}
}
animationYList.forEach {
animationScope.launch {
launch {
it.animateTo(
targetValue = (90..1500)
.random()
.toFloat()
)
}
}
}
}
){
for(i in 0 until circleNumber) {
drawCircle(
color = randomColorList[i],
radius = 90f,
center = Offset(animationXList[i].value, animationYList[i].value),
)
}
}
}
I need to use swipe to reveal when swiping right and swipe to dismiss when swiping left with jetpack compose android how can I achieve that ?
You can modify this. Try it yourself or I may as well help in a while, but it should be fairly easy. Try it.
fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
// Used to calculate fling decay.
val decay = splineBasedDecay<Float>(this)
// Use suspend functions for touch events and the Animatable.
coroutineScope {
while (true) {
// Detect a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
val velocityTracker = VelocityTracker()
// Stop any ongoing animation.
offsetX.stop()
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// Update the animation value with touch events.
launch {
offsetX.snapTo(
offsetX.value + change.positionChange().x
)
}
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)
}
}
// No longer receiving touch events. Prepare the animation.
val velocity = velocityTracker.calculateVelocity().x
val targetOffsetX = decay.calculateTargetValue(
offsetX.value,
velocity
)
// The animation stops when it reaches the bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back.
offsetX.animateTo(
targetValue = 0f,
initialVelocity = velocity
)
} else {
// The element was swiped away.
offsetX.animateDecay(velocity, decay)
onDismissed()
}
}
}
}
}
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
I've solved such a problem with using this library dependency
de.charlex.compose:revealswipe:1.0.0
See here
Check this implementation here without additional libraries.
https://proandroiddev.com/swipe-to-reveal-in-jetpack-compose-6ffa8928a4c2
#Composable
fun DraggableCardComplex(
card: CardModel,
isRevealed: Boolean,
cardOffset: Float,
onExpand: () -> Unit,
onCollapse: () -> Unit,
) {
val offsetX by remember { mutableStateOf(0f) }
val transitionState = remember {
MutableTransitionState(isRevealed).apply {
targetState = !isRevealed
}
}
val transition = updateTransition(transitionState)
val offsetTransition by transition.animateFloat(
label = "cardOffsetTransition",
transitionSpec = { tween(durationMillis = ANIMATION_DURATION) },
targetValueByState = { if (isRevealed) cardOffset - offsetX else -offsetX },
)
Card(
modifier = Modifier
.offset { IntOffset((offsetX + offsetTransition).roundToInt(), 0) }
.pointerInput(Unit) {
detectHorizontalDragGestures { change, dragAmount ->
..
}
},
content = { CardTitle(cardTitle = card.title) }
)
}
How to implement a progress bar like animation to the second rectangle, as well as control the animation such as start & pause
#Composable
fun ProgressBar(modifier: Modifier = Modifier.size(300.dp, 180.dp)) {
Canvas(modifier = modifier.fillMaxSize().padding(16.dp)) {
val canvasWidth = size.width
val canvasHeight = size.height
val canvasSize = size
val percentage = 0.2F
drawRoundRect(
color = Pink80,
topLeft = Offset(x = 0F, y = 0F),
size = Size(canvasSize.width, 55F),
cornerRadius = CornerRadius(60F, 60F),
)
drawRoundRect(
color = Purple40,
topLeft = Offset(x = 0F, y = 0F),
size = Size(canvasWidth * 0.2F , 55F),
cornerRadius = CornerRadius(60F, 60F),
)
}
}
Here are two solutions. The first one uses Compose's own LinearProgressIndicator. The second one is a custom built one. It is very simple and has the advantage that you can easily customize it.
No need to use a canvas. When the progress bar reaches it's maximum, the code resets its value back to zero, but you can leave it set to 100% by not resetting the value. The LaunchedEffect is only being used to simulate updating the value. In a production app, you wouldn't use this but rather update the value with some other means that aligns with your code's business logic...
LinearProgressIndicator:
See: LinearProgressIndicator
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
var enabled by remember { mutableStateOf(false) }
var progress by remember { mutableStateOf(0.1f) }
val animatedProgress by animateFloatAsState(
targetValue = progress,
)
LaunchedEffect(enabled) {
while ((progress < 1) && enabled) {
progress += 0.005f
delay(10)
}
}
if (progress >= 1f) {
enabled = false
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally
) {
LinearProgressIndicator(progress = animatedProgress, modifier = Modifier.requiredHeight(20.dp))
Spacer(Modifier.requiredHeight(30.dp))
Button(
onClick = {
enabled = !enabled
}
) {
if (enabled) {
Text("Pause")
} else {
Text("Start")
}
}
}
}
}
}
Custom Progress Indicator:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
var enabled by remember { mutableStateOf(false) }.apply { this.value }
var progressValue by remember { mutableStateOf(0) }
LaunchedEffect(enabled) {
while ((progressValue < 100) && enabled) {
progressValue++
delay(10)
}
if (progressValue == 100) {
enabled = false
progressValue = 0
}
}
Column(modifier = Modifier.fillMaxSize().padding(20.dp)) {
ProgressBar(value = progressValue)
Button(
onClick = {
enabled = !enabled
}
) {
if (enabled) {
Text("Pause")
} else {
Text("Start")
}
}
}
}
}
}
#Composable
fun ProgressBar(
value: Int
) {
Box(
modifier = Modifier
.fillMaxWidth()
.requiredHeight(20.dp)
.border(width = 1.dp, color = Color.Black)
) {
Box(
modifier = Modifier
.fillMaxWidth(value.toFloat() / 100f)
.fillMaxHeight()
.background(color = Color.Red)
) {
}
}
}
https://github.com/DogusTeknoloji/compose-progress
You can check this library as support animated and also step progress. The library base on canvas
I want to use Compose instead of ConstraintLayout in this codelab: https://codelabs.developers.google.com/codelabs/advanced-android-kotlin-training-property-animation/#1
How can I apply any animation to Compose?
As of beta08 now it's lot easier
#Composable
fun RotateLoader() {
val animation = rememberInfiniteTransition()
val angle = animation.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
)
)
Icon(
painter = painterResource(id = R.drawable.ic_loader),
contentDescription = null,
modifier = Modifier.rotate(angle.value)
)
}
Here is the guide how to apply animation:
And piece of code from that tutorial:
private val rotation = FloatPropKey()
private fun createDefinition(duration: Int) = transitionDefinition {
state(0) { this[rotation] = 0f }
state(1) { this[rotation] = 360f }
transition {
rotation using repeatable {
animation = tween {
easing = LinearEasing
this.duration = duration
}
iterations = Infinite
}
}
}
#Composable
fun RotateIndefinitely(durationPerRotation: Int, children: #Composable() () -> Unit) {
Transition(definition = createDefinition(durationPerRotation), initState = 0, toState = 1) {
Rotate(it[rotation], children)
}
}
#Composable
fun Rotate(degree: Float, children: #Composable() () -> Unit) {
Draw(children = children) { canvas, parent ->
val halfWidth = parent.width.value / 2
val halfHeight = parent.height.value / 2
canvas.save()
canvas.translate(halfWidth, halfHeight)
canvas.rotate(degree)
canvas.translate(-halfWidth, -halfHeight)
drawChildren()
canvas.restore()
}
}
#Composable
private fun RotatingPokeBall() {
RotateIndefinitely(durationPerRotation = 4000) {
Opacity(opacity = 0.75f) {
DrawImage(
image = +imageResource(R.drawable.pokeball),
tint = +colorResource(R.color.poke_red)
)
}
}
}