The introduction
- For Example, Jetpack Compose is a new UI framework for Android development, which Google has recently introduced. It is composed’s declarative programming philosophy and the support of coroutines, which make it very comfortable for Compose to develop.
- Some time ago, I made a systematic study of Compose, so I selected animation content from Compose and communicated with you based on official documents and my own practical experience.
Purpose:
- The purpose of this two-part article series is to cover 80% of the development API needs and common problems in the Compose animation by the end of this article.
- I wanted to talk about the advantages of Compose animation and focus on the design and use of the officially packaged high-level API.
- In the next part, we will talk about the Compose animation API and briefly talk about the animation trigger process. We will also talk about multiple animation listening and concurrent execution.
Knowledge:
- I hope you have a basic understanding of both Kotlin coroutines and Jetpack Compose before reading this article
Why do I like composing animations with Compose?
1.1 Declarative programming
-
Thanks to declarative programming, you don’t have to spend as much time choosing between frame, tween, and property animation for most of the animation types; You don’t have to decide whether to animate with XML or use the Animation class;
-
Compose’s animations are written in code to the @compose method. It’s usually just a matter of identifying the properties (size, position, transparency, etc.) that need to be modified and using the appropriate animation type to modify those properties.
-
Let’s take an example: here’s a very simple animation that switches the size and transparency of an image when clicked.
-
If we write imperative programming, we need to think about using ScaleAnimation, AlphaAnimation, and AnimationSet to merge the animation. Write toBigAnimateSet and toSmallAnimateSet at the same time, and “command” the specified view to execute the animation.
-
In the world of Compose declarative programming, you just animate the specified properties from the original code, so let’s get a feel for that.
enum class HeartState { SMALL, BIG } // Define two types of hearts
var action by remember { mutableStateOf(HeartState.SMALL) } // Specify the state of the heart (also the trigger of the animation)
val animationTransition = updateTransition(targetState = action, label = "") // Create an animation class
// Define dimensions in different states
val size by animationTransition.animateDp(label = "Change the size") { state ->
if (state == HeartState.SMALL) 40.dp else 60.dp
}
// Define transparency in different states
val alpha by animationTransition.animateFloat(label = "Change transparency.") { state ->
if (state == HeartState.SMALL) 0.5 f else 1f
}
// Draw hearts
Image(
modifier = Modifier.size(size = size).alpha(alpha = alpha),
painter = painterResource(id = R.drawable.heart),
contentDescription = "heart"
)
// Click to trigger a state switch
Text(
text = "Switch",
modifier = Modifier
.padding(top = 10.dp, bottom = 50.dp)
.clickable { action = if (action == HeartState.SMALL) HeartState.BIG else HeartState.SMALL }
)
Copy the code
1.2 Packaged advanced apis
- Compose provides some of the high-level,Out of the boxThe animation API allows us to pass throughA small amount of codeYou can do the animation you want. For example, an animation appears and disappears, which we can provide with Compose
AnimatedVisibility
To implement. Maybe even a line or two of code, controlappearanceandexitThe way.
var isVisible by remember { mutableStateOf(true) }
AnimatedVisibility(
visible = isVisible,
enter = slideInVertically() + fadeIn(), // Horizontal + gradient display
exit = slideOutHorizontally() + fadeOut() // Add a gradient in the vertical direction
) {
Image(
modifier = Modifier.size(size = 50.dp),
painter = painterResource(id = R.drawable.heart),
contentDescription = "heart")}Copy the code
- Then it will have this effect (convenient!).
1.3 Tool Support
-
The IDE provides tooling support for the Compose animation. With The Arctic Fox version of Android Studio, we can check and debug the animation frame by frame, play the animation that the view switches from different states, and intuitively observe the specific data of the view to make perfect effect.
-
Ps: The current version of AS does not fully support animation, but it will be improved in the future.
- ✅ AnimateVisibility/updateTransition
- ❎ AnimatedContent / animate*AsState
Two, how to choose the appropriate animation implementation
In addition to the aforementioned AnimatedVisibility, Compose provides a number of encapsulated animations that have been specifically designed to conform to the best practices of the Material Design movement. Next, I will introduce them one by one.
- Use different apis for different scenarios by referring to the figure below.
- This article will take you through the high-level APIS and related content.
3. Animation based on content changes
3.1 Appearance and disappearance → Change content
- As mentioned in the example above, we can use Compose directly
AnimatedVisibility
Animation, now let’s look at the specific use:portal
@Composable
fun AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandIn(),
exit: ExitTransition = shrinkOut() + fadeOut(),
content: @Composable() AnimatedVisibilityScope.() -> Unit
)
Copy the code
- Parameter analysis:
-
Visible: Trigger for animation. When the value goes from false to true, the Enter animation is performed; Instead, an exit animation is executed;
-
Enter: The entry animation of the object, passing in a subclass of EnterTransition. Compose already packages highly easy-to-use animation classes like Fade, Slide, Scale, expand, and more;
-
Exit: The exit animation of the object, passed in a subclass of ExitTransition. All the above entry animations have corresponding exit animations.
-
Content: The content that needs to be animated. It is worth noting that content is currently defined in the AnimatedVisibilityScope, which provides a transition object that can be used directly, which can be understood as an object that synchronizes the animation state at any time. We can highly customize the animation process to customize other animations. (More on the Transition class later)
-
Add custom animation effects using the Transition from AnimatedVisibilityScope:
-
Example: We need to change the color of hearts at the same time as they appear and disappear
var isVisible by remember { mutableStateOf(true) }
AnimatedVisibility(
visible = isVisible,
enter = slideInVertically() + fadeIn(),
exit = slideOutHorizontally() + fadeOut()
) {
// Transition is a member of AnimatedVisibilityScope
val customColor by transition.animateColor(label = "Color change") { state ->
if (state == EnterExitState.Visible) Color.Blue else Color.Red
}
Icon(
modifier = Modifier.size(size = 50.dp),
painter = painterResource(id = R.drawable.heart),
tint = customColor,
contentDescription = "heart")}Copy the code
- A few additions:
- If you need to customize
AnimatedVisibility
内childrenThe in and out animation can be usedModifier.animateEnterExit
To recustomize the animation; - The appearance and disappearance animations correspond to Native
Visible
和Gone
State, which changes the layout container when the view disappears;
- If you need to customize
3.2 Fade in and fade out → Switch content
- We have priority
AnimatedContent
Animation, let’s look at the specific use:portal
@ExperimentalAnimationApi
@Composable
fun <S> AnimatedContent(
targetState: S,
modifier: Modifier = Modifier,
transitionSpec: AnimatedContentScope<S>. () - >ContentTransform = {
fadeIn(animationSpec = tween(220, delayMillis = 90)) with fadeOut(animationSpec = tween(90))
},
contentAlignment: Alignment = Alignment.TopStart,
content: @Composable() AnimatedVisibilityScope.(targetState: S) -> Unit
)
Copy the code
- Parameter analysis:
- from
<S>
As you can see, this is a generic method that can be adapted to different content types and can be usedS
To define its type; targetState
: The trigger of the animation, passing in the state of the next stage. For example, the content changes from “Hello” to “world”, where “world” is passed intargetState
;transitionSpec
: implements the animation specification. All the current parameter needs to be passed in is the returnContentTransform
Object method;ContentTransform
The generation of is usually calledwith
The infix method is generated as follows:
// ContentTransform records the view entry/exit animation infix fun EnterTransition.with(exit: ExitTransition) = ContentTransform(this, exit) Copy the code
- From the signature
AnimatedContentScope<S>.() -> ContentTransform
, means that within the current method, the user can base onAnimatedContentScope
The object inside returns the animation specification it needs.
contentAlignment
: Alignment of content. Enter and exit from the upper-left side of the layout by default;
- from
- about
AnimatedContentScope
:- forContent switchAnimation, within its Scope, in addition to providing a listener for the animation state
transition
Object is also provided to the callerinitialState
(state before change) andtargetState
(changed state) for our use;
- forContent switchAnimation, within its Scope, in addition to providing a listener for the animation state
- Next we will simply write a number switch animation:
var count by remember { mutableStateOf(0)}// The initial value is 0
AnimatedContent(
targetState = count,
transitionSpec = {
if (targetState > initialState) {
// As the number becomes larger, the entering number becomes deeper and the exiting number becomes shallower
slideInVertically({ height -> height }) + fadeIn() with slideOutVertically({ height -> -height }) + fadeOut()
} else {
// As the number becomes smaller, the entering number becomes deeper and the exiting number becomes shallower
slideInVertically({ height -> -height }) + fadeIn() with slideOutVertically({ height -> height }) + fadeOut()
}
}
) { targetCount ->
Text(text = "$targetCount")}// Start coroutine switch number, achieve the effect of automatic switch every 1 second
LaunchedEffect(Unit) {
while (true) {
if (count == 0) count++ else count--
delay(1000)}}Copy the code
- A few additions:
- You can still use other highly encapsulated apis for content switching animations, such as
animateContentSize
The animation effect changes the size of the viewCrossfade
(Fade in and out effect to achieve layout switch); - You can also combine
using
和SizeTransform
To define the size change during the switching process, which is not detailed here.
- You can still use other highly encapsulated apis for content switching animations, such as
infix fun ContentTransform.using(sizeTransform: SizeTransform?).
Copy the code
Animation based on effect state
4.1 Changes to a single property of a view
animate*AsState
Is a very simple API that only needs to be providedFinal value, the API will go fromThe current valueStart playing animation;- Compose the
Float
、Color
、Dp
、Size
、Offset
、Rect
、Int
、IntOffset
和IntSize
And other basic types are providedanimate*AsState
Method, we liftanimateFloatAsState
As an example
@Composable
fun animateFloatAsState(
targetValue: Float,
animationSpec: AnimationSpec<Float> = defaultAnimation,
visibilityThreshold: Float = 0.01f,
finishedListener: ((Float) - >Unit)? = null
): State<Float>
Copy the code
-
Parameter analysis:
targetValue
: Triggers the animation. When this value changes, the animation is triggered;animationSpec
: implements the animation specification. We’ll talk more about that later.visibilityThreshold
: Determines whether the target value is close to the threshold.finishedListener
: End of animation listener.
-
From the parameters are very easy to understand, length reasons do not make example introduction.
4.2 Changes of multiple properties of the view
- For a view where multiple properties need to be modified at the same time, we recommend this
updateTransition
。 - The idea here is to divide the view into different states, and then calculate the value of the properties in the different states from the state changes.
@Composable
fun <T> updateTransition(targetState: T, label: String? = null): Transition<T>
Copy the code
- In the same way,
targetState
Is the trigger of the animation, and its type can be different states that we customize; - As you can see, this method returns
Transition<T>
Object, which also provides a class that returns different typesanimate*
Method, and aboveanimate*AsState
Similar. - Code examples can be seen at the beginning of this article.
4.3 Attribute Changes for user-defined Types
- Whether it is
animate*AsState
orTransition.animate*
, we all encounter changes in the properties ofCustom types (non-basic types)In the case. Compose provides a convenient API for customizingChange the rules. And he isTwoWayConverter
; - First, where does he use it
@Composable
fun <T, V : AnimationVector> animateValueAsState(
targetValue: T,
typeConverter: TwoWayConverter<T, V>,
// ...
): State<T>
@Composable
inline fun <S, T, V : AnimationVector> Transition<S>.animateValue(
typeConverter: TwoWayConverter<T, V>,
// ...
): State<T>
Copy the code
convert
A) to b) to C) to D) toTwoWayConverter
Is to define aCustom typeintoN floating point valuesTo perform the animation and return the animationN floating point valuesintoCustom typeIn the process.- We look at the
animateRectAsState
It is easy to understand the above sentence.
AnimateValueAsState < animateValueAsState
@Composable
fun animateRectAsState( / *... * /): State<Rect> {
return animateValueAsState(
targetValue, Rect.VectorConverter, animationSpec, finishedListener = finishedListener
)
}
For the current method, the passed TwoWayConverter is rect. VectorConverter
private val RectToVector: TwoWayConverter<Rect, AnimationVector4D> =
TwoWayConverter(
convertToVector = { AnimationVector4D(it.left, it.top, it.right, it.bottom) },
convertFromVector = { Rect(it.v1, it.v2, it.v3, it.v4) }
)
// 3. The TwoWayConverter
type takes two arguments, T refers to the original type, V refers to the type to be converted;
,>
// AnimationVector4D refers to the transfer using a Vector that holds four floating-point values;
Rect -> AnimationVector4D -> Rect.
Copy the code
4.4 AnimationSpec
- An AnimationSpec is an animation specification that defines the rules by which the animation will be performed. The following commonly used apis are officially defined, which we can take a quick look at. portal
API | meaning | attribute |
---|---|---|
spring |
Popup animation | dampingRatio : Defines the spring elasticity, optional parameters such asSpring.DampingRatioHighBouncy ;stiffness : Defines the speed at which the spring moves towards the end valueSpring.StiffnessMedium |
tween |
Timing of the animation | durationMillis : Defines the duration of the animation;delayMillis : Defines the delay time for the animation to start.easing : defines the animation effect between the start value and the end value, as inLinearOutSlowInEasing |
keyframe |
Keyframe animation | withtween |
repeatable |
Repetition of the animation | iterations : number of iterations;animation : an animation that needs to be repeated;repeatMode : Repetitive patterns, such as starting from scratch (RepeatMode.Restart ) or start at the end (RepeatMode.Reverse ) |
Best practices for encapsulating the same animation
In some of the same scenarios, the same objects are executed for different views, so we should pull out the same parts. Of course, there’s a way to do all of this, and it’s not as simple as just taking functions.
- We’ll use the example at the beginning of this article to put this into practice.
- We can encapsulate the properties that the animation needs to change. Like the size of the heart and the alpha.
// Note that the State object is passed in to ensure that the view is continuously refreshed during reorganization
private class AnimateTransitionData(size: State<Dp>, alpha: State<Float>) {
val size by size // Kotlin's syntactic sugar, the same name delegate represents the value
val alpha by alpha
}
Copy the code
- Create a method that returns the above object, encapsulating the animation logic
@Composable
private fun updateTransitionData(targetState: HeartState): AnimateTransitionData {
val transition = updateTransition(targetState = targetState, label = "")
// Define dimensions in different states
val size = transition.animateDp(label = "Change the size") { state ->
if (state == HeartState.SMALL) 40.dp else 60.dp
}
// Define transparency in different states
val alpha = transition.animateFloat(label = "Change transparency.") { state ->
if (state == HeartState.SMALL) 0.5 f else 1f
}
// The transition object is used as an entry to remember, indicating that AnimateTransitionData is updated and returned every transitio update
return remember(transition) { AnimateTransitionData(size, alpha) }
}
Copy the code
- Loop the method execution into the original structure
var action by remember { mutableStateOf(HeartState.SMALL) }
val data = updateTransitionData(action)
// Draw hearts
Image(
modifier = Modifier
.size(size = data.size)
.alpha(alpha = data.alpha),
painter = painterResource(id = R.drawable.heart),
contentDescription = "heart"
)
Copy the code
Six, the summary
- Learn about some of the advantages of Compose development animation: API design based on declarative programming; Provides advanced API packaging based on Material Design; Support for new IDE features;
- Learned how to write animation based on content changes: control content to appear hidden, for example
AnimatedVisibility
, controlling content changesAnimatedContent
和Crossfade
And so on; - Learned how to write state-based animations; As controlling a single state change
animate*AsState
And control multi-state changesupdateTransition
And so on; - You learned about type converters for non-basic types
TwoWayConverter
; - You learned about best practices for encapsulating the same animation logic code;
Related articles:
- Let’s talk about Jetpack Compose animation.
- Let’s talk about Jetpack Compose animation.