My Composable has the following structure:
var expandedState by remember { mutableStateOf(false) }
Box(modifier = Modifier.animateContentSize()) {
Crossfade(targetState = expandedState) { expanded ->
if (expanded) {
ExpanedContent()
} else {
CollapsedContent()
}
}
}
The animation from collapsed to expanded state works as expected: both crossfade and size change animation run simultaneously. When collapsing, though. The crossfade animation runs first, after which the size change animation starts.
How can I animate the state in such a way that the crossfade and size change animations run simultaneously for both directions?
You could use one of the animate*AsState, but unfortunately the shorter animateContentSize is not usable here. Whenever you move to a targetsize which is smaller, animateContentSize does not animate at all. This is because the space in which it can do its animation in, is set before the animation kicks in. So, the space in which you wanted the animation to work in, is already gone. You can see this in the SizeAnimationModifier which the animateContentSize uses.
Below is an example which should work with animateDpAsState, which works by changing a single value:
#Composable
fun Foo() {
var expandedState by remember { mutableStateOf(false) }
val animationDurationMillis = 2000
val height = animateDpAsState(
targetValue = if (expandedState) 500.dp else 30.dp,
animationSpec = tween(
durationMillis = animationDurationMillis,
easing = LinearEasing
)
)
Box(modifier = Modifier
.height(height.value)
.clickable { expandedState = !expandedState }) {
Crossfade(
targetState = expandedState,
animationSpec = tween(
durationMillis = animationDurationMillis,
easing = LinearEasing
)
) { expanded ->
if (expanded) {
ExpandedContent()
} else {
CollapsedContent()
}
}
}
}
#Composable
fun CollapsedContent() {
Box(
Modifier
.fillMaxSize()
.background(Color.Red)
)
}
#Composable
fun ExpandedContent() {
Box(
Modifier
.fillMaxSize()
.background(Color.Green)
)
}
Related
I'm new to Jetpack Compose, and I'm trying to rotate the home screen with animation when the menu button is tapped. It works fine 3-5 times, but suddenly it lags like crazy and I don't know why? Is this a bug, or am I doing something wrong?
var isOpen by remember { mutableStateOf(false) }
val transition = updateTransition(targetState = isOpen, "Menu")
val rotation by transition.animateFloat(
transitionSpec = { spring(0.4f, Spring.StiffnessLow) },
label = "MenuRotation",
targetValueByState = { if (it) -30f else 0f }
)
val scale by transition.animateFloat(
label = "MenuScale",
targetValueByState = { if (it) 0.9f else 1f }
)
val translateX by transition.animateFloat(
transitionSpec = { tween(400) },
label = "MenuTranslation",
targetValueByState = { if (it) 536f else 0f }
)
Box(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
cameraDistance = density * 10f
rotationY = rotation
scaleX = scale
translationX = translateX
}
) {
HomeScreen()
}
Box {
DefaultButton(
onClick = { isOpen = ! isOpen },
modifier = Modifier
.padding(16.dp)
.padding(top = 32.dp)
.shadow(blur = 8.dp, radius = 16.dp)
.size(32.dp, 32.dp),
shape = Shapes.large,
) {
Icon(
imageVector = if (isOpen) Icons.Filled.Close else Icons.Filled.Menu,
contentDescription = "Menu",
tint = Color.Black,
)
}
}
Update #1
I found this in the logcat
Skipped 52 frames! The application may be doing too much work on its main thread.
Davey! duration=1067ms; Flags=0, FrameTimelineVsyncId=2511155, IntendedVsync=4294436921331, Vsync=4294870254647, InputEventId=0, HandleInputStart=4294871069349, AnimationStart=4294871070287, PerformTraversalsStart=4294871939089, DrawStart=4294872039558, FrameDeadline=4294486921330, FrameInterval=4294870971954, FrameStartTime=41666666, SyncQueued=4294872645860, SyncStart=4295312217578, IssueDrawCommandsStart=4295312304089, SwapBuffers=4295937520703, FrameCompleted=4295944298047, DequeueBufferDuration=5729, QueueBufferDuration=166719, GpuCompleted=4295944298047, SwapBuffersCompleted=4295937862943, DisplayPresentTime=4237530536663, CommandSubmissionCompleted=4295937520703,
Update #2
The animation works flawlessly when I comment out all text components and vice versa. So what's wrong with the text components?
Please check the HomeScreen composable, component recomposition counts。
I suspect that HomeScreen reorganizes too many times。
You can just replace #Composable HomeScreen with an #Composeable Image verify。
I have a toggle issue with Switch that can look as following:
In the picture you can see only two of many possible states.
I have complex business logics in ViewModel that updates my whole screen state after clicking on switch.
However, in order to make it easier and demonstrate you the problem I found a simple example that is similar to my real-life scenario.
#Composable
fun MyCoolWidget() {
var isChecked by remember { mutableStateOf(true) }
val scope = rememberCoroutineScope()
Switch(
checked = isChecked,
onCheckedChange = {
scope.launch {
delay(50) // to mimic the business logic and state update delay
// I use the 50 millis delay and then update the state
isChecked = it
}
},
)
}
Now you can test it by putting your finger to the one edge, holding the finger on the screen and moving it to the opposite edge. (Don't click on switch, SWIPE it!)
Observe the result.
How can I fix this problem?
Dependencies:
androidx.compose.material:material:1.1.1. Jetpack Compose version - 1.2.0-rc01. Kotlin version - 1.6.21
Thanks, best wishes!
It looks like a bug.
I would code my custom switch if i were you because you have to disable swipe feature.
Here is my custom ios like switch :
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun MySwitch(
modifier: Modifier = Modifier,
isChecked: Int,
onCheckedChange: (checked: Int) -> Unit
) {
var size by remember {
mutableStateOf(IntSize.Zero)
}
val marbleSize by remember(size) {
mutableStateOf(size.height.div(2))
}
val yOffset by remember(size, marbleSize) {
mutableStateOf((size.height.div(2) - marbleSize.div(2f)).toInt())
}
val marblePadding = 4.dp.value
val scope = rememberCoroutineScope()
val swipeableState = rememberSwipeableState(isChecked)
val backgroundColor = animateColorAsState(
targetValue = if (swipeableState.currentValue != 0) Color(0xFF34C759) else Color(0xD6787880)
)
val sizePx = size.width.minus(marbleSize + marblePadding.times(2))
val anchors = mapOf(0f to 0, sizePx - 1f to 1)
LaunchedEffect(key1 = swipeableState.currentValue, block = {
onCheckedChange.invoke(swipeableState.currentValue)
})
Box(
modifier = modifier
.aspectRatio(2f)
.clip(CircleShape)
.swipeable(
state = swipeableState,
anchors = anchors,
enabled = false, //because you need to disable swipe
orientation = Orientation.Horizontal
)
.pointerInput(Unit) {
detectTapGestures(
onTap = {
if (it.x > size.width.div(2))
scope.launch {
swipeableState.animateTo(
1,
anim = tween(250, easing = LinearEasing)
)
}
else
scope.launch {
swipeableState.animateTo(
0,
anim = tween(250, easing = LinearEasing)
)
}
}
)
}
.background(backgroundColor.value)
.onSizeChanged {
size = it
}
) {
Box(
modifier = Modifier
.padding(horizontal = marblePadding.dp)
.offset {
IntOffset(
x = swipeableState.offset.value.roundToInt(),
y = yOffset
)
}
.size(with(LocalDensity.current) { marbleSize.toDp() })
.clip(CircleShape)
.background(Color.Red)
)
}
}
I hope it helps you.
I want to animate only once and I'm trying different options. I wanted to use animate*AsState but that doesn't work since you have to kind of trigger it by doing some kind of SideEffect with LaunchedEffect when entering in that screen like this:
#Composable
fun StartAnimation(width: Float = 0f) {
var startAnimation by remember { mutableStateOf(false) }
val widthAnimation = animateDpAsState(targetValue = if(startAnimation) width.dp else 0.dp, tween(600))
Box(
modifier = Modifier
.padding(top = 30.dp)
.padding(horizontal = 30.dp)
.fillMaxWidth()
.height(20.dp)
.background(Color.Gray),
contentAlignment = Alignment.CenterStart
) {
Box(
Modifier
.background(Color.Black)
.width(widthAnimation.value)
.height(20.dp)
.background(Color.Black)
)
}
LaunchedEffect(key1 = true) {
startAnimation = true
}
}
Maybe there's a better way to do this and I'm missing it?
I don't think there is anything that will let you get rid of LaunchedEffect. But this might be better, at least you don't have to use that artificial variable:
val animatableWidth = remember { Animatable(0.dp, Dp.VectorConverter) }
LaunchedEffect(width) {
animatableWidth.animateTo(width.dp, tween(600))
}
Just create a custom fire-and-forget system. Works as a single-shot animator and can be spawned practically as many times as you like, in parallel.
#Composable
fun animateFloatAsState(
initialValue: Float,
targetValue: Float,
delay: Long = 0,
animationSpec: AnimationSpec<Float> = spring<Float>(),
visibilityThreshold: Float = 0.01f,
finishedListener: ((Float) -> Unit)? = null
): State<Float> {
var trigger by remember { mutableStateOf(false) }
return animateFloatAsState(
targetValue = if (trigger) targetValue else initialValue,
animationSpec = animationSpec,
visibilityThreshold = visibilityThreshold,
finishedListener = finishedListener
).also {
LaunchedEffect(Unit) {
delay(delay)
trigger = true
}
}
}
This is for Float, but can as easily be translated to fit Dp. I think it is just replacing the word Float with Dp
I am trying to convert my View based code to Compose. I have a composable which takes an image (Painter) as argument and displays it using Image composable. What I want is that whenever the argument value changes, my Image should do a 360 degree rotation and the image should change while angle is approx. 180 degree (i.e. mid-way in the animation)
This is the composable I made.
#Composable
fun MyImage(displayImage: Painter) {
Image(
painter = displayImage,
contentDescription = null,
modifier = Modifier
.size(36.dp)
.clip(CircleShape)
)
}
Right now when the displayImage changes, the new image is displayed immediately without any animation (obviously). How can I achieve the desired animation?
The code that I am trying to convert looks like this:
fun onImageChange(imageRes: Int) {
ObjectAnimator.ofFloat(imageView, View.ROTATION, 0f, 360f)
.apply {
addUpdateListener {
if (animatedFraction == 0.5f) {
imageView.setImageResource(imageRes)
}
}
start()
}
}
It can be done using Animatable.
Compose animations are based on coroutines, so you can wait for the animateTo suspend function to complete, change the image and run another animation. Here's a basic example:
var flag by remember { mutableStateOf(true) }
val resourceId = remember(flag) { if (flag) R.drawable.profile else R.drawable.profile_inverted }
val rotation = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
Column(Modifier.padding(30.dp)) {
Button(onClick = {
scope.launch {
rotation.animateTo(
targetValue = 180f,
animationSpec = tween(1000, easing = LinearEasing)
)
flag = !flag
rotation.animateTo(
targetValue = 360f,
animationSpec = tween(1000, easing = LinearEasing)
)
rotation.snapTo(0f)
}
}) {
Text("Rotate")
}
Image(
painterResource(id = resourceId),
contentDescription = null,
modifier = Modifier
.size(300.dp)
.rotate(rotation.value)
)
}
Output:
If you want to animate the changing images, you have to put two images in a Box and animate the opacity of both as they rotate using one more Animatable.
I know I can use the AnimatedVisibility Composable function and achieve slide-in animation for the visibility animation, but what I want to achieve is when one layout is in entering animation the other in the exit animation, something similar to the image below.
NB : I know that I should use Navigation compose for different screens and that animation between destinations is still under development, but I want to achieve this on the content of a part of screen, similar to CrossFade Animation.
As you mentioned, this animation should be implemented by the Navigation Library and there's a ticket opened to that.
Having that in mind, I'm leaving my answer here and I hope it helps...
I'll break it in three parts:
The container:
#Composable
fun SlideInAnimationScreen() {
// I'm using the same duration for all animations.
val animationTime = 300
// This state is controlling if the second screen is being displayed or not
var showScreen2 by remember { mutableStateOf(false) }
// This is just to give that dark effect when the first screen is closed...
val color = animateColorAsState(
targetValue = if (showScreen2) Color.DarkGray else Color.Red,
animationSpec = tween(
durationMillis = animationTime,
easing = LinearEasing
)
)
Box(Modifier.fillMaxSize()) {
// Both Screen1 and Screen2 are declared here...
}
}
The first screen just do a small slide to create that parallax effect. I'm also changing the background color from Red to Dark just to give this overlap/hide/dark effect.
// Screen 1
AnimatedVisibility(
!showScreen2,
modifier = Modifier.fillMaxSize(),
enter = slideInHorizontally(
initialOffsetX = { -300 }, // small slide 300px
animationSpec = tween(
durationMillis = animationTime,
easing = LinearEasing // interpolator
)
),
exit = slideOutHorizontally(
targetOffsetX = { -300 }, =
animationSpec = tween(
durationMillis = animationTime,
easing = LinearEasing
)
)
) {
Box(
Modifier
.fillMaxSize()
.background(color.value) // animating the color
) {
Button(modifier = Modifier.align(Alignment.Center),
onClick = {
showScreen2 = true
}) {
Text(text = "Ok")
}
}
}
The second is really sliding from the edges.
// Screen 2
AnimatedVisibility(
showScreen2,
modifier = Modifier.fillMaxSize(),
enter = slideInHorizontally(
initialOffsetX = { it }, // it == fullWidth
animationSpec = tween(
durationMillis = animationTime,
easing = LinearEasing
)
),
exit = slideOutHorizontally(
targetOffsetX = { it },
animationSpec = tween(
durationMillis = animationTime,
easing = LinearEasing
)
)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Blue)
) {
Button(modifier = Modifier.align(Alignment.Center),
onClick = {
showScreen2 = false
}) {
Text(text = "Back")
}
}
}
Here is the result:
After digging in the code of CrossFade I implemented a similar one for the cross slide and it enables reverse animation for when pressing backButton
Here it is : https://gist.github.com/DavidIbrahim/5f4c0387b571f657f4de976822c2a225
Usage Example
#Composable
fun CrossSlideExample(){
var currentPage by remember { mutableStateOf("A") }
CrossSlide(targetState = currentPage, reverseAnimation: Boolean = false) { screen ->
when (screen) {
"A" -> Text("Page A")
"B" -> Text("Page B")
}
}
}
Now we have an official solution for this. Lately Google Accompanist has added a library which provides Compose Animation support for Jetpack Navigation Compose..
https://github.com/google/accompanist/tree/main/navigation-animation
As of now, we don't have anything comparable to Activity Transitions in Compose.
Jetpack should be working on them I hope. An awful lot of transition APIs are either internal or private to Compose Library so implementing a fine one is harder.
If for production, Kindly use Activity/Fragment with Navigation Host. If not use AnimatedVisibility to slide without the navigation component.
https://issuetracker.google.com/issues/172112072