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。
Related
I'm creating a 2D game with simple animations in jetpack compose. The game is simple, a balloon will float on top, a cannon will shoot arrow towards balloon. If the arrow hits balloon, player gets a point. I'm able to animate all of the above things but one. How would I detect when the arrow and balloon areas intersect?
The arrow is moving upwards with animation, also the balloon is animating side-ways. I want to capture the point at which the arrow touches the balloon. How would I proceed with this?
CODE:
Balloon Composable
#Composable
fun Balloon(modifier: Modifier, gameState: GameState) {
val localConfig = LocalConfiguration.current
val offsetX by animateDpAsState(
targetValue = if (gameState == GameState.START) 0.dp else (localConfig.screenWidthDp.dp - 80.dp),
infiniteRepeatable(
animation = tween((Math.random() * 1000).toInt(), easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
val offsetY by animateDpAsState(
targetValue = if (gameState == GameState.START) 0.dp else (localConfig.screenHeightDp.dp / 3),
infiniteRepeatable(
animation = tween((Math.random() * 1000).toInt(), easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Box(
modifier = modifier
.offset(offsetX, offsetY)
.size(80.dp)
.background(Color.Red, shape = CircleShape),
contentAlignment = Alignment.Center,
) {
Text(text = "D!!")
}
}
Cannon Composable:
#Composable
fun ShootBalloon(modifier: Modifier) {
val localConfig = LocalConfiguration.current
var gameState by remember { mutableStateOf(GameState.START) }
val offsetX by animateDpAsState(
targetValue = if (gameState == GameState.START) 0.dp else (localConfig.screenWidthDp.dp - 50.dp),
infiniteRepeatable(
animation = tween(2000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
val offsetY by animateDpAsState(
targetValue = if (gameState == GameState.START) 0.dp else (-(localConfig.screenHeightDp.dp - 50.dp)),
infiniteRepeatable(
animation = tween(500, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
Column(
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.Start
) {
if (gameState == GameState.START) {
Button(onClick = { gameState = GameState.PLAYING }) { Text(text = "Play") }
}
Box(
modifier = Modifier
.size(50.dp)
.offset(x = offsetX)
.background(Color.Green),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.ic_baseline_arrow_upward_24),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.offset(y = offsetY)
.onGloballyPositioned { layoutCoordinates ->
val offset = layoutCoordinates.positionInRoot()
}
)
}
}
}
I'm animating following things,
The cannon box moves side ways, the arrow image moves upwards.
The balloon has random movement.
Add a ticker and on each tick check the global coordinate of arrow and offsets of balloon. If values are same, it means they hit on another
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.
How show touch point using Android Jetpack Compose?
Example:
Here is my solution.
#Composable
fun TouchableFeedback() {
// Some constants here
val sizeAnimationDuration = 200
val colorAnimationDuration = 200
val boxSize = 100.dp
val startColor = Color.Red.copy(alpha = .05f)
val endColor = Color.Red.copy(alpha = .8f)
// These states are changed to update the animation
var touchedPoint by remember { mutableStateOf(Offset.Zero) }
var visible by remember { mutableStateOf(false) }
// circle color and size in according to the visible state
val colorAnimation by animateColorAsState(
if (visible) startColor else endColor,
animationSpec = tween(
durationMillis = colorAnimationDuration,
easing = LinearEasing
),
finishedListener = {
visible = false
}
)
val sizeAnimation by animateDpAsState(
if (visible) boxSize else 0.dp,
tween(
durationMillis = sizeAnimationDuration,
easing = LinearEasing
)
)
// Box for the whole screen
Box(
Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures {
// changing the state to set the point touched on the screen
// and make it visible
touchedPoint = it
visible = true
}
}
) {
// The touch offset is px and we need to convert to Dp
val density = LocalDensity.current
val (xDp, yDp) = with(density) {
(touchedPoint.x.toDp() - boxSize / 2) to (touchedPoint.y.toDp() - boxSize / 2)
}
// This box serves as container. It has a fixed size.
Box(
Modifier
.offset(xDp, yDp)
.size(boxSize),
) {
// And this box is animating the background and the size
Box(
Modifier
.align(Alignment.Center)
.background(colorAnimation, CircleShape)
.height(if (visible) sizeAnimation else 0.dp)
.width(if (visible) sizeAnimation else 0.dp),
)
}
}
}
Here is the result:
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)
)
}
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