Jetpack Compose Animations

From Beginner to Hero

Wajahat Karim

wajahatkarim.com

🔥 Google Dev Expert (GDE) in Android
⭐️ Android Engineering at Nebula
📱 Over 10 years in Android
💻 Open Source Contributor .
📝 Technical Blogs & Books Writer
🎤 International Public Speaker

Howdy! This is Wajahat Karim

Jetpack Compose

a modern UI toolkit which simplifies and accelerates UI development on Android with less code, powerful tools, and intuitive Kotlin APIs.

Made with Jetpack Compose

Animations in Jetpack Compose

  • The animateContentSize() Modifier
  • The AnimatedVisibility Composable
  • Single Value Animations with animatec methods
  • Repeated Animations with Animatable
  • Utilizing transitions to make solid animations

Animations in Jetpack Compose

  • The animateContentSize() Modifier
  •  
  • The AnimatedVisibility Composable
  • Single Value Animations with animate*AsState() methods
  • Repeated Animations with Animatable
  • Utilizing transitions to make solid animations

animateContentSize() Modifier

@Composable
fun ExpandableText() {
    val shortText = "Click me"
    val longText = "Very long text passage that spans
    		\nacross multiple lines, paragraphs
            	\nand pages"
    var short by remember { mutableStateOf(true) }
    Box(
        modifier = Modifier
            .background(
                Color.Blue,
                RoundedCornerShape(15.dp)
            )
            .clickable { short = !short }
            .padding(20.dp)
            .wrapContentSize()
            .animateContentSize()
    ) {
        Text(
            if (short) {
                shortText
            } else {
                longText
            },
            style = TextStyle(color = Color.White)
        )
    }
}

animateContentSize() Modifier

@Composable
fun PortraitModeImage() {
  var portraitMode by remember { mutableStateOf(true) }
  Box(
    Modifier.clickable { portraitMode = !portraitMode }
      .sizeIn(maxWidth = 300.dp, maxHeight = 300.dp)
      .background(
          if (portraitMode) Color.Yellow else Color.Green)
      .animateContentSize(
          animSpec = tween(500, easing = LinearEasing),
          endListener = { startSize, endSize ->  
             Log.d("Compose", "$startSize -> $endSize")
          }
      )
      .aspectRatio(if (portraitMode) 3 / 4f else 16 / 9f)
    ) {
        Text(
          if (portraitMode) {
             "3 : 4"
          } else {
             "16 : 9"
          },
          style = TextStyle(color = Color.Black)
      )
    }
}

Animations in Jetpack Compose

  • The AnimatedVisibility Composable
  • The animateContentSize() Modifier
  •  
  • Single Value Animations with animate*AsState() methods
  • Repeated Animations with Animatable
  • Utilizing transitions to make solid animations

AnimatedVisbility Composable

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun VisibilityAnimationFAB() {
    var expanded by remember { mutableStateOf(true) }
    FloatingActionButton(
        onClick = { expanded = !expanded },
    ) {
        Row(Modifier.padding(start = 16.dp, end = 16.dp)) {
            Icon(
                imageVector = Icons.Default.Favorite,
                contentDescription = "Favorite Icon",
                Modifier.align(Alignment.CenterVertically)
            )
            AnimatedVisibility(
                expanded,
                modifier = Modifier.align(Alignment.CenterVertically)
            ) {
                Text(modifier = Modifier.padding(start = 8.dp), text = "Like")
            }
        }
    }
}

AnimatedVisbility Composable

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun VisibilityAnimationFAB() {
    var expanded by remember { mutableStateOf(true) }
    FloatingActionButton(
        onClick = { expanded = !expanded },
    ) {
        Row(Modifier.padding(start = 16.dp, end = 16.dp)) {
            Icon(
                imageVector = Icons.Default.Favorite,
                contentDescription = "Favorite Icon",
                Modifier.align(Alignment.CenterVertically)
            )
            AnimatedVisibility(
                expanded,
                modifier = Modifier.align(Alignment.CenterVertically),
                enter = slideInHorizontally(
                    initialOffsetX = { 300 }, 
                    animSpec = tween(durationMillis = 2000)
                ),
                exit = slideOutVertically(
                    targetOffsetY = { 100 }, 
                    animSpec = tween(durationMillis = 2000)
                )
            ) {
                Text(text = "Like")
            }
        }
    }
}

Default

Enter

Enter + Exit

AnimatedVisbility Composable

cs.android.com

AnimatedVisibility in LazyColumn

AnimatedVisibility in multiple Composables

Animations in Jetpack Compose

  • Single Value Animations with animate*AsState() methods
  • The animateContentSize() Modifier
  • The AnimatedVisibility Composable
  • Single Value Animations with animate*AsState() methods
  • Repeated Animations with Animatable
  • Utilizing transitions to make solid animations

The animate*AsState() methods

The fire-and-forget functions to create single value based animations.

  • Previously, the animate() method. - DEPRECATED
  • The animatedColorAsState() for Colors
  • The animatedDpAsState() for DP values
  • And more...

The animate*AsState() methods - 1

@Composable
fun ScaleAndColorAnimation() {
    val enabled = remember { mutableStateOf(true) }
    
    val color: Color by animateColorAsState(
        if (enabled.value) Color.Blue else Colors.green)
        
    val height: Dp by animateDpAsState(if (enabled.value) 40.dp else 60.dp)
    val width: Dp by animateDpAsState(if (enabled.value) 150.dp else 300.dp)
    
    Button(
        onClick = { enabled.value = !enabled.value },
        colors = ButtonDefaults.buttonColors(backgroundColor = color),
        modifier = Modifier
            .padding(16.dp)
            .preferredHeight(height)
            .preferredWidth(width),
    ) {
        Text("Scale & Color")
    }
}

The animate*AsState() methods - 2

@Composable
fun GenderSelectAnimation() {
  val female = remember { mutableStateOf(true) }
  Row(horizontalArrangement = Arrangement.Center,
      modifier = Modifier.padding(8.dp).fillMaxWidth(),
      verticalAlignment = Alignment.CenterVertically
  ) {
      Image(
        painter = painterResource(R.drawable.male),
        contentDescription = "Male Image",
        contentScale = ContentScale.Crop,
        modifier = Modifier
            .preferredSize(animateDpAsState(if (female.value) 100.dp else 250.dp).value)
            .border(width = animateDpAsState(if (female.value) 0.dp else 4.dp).value,
                color = animateColorAsState(if (female.value) Color.Transparent else Color.Red).value)
            .padding(8.dp)
            .clickable { female.value = !female.value }
      )
      Image(
        painter = painterResource(R.drawable.female),
        contentDescription = "Female Image",
        contentScale = ContentScale.Crop,
        modifier = Modifier
        // ... 
        // Like previous image
      )
  }
}

Examples of animate*AsState() methods

github.com/Gurupreet/ComposeCookBook

Animations in Jetpack Compose

  • Repeated Animations with Animatable
  • The animateContentSize() Modifier
  • The AnimatedVisibility Composable
  • Single Value Animations with animate methods
  • Repeated Animations with Animatable
  • Utilizing transitions to make solid animations

Repeated Animations with Animatable

@Composable
fun HeartBeatDemo() {
    val animScale = remember { Animatable(initialValue = 1f) }
    val animColor = remember { Animatable(initialValue = Color.Red) }

    LaunchedEffect(animScale) {
        animScale.animateTo(
            targetValue = 2f,
            animationSpec = infiniteRepeatable(
                animation = tween(durationMillis = 300, delayMillis = 1000),
                repeatMode = RepeatMode.Reverse
            )
        )
    }

    LaunchedEffect(animColor) {
        animColor.animateTo(
            targetValue = Color.Blue,
            animationSpec = repeatable(iterations = 2,
                animation = tween(durationMillis = 300, delayMillis = 1000),
                repeatMode = RepeatMode.Reverse
            )
        )
    }

    Image(imageVector = Icons.Default.Favorite,
    	contentDescription = "Favourite Icon",
        modifier = Modifier.padding(10.dp).size((40*animScale.value).dp),
        colorFilter = ColorFilter.tint(animColor.value)
    )
}

Compose Canvas 101

A simple Spacer() to allow you to draw anything on it

@Composable
fun Canvas
(
   modifier: Modifier, 
   onDraw: DrawScope.() -> Unit
) = Spacer(modifier.drawBehind(onDraw))

Compose Canvas 101

DrawScope - Handles the drawing API

drawRect()

drawOval()

drawLine()

drawImage()

drawRoundRect()

drawCircle()

drawArc()

drawPath()

fun DrawScope.drawMyShape() { }

Compose Canvas 101

Custom View Example in Canvas

Canvas(modifier = Modifier.fillMaxSize()) {
    drawCircle(
        color = Color.Red,
        radius = 300f
    )

    drawCircle(
        color = Color.Green,
        radius = 200f
    )

    drawCircle(
        color = Color.Blue,
        radius = 100f
    )
}

Animatable on Canvas

@Composable
fun MovingSquare() {
    val animPosX = remember { Animatable(initialValue = 0f) }
    
    LaunchedEffect(animPosX) {
        animPosX.animateTo(
            targetValue = 500f,
            animationSpec = infiniteRepeatable(
                animation = tween(durationMillis = 1000)
            )
        )
    }

    Canvas(modifier = Modifier.preferredSize(100.dp), onDraw = {
        withTransform({
            translate(left = animPosX.value)
        }) {
            drawRect(color = Color.Red)
        }
    })
}

A simple moving square example

More Canvas Examples

Its all about mathematics & drawing

 github.com/wajahatkarim3/DinoCompose

github.com/alexjlockwood/bees-and-bombs-compose/

Animations in Jetpack Compose

  • Utilizing transitions to make solid animations
  • The animateContentSize() Modifier
  • The AnimatedVisibility Composable
  • Single Value Animations with animate methods
  • Repeated Animations with Animatable
  • Utilizing transitions to make solid animations

Transitions - 1

class DemoActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContent {
         DemoTheme {
            Scaffold(
               topBar = {
                  TopAppBar( /* App Bar */ )
               },
               floatingActionButton = {
                  ExplodingFabButton()
               },
               bodyContent = {
                  // Content Composables
              }
           }
        }
    }
}

Transitions - 2

enum class FabSizeState {
    NORMAL, EXPLODED
}

@Composable
fun ExplodingFabButton() {
   var fabSizeState by remember { mutableStateOf(FabSizeState.NORMAL) }
   
   val fabTransition: Transition<FabSizeState> = updateTransition(fabSizeState)
   
   val fabSize: Float by fabTransition.animateFloat() { state ->
        when (state) {
            FabSizeState.NORMAL -> 80f
            FabSizeState.EXPLODED -> 5000f
        }
    }

    val fabColor: Color by fabTransition.animateColor() { state ->
        when (state) {
            FabSizeState.NORMAL -> secondaryColor
            FabSizeState.EXPLODED -> primaryColor
        }
    }
}

Transitions - 3

@Composable
fun ExplodingFabButton() {
   var fabSizeState by remember { mutableStateOf(FabSizeState.NORMAL) }
   
   val fabTransition: Transition<FabSizeState> = updateTransition(fabSizeState)
   
   val fabSize: Float by fabTransition.animateFloat() { /* from previous slide */ }
   val fabColor: Color by fabTransition.animateColor() { /* from previous slide */ }
      
   FloatingActionButton(
        onClick = {
            fabSizeState = if (fabSizeState == FabSizeState.NORMAL)
                FabSizeState.EXPLODED
            else FabSizeState.NORMAL
        },
        modifier = Modifier.size(fabSize.dp),
        backgroundColor = fabColor
    ) {
        Icon(
            imageVector = Icons.Default.Add,
            contentDescription = "Add"
        )
    }
}

Transitions - 4

val fabSize: Float by fabTransition.animateFloat(
        transitionSpec = {
            when {
                FabSizeState.NORMAL isTransitioningTo FabSizeState.EXPLODED -> {
                    keyframes {
                        durationMillis = 1000
                        80f at 0
                        35f at 200
                        5000f at 1000
                    }
                }
                FabSizeState.EXPLODED isTransitioningTo FabSizeState.NORMAL -> {
                    tween(durationMillis = 1000, easing = FastOutSlowInEasing)
                }
                else -> snap()
            }
        }
    ) { state ->
        when (state) {
            FabSizeState.NORMAL -> 80f
            FabSizeState.EXPLODED -> 5000f
        }
    }

Transitions - 5

Transitions - Examples

https://joebirch.co/

https://www.raywenderlich.com/13282144-jetpack-compose-animations-tutorial-getting-started

https://github.com/wajahatkarim3/Flippable

https://joebirch.co/

Animation Inspector

Which animation API to use?

https://developer.android.com/jetpack/compose/animation

Resources

Alex Lockwood

https://github.com/alexjlockwood/bees-and-bombs-compose

https://github.com/alexjlockwood/android-2048-compose

Joe Birch

https://joebirch.co/exploring-jetpack-compose/

Gurupreet Singh

https://github.com/Gurupreet/ComposeCookBook

Leland Richardson

https://www.twitch.tv/intelligibabble

Official by Google

https://developer.android.com/courses/pathways/compose

Vinay Gaba

https://github.com/vinaygaba/Learn-Jetpack-Compose-By-Example

Thank You for Listening!

Code is available at

http://github.com/wajahatkarim3/ComposeAnimationsDemo

WajahatKarim

wajahatkarim.com/subscribe

wajahatkarim.com

Made with Slides.com