On Android views, we had an animation using an OvershootInterpolator
We want to replicate the same animation in Jetpack Compose.
I see that there are several AnimationSpec described in https://developer.android.com/jetpack/compose/animation but I don't see which one might replicate the OvershootInterpolator
tween spec doesn't seem to do any overshoot, just animating between start and end value without overshooting
spring spec does overshooting, however it doesn't have a durationMillis parameter as tween, so we can't control how fast it plays
keyFrames spec seems a possible solution by doing something like this:
animationSpec = keyframes {
durationMillis = 500
0f at 100 with FastOutSlowInEasing
// Overshoot value to 50f
50f * 2 at 300 with LinearOutSlowInEasing
// Actual final value after overshoot animation
25f at 500
}
Is there a better / simpler way than using keyFrames to replicate OvershootInterpolator?
You can use any Interpolator from Animator world via a custom Easing:
animationSpec = tween(easing = Easing {
OvershootInterpolator().getInterpolation(it)
})
See Easing interface: https://developer.android.com/reference/kotlin/androidx/compose/animation/core/Easing
Though I would recommend using springs over interpolators, especially ones that mimic springs. Springs would give a much more natural look. :)
We can use spring animation of compose to achieve OvershootInterpolator. Even though we cannot provide any custom duration for the animation, we can control the speed of the animation using the stiffness attribute.
We can put any custom float values to the stiffness attribute.
androidx.compose.animation.core currently provides 4 stiffness constant values by default i.e,
object Spring {
/**
* Stiffness constant for extremely stiff spring
*/
const val StiffnessHigh = 10_000f
/**
* Stiffness constant for medium stiff spring. This is the default stiffness for spring
* force.
*/
const val StiffnessMedium = 1500f
/**
* Stiffness constant for a spring with low stiffness.
*/
const val StiffnessLow = 200f
/**
* Stiffness constant for a spring with very low stiffness.
*/
const val StiffnessVeryLow = 50f
.....
}
We can check which stiffness suits our case.
For eg:- (medium speed)
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium // with medium speed
)
We can also put a custom value to the stiffness for eg:-
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy, // this would define how far the overshoot would happen.
stiffness = 1000f // with custom speed less than medium speed.
)
You can play around with custom float values for stiffness to achieve your ideal duration for the animation.
Related
I have a HorizontalPager (from Accompanist) with a currentPage focus effect. On the emulator, it works correctly and with the correct spacing that I intend, however, on a smartphone, it doesn't respect the itemSpacing and completely ignores it, overlapping the pager items on the right. Is there any way to make this pager always have the same spacing regardless of the device? Or, at least, that the item on the right does not overlap and that the main item overlaps the other two?
Below a few screenshots of what is happening and the behavior that I want:
Correct behavior (working on emulator):
Wrong behavior & problem (working on physical device):
Code:
HorizontalPager(
state = state,
count = items.count(),
contentPadding = PaddingValues(horizontal = 72.dp),
itemSpacing = 10.dp,
modifier = modifier.padding(top = 16.dp)
) { page ->
Card(
shape = RectangleShape,
modifier = Modifier
.graphicsLayer {
// Calculate the absolute offset for the current page from the
// scroll position. We use the absolute value which allows us to mirror
// any effects for both directions
val pageOffset = calculateCurrentOffsetForPage(page).absoluteValue
// We animate the scaleX + scaleY, between 85% and 100%
lerp(
start = 0.85f,
stop = 1f,
fraction = 1f - pageOffset.coerceIn(0f, 1f)
).also { scale ->
scaleX = scale
scaleY = scale
}
// We animate the alpha, between 50% and 100%
alpha = lerp(
start = 0.6f,
stop = 1f,
fraction = 1f - pageOffset.coerceIn(0f, 1f)
)
}
.requiredWidth(284.dp)
.requiredHeight(168.dp)
//.coloredShadow(Shadow, 0.28f)
) {
// content
}
}
Any additional necessary code can be provided, just let me know.
You should be using how to support different screen sizes in android with jetpack compose, in which you can define dimensions for different configurations.
You can refer to this tutorial on the same
https://proandroiddev.com/supporting-different-screen-sizes-on-android-with-jetpack-compose-f215c13081bd
I am trying to implement a scrolling image that is much wider then device's screen. See example:
Right now I implemented it using Row with horizontalScroll modifier, and calling scrollState.animateScrollTo
I do not like this implementation since I need this animation to go back and forth if the image reaches it's end, and scrolling it using animateScrollTo results in ugly code (need to call it in a loop to reverse when it's do) and it's not completely smooth for some reason.
while (true) {
animationPositions.forEach {
if ((scrollState.maxValue * it).toInt() != scrollState.value) {
scrollState.animateScrollTo(
(scrollState.maxValue * it).toInt(),
animationSpec = tween(
cycleDurationMillis / animationPositions.size,
easing = LinearEasing
)
)
}
}
}
Is there a way to do this in cleaner way? I was thinking about simple offset/translationX usage, but the image is clipped on the sides because of parent container (e.g. if I just put in a column and try to move it with offset).
Is there a way to not clip the image if it's too large to fit?
Thanks
// Min and Max possible states
val minima = ...
val maxima = ...
val value by rememberInfiniteTransition().animateInt(
initialValue = minima,
targetValue = maxima,
animationSpec = infiniteRepeatable(
animation = tween(
cycleDurationMillis / animationPositions.size,
easing = LinearEasing
)
),
repeatMode = RepeatMode.Restart
)
scrollState.scrollTo(value) // Do not use animateScrollState here, that's already taken care of
How can I create an animation similar to the stock android launcher animation applied to apps when you change homescreen pages.
Here's a gif: https://i.stack.imgur.com/Zh7qE.gif
As the page swipes, the icons slightly overshoot their mark, and settle back to the center. I don't see how I can do that with a PageTransformer and I can't find any resources to point me in the right direction.
I once created a one-time bounce animation:
val pixels = binding.pager.width / 8
ValueAnimator.ofInt(0, pixels).apply {
duration = 200L
interpolator = DecelerateInterpolator()
repeatCount = 3
repeatMode = ValueAnimator.REVERSE
addUpdateListener {
binding.pager.scrollX = it.animatedValue as Int
}
}.start()
Use ofInt(0, pixels) for bouncing left or ofInt(0, -pixels) to bouncing right.
In my code I used a dragging distance of 1/8 of the view pager.
Feel free to choose how much you want to bounce:
val pixels = PIXELS_TO_DRAG
Finally, the repeatCount determines how many bounces - use 3 for 2 bounces, 5 for 3 bounces, 7 for 4 bounces etc.
You can fire this animation each time a pager transition end and tweak the parameters to get the desired behavior.
Enjoy,
Hope it helps :)
According to #Shlomi, you can do this implementation:
var currentPage = 0
binding.viewPager.onPageChangeListener {
val pixels = binding.vpBennefits.width / 8
ValueAnimator.ofInt(
0, if (currentPage < it)
pixels
else
-pixels
).apply {
duration = 200L
interpolator = DecelerateInterpolator()
repeatCount = 1
repeatMode = ValueAnimator.REVERSE
addUpdateListener {
binding.vpBennefits.scrollX = it.animatedValue as Int
}
}.start()
currentPage = it
}
By this way you can obtain the desired effect in both sides
Loosely, this is my code, boiled down to the relevant parts:
fun animate(tv: TextView) {
var animationSlide = TranslateAnimation(0.0f, 0.0f, 100.0f, 0f)
var animationScale = ScaleAnimation(1.0f, 1.0f, 0.0f, 1.0f)
animationSlide.interpolator = AccelerateInterpolator() // want custom interpolator
animationScale.interpolator = DecelerateInterpolator() // want custom interpolator
animationSlide.duration = 1000
animationScale.duration = 1000
var animationSet = AnimationSet(false)
animationSet.addAnimation(animationSlide)
animationSet.addAnimation(animationScale)
tv.startAnimation(animationSet)
}
Is it possible to make a custom interpolator which takes an array of points, or a lambda function which takes a time input and returns a value for that time point? The Android documentation only shows a small list of interpolators, none of which can be customised to a general function curve.
Thanks for any help!
This is done by making a class that implements Interpolator. Or you can use a SAM-converted lambda. For example, an interpolation based on the smoothstep function:
animationSlide.setInterpolator { amount ->
amount * amount * (3f - 2f * amount)
}
If you want to define a set of points to define your curve, you can do that with the Path class and instantiate a PathInterpolator with that. From looking at the source code, it looks like PathInterpolator approximates the Path by breaking it down into straight line segments, so if there are arcs and Beziers, it could potentially become kind of a heavy class instance that you might want to cache a reference to if you use it repeatedly.
I have an AnimatedFloat that I created using
val animatedFloat = animatedFloat(0f)
I start the animation using
animatedFloat.animateTo(
targetValue = 1f,
anim = repeatable(
iterations = AnimationConstants.Infinite,
animation = tween(durationMillis = 2000, easing = LinearEasing),
)
)
All these works and it starts to move from 0f to 1f in 2 seconds.
When I stop it using the below, it stops well.
animatedFloat.stop()
However when I restart it using the same animateTo function i.e.
animatedFloat.animateTo(
targetValue = 1f,
anim = repeatable(
iterations = AnimationConstants.Infinite,
animation = tween(durationMillis = 2000, easing = LinearEasing),
)
)
It doesn't start from 0f anymore. It starts from the value where it previously stopped. Not only that, it will take the same amount of time from where it previously stop to the targetValue
e.g. if it stop at 0.7f, then it will take 2s to start from 0.7f to 1.0f, which slows down the rate of change.
How can I reset the initialValue after stopping it?
The stop function simply stops the animation, it doesn't reset the value.
You can use animatedFloat.snapTo(0f) to set the value to 0f immediately without any animation.
https://developer.android.com/reference/kotlin/androidx/compose/animation/core/AnimatedFloat#snapto
AnimatedFloat is designed around fluidity. It assumes that in most cases it's undesirable to skip to a different value. That's why it stops where it is, and picks up from where it was stopped the next time animateTo is called. Could you share your use cases for pausing/resuming the animations?