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
Related
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 know that I can track the moment when lottie animation is completed using progress.
But the problem is that I want to start a new screen at the moment when the animation is completely finished.
Here is the code of my animation
#Composable
fun AnimatedScreen(
modifier: Modifier = Modifier,
rawId: Int
) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier.fillMaxSize()
) {
val compositionResult: LottieCompositionResult = rememberLottieComposition(
spec = LottieCompositionSpec.RawRes(rawId)
)
AnimatedScreenAnimation(compositionResult = compositionResult)
}
}
#Composable
fun AnimatedScreenAnimation(compositionResult: LottieCompositionResult) {
val progress by animateLottieCompositionAsState(composition = compositionResult.value)
Column {
if (progress < 1) {
Text(text = "Progress: $progress")
} else {
Text(
modifier = Modifier.clickable { },
text = "Animation is done"
)
}
LottieAnimation(
composition = compositionResult.value,
progress = progress,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.FillBounds
)
}
}
And here is code of my screen where i want to wait for the end of the animation and then go to a new screen:
#Composable
fun SplashScreen(
navController: NavController,
modifier: Modifier = Modifier,
viewModel: SplashScreenViewModel = getViewModel()
) {
val resIdState = viewModel.splashScreenResId.collectAsState()
val resId = resIdState.value
if (resId != null) {
AnimatedScreen(modifier = modifier, rawId = resId)
}
LaunchedEffect(true) {
navigate("onboarding_route") {
popUpTo(0)
}
}
}
I used the progress & listened to it's updates & as soon as it reaches 1f I'll call my function.
Example:
#Composable
fun Splash() {
LottieTest {
// Do something here
}
}
#Composable
fun LottieTest(onComplete: () -> Unit) {
val composition: LottieCompositionResult =
rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.camera))
val progress by animateLottieCompositionAsState(
composition.value,
iterations = 1,
)
LaunchedEffect(progress) {
Log.d("MG-progress", "$progress")
if (progress >= 1f) {
onComplete()
}
}
LottieAnimation(
composition.value,
progress,
)
}
Note: This is just the way I did it. The best way is still unknown(to me atleast). I feel it lacks the samples for that.
Also, You can modify a lot from this & just concentrate on the core flow.
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
}
I have a green square that I can drag vertically. But whenever I stop dragging it, I want it to reset the offset to the start with an animation. I tried it like this, but I can't figure it out. Does someone know how to do it?
#Composable
fun DraggableSquare() {
var currentOffset by remember { mutableStateOf(0F) }
val resetAnimation by animateIntOffsetAsState(targetValue = IntOffset(0, currentOffset.roundToInt()))
var shouldReset = false
Box(contentAlignment = Alignment.TopCenter, modifier = Modifier.fillMaxSize()) {
Surface(
color = Color(0xFF34AB52),
modifier = Modifier
.size(100.dp)
.offset {
when {
shouldReset -> resetAnimation
else -> IntOffset(0, currentOffset.roundToInt())
}
}
.draggable(
state = rememberDraggableState { delta -> currentOffset += delta },
orientation = Orientation.Vertical,
onDragStopped = {
shouldReset = true
currentOffset = 0F
}
)
) {}
}
}
You can define the offset as an Animatable.
While dragging use the method snapTo to update the current value as the initial value and the onDragStopped to start the animation.
val coroutineScope = rememberCoroutineScope()
val offsetY = remember { Animatable(0f) }
Box(contentAlignment = Alignment.TopCenter, modifier = Modifier.fillMaxSize()) {
Surface(
color = Color(0xFF34AB52),
modifier = Modifier
.size(100.dp)
.offset {
IntOffset(0, offsetY.value.roundToInt())
}
.draggable(
state = rememberDraggableState { delta ->
coroutineScope.launch {
offsetY.snapTo(offsetY.value + delta)
}
},
orientation = Orientation.Vertical,
onDragStopped = {
coroutineScope.launch {
offsetY.animateTo(
targetValue = 0f,
animationSpec = tween(
durationMillis = 3000,
delayMillis = 0
)
)
}
}
)
) {
}
}
I want to build this awesome button animation pressed from the AirBnB App with Jetpack Compose
Unfortunately, the Animation/Transition API was changed recently and there's almost no documentation for it. Can someone help me get the right approach to implement this button press animation?
Edit
Based on #Amirhosein answer I have developed a button that looks almost exactly like the Airbnb example
Code:
#Composable
fun AnimatedButton() {
val boxHeight = animatedFloat(initVal = 50f)
val relBoxWidth = animatedFloat(initVal = 1.0f)
val fontSize = animatedFloat(initVal = 16f)
fun animateDimensions() {
boxHeight.animateTo(45f)
relBoxWidth.animateTo(0.95f)
// fontSize.animateTo(14f)
}
fun reverseAnimation() {
boxHeight.animateTo(50f)
relBoxWidth.animateTo(1.0f)
//fontSize.animateTo(16f)
}
Box(
modifier = Modifier
.height(boxHeight.value.dp)
.fillMaxWidth(fraction = relBoxWidth.value)
.clip(RoundedCornerShape(8.dp))
.background(Color.Black)
.clickable { }
.pressIndicatorGestureFilter(
onStart = {
animateDimensions()
},
onStop = {
reverseAnimation()
},
onCancel = {
reverseAnimation()
}
),
contentAlignment = Alignment.Center
) {
Text(text = "Explore Airbnb", fontSize = fontSize.value.sp, color = Color.White)
}
}
Video:
Unfortunately, I cannot figure out how to animate the text correctly as It looks very bad currently
Are you looking for something like this?
#Composable
fun AnimatedButton() {
val selected = remember { mutableStateOf(false) }
val scale = animateFloatAsState(if (selected.value) 2f else 1f)
Column(
Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = { },
modifier = Modifier
.scale(scale.value)
.height(40.dp)
.width(200.dp)
.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
selected.value = true }
MotionEvent.ACTION_UP -> {
selected.value = false }
}
true
}
) {
Text(text = "Explore Airbnb", fontSize = 15.sp, color = Color.White)
}
}
}
Here's the implementation I used in my project. Seems most concise to me.
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val sizeScale by animateFloatAsState(if (isPressed) 0.5f else 1f)
Button(
onClick = { },
modifier = Modifier
.wrapContentSize()
.graphicsLayer(
scaleX = sizeScale,
scaleY = sizeScale
),
interactionSource = interactionSource
) { Text(text = "Open the reward") }
Use pressIndicatorGestureFilter to achieve this behavior.
Here is my workaround:
#Preview
#Composable
fun MyFancyButton() {
val boxHeight = animatedFloat(initVal = 60f)
val boxWidth = animatedFloat(initVal = 200f)
Box(modifier = Modifier
.height(boxHeight.value.dp)
.width(boxWidth.value.dp)
.clip(RoundedCornerShape(4.dp))
.background(Color.Black)
.clickable { }
.pressIndicatorGestureFilter(
onStart = {
boxHeight.animateTo(55f)
boxWidth.animateTo(180f)
},
onStop = {
boxHeight.animateTo(60f)
boxWidth.animateTo(200f)
},
onCancel = {
boxHeight.animateTo(60f)
boxWidth.animateTo(200f)
}
), contentAlignment = Alignment.Center) {
Text(text = "Utforska Airbnb", color = Color.White)
}
}
The default jetpack compose Button consumes tap gestures in its onClick event and pressIndicatorGestureFilter doesn't receive taps. That's why I created this custom button
You can use the Modifier.pointerInput to detect the tapGesture.
Define an enum:
enum class ComponentState { Pressed, Released }
Then:
var toState by remember { mutableStateOf(ComponentState.Released) }
val modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = {
toState = ComponentState.Pressed
tryAwaitRelease()
toState = ComponentState.Released
}
)
}
// Defines a transition of `ComponentState`, and updates the transition when the provided [targetState] changes
val transition: Transition<ComponentState> = updateTransition(targetState = toState, label = "")
// Defines a float animation to scale x,y
val scalex: Float by transition.animateFloat(
transitionSpec = { spring(stiffness = 50f) }, label = ""
) { state ->
if (state == ComponentState.Pressed) 1.25f else 1f
}
val scaley: Float by transition.animateFloat(
transitionSpec = { spring(stiffness = 50f) }, label = ""
) { state ->
if (state == ComponentState.Pressed) 1.05f else 1f
}
Apply the modifier and use the Modifier.graphicsLayer to change also the text dimension.
Box(
modifier
.padding(16.dp)
.width((100 * scalex).dp)
.height((50 * scaley).dp)
.background(Color.Black, shape = RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center) {
Text("BUTTON", color = Color.White,
modifier = Modifier.graphicsLayer{
scaleX = scalex;
scaleY = scaley
})
}
Here is the ScalingButton, the onClick callback is fired when users click the button and state is reset when users move their finger out of the button area after pressing the button and not releasing it. I'm using Modifier.pointerInput function to detect user inputs:
#Composable
fun ScalingButton(onClick: () -> Unit, content: #Composable RowScope.() -> Unit) {
var selected by remember { mutableStateOf(false) }
val scale by animateFloatAsState(if (selected) 0.7f else 1f)
Button(
onClick = onClick,
modifier = Modifier
.scale(scale)
.pointerInput(Unit) {
while (true) {
awaitPointerEventScope {
awaitFirstDown(false)
selected = true
waitForUpOrCancellation()
selected = false
}
}
}
) {
content()
}
}
OR
Another approach without using an infinite loop:
#Composable
fun ScalingButton(onClick: () -> Unit, content: #Composable RowScope.() -> Unit) {
var selected by remember { mutableStateOf(false) }
val scale by animateFloatAsState(if (selected) 0.75f else 1f)
Button(
onClick = onClick,
modifier = Modifier
.scale(scale)
.pointerInput(selected) {
awaitPointerEventScope {
selected = if (selected) {
waitForUpOrCancellation()
false
} else {
awaitFirstDown(false)
true
}
}
}
) {
content()
}
}
If you want to animated button with different types of animation like scaling, rotating and many different kind of animation then you can use this library in jetpack compose. Check Here