Jetpack Compose for

Games & Animations

Wajahat Karim

wajahatkarim.com

WajahatKarim

🔥 Google Dev Expert (GDE) in Android .
📱 Android Dev. 💻 Open Source Contributor .
  📝 Technical Writer . 🎤 Public Speaker

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 animate()
  • Repeated Animations with AnimatedValue
  • Utilizing transitions to make solid animations

Animations in Jetpack Compose

  • The animateContentSize() Modifier

  • The AnimatedVisibility Composable
  • Single Value Animations with animate()
  • Repeated Animations with AnimatedValue
  • 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(0xFFfffbd0) else Color(0xFFe3ffd9))
      .animateContentSize(
          animSpec = tween(500, easing = LinearEasing),
          endListener = { startSize, endSize ->  
             Log.d("droidcon", "$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()
  • Repeated Animations with AnimatedValue
  • 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(
                asset = Icons.Default.Favorite,
                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(
                asset = Icons.Default.Favorite,
                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()
  • The animateContentSize() Modifier
  • The AnimatedVisibility Composable
  • Single Value Animations with animate()
  • Repeated Animations with AnimatedValue
  • Utilizing transitions to make solid animations

The animate()

A simple method for animations between current and target state.

Supports Int, Dp, Color, Rect and many more types.

@Composable
fun ScaleAndColorAnimation() {
    val enabled = remember { mutableStateOf(true) }
    val color = if (enabled.value) MaterialTheme.colors.primary 
    		else MaterialTheme.colors.secondary
            
    val height = if (enabled.value) 40.dp else 60.dp
    val width = if (enabled.value) 150.dp else 300.dp
    
    Button(
        onClick = { enabled.value = !enabled.value },
        backgroundColor = animate(color),
        modifier = Modifier
            .padding(16.dp)
            .preferredHeight(animate(height))
            .preferredWidth(animate(width)),
    ) {
        Text("Scale & Color")
    }
}

The animate()

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

Examples of animate()

github.com/Gurupreet/ComposeCookBook

Animations in Jetpack Compose

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

Repeated Animations with AnimatedValue

@Composable
fun HeartBeatDemo() {
    val animScale = animatedFloat(initVal = 1f)
    val animColor = animatedColor(initVal = Color.Red)

    onActive {
        animScale.animateTo(
            targetValue = 1.3f,
            anim = repeatable(
                iterations = AnimationConstants.Infinite,
                animation = tween(durationMillis = 300, 
                	easing = FastOutLinearInEasing, delayMillis = 1000)
            )
        )

        animColor.animateTo(
            targetValue = Color.Blue,
            anim = repeatable(
                iterations = AnimationConstants.Infinite,
                animation = tween(durationMillis = 300, 
                	easing = LinearEasing, delayMillis = 1000)
            )
        )
    }

    Image(asset = Icons.Default.Favorite,
        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
    )
}

AnimatedValue on Canvas

@Composable
fun MovingSquare() {
    val animPosX = animatedFloat(initVal = 0f)
    onActive {
        animPosX.animateTo(
            targetValue = 500f,
            anim = repeatable(
                iterations = AnimationConstants.Infinite,
                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()
  • Repeated Animations with AnimatedValue
  • Utilizing transitions to make solid animations

Transitions - 1

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

Transitions - 2

enum class FabSizeState {
    NORMAL, EXPLODED
}

val fabSizeKey = FloatPropKey()

@Composable
fun ExplodingFabButton() {
   val fabSizeState = remember { mutableStateOf(FabSizeState.NORMAL) }
   
   val fabTransitionDef = transitionDefinition<FabSizeState> {
      // What happens when Normal
      state(FabSizeState.NORMAL) {
         this[fabSizeKey] = 80f
      }
      
      // What happens when Exploded
      state(FabSizeState.EXPLODED) {
         this[fabSizeKey] = 5000f
      }
   }
}

Transitions - 3

@Composable
fun ExplodingFabButton() {
   val fabSizeState = remember { mutableStateOf(FabSizeState.NORMAL) }
   
   val fabTransitionDef = transitionDefinition<FabSizeState> {
      /* ---- States from Previous Slide ---- */
      
      // Transition from Normal to Exploded
      transition(FabSizeState.NORMAL to FabSizeState.EXPLODED) {
         fabSizeKey using keyframes {
            durationMillis = 1000
            80f at 0
            35f at 200
            5000f at 1000
         }
      }
      
      // Transition from Exploded to Normal
      transition(FabSizeState.EXPLODED to FabSizeState.NORMAL) {
         fabSizeKey using tween(durationMillis = 1000, 
            easing = FastOutSlowInEasing)
      }
   }
}

Transitions - 4

@Composable
fun ExplodingFabButton() {
   val fabSizeState = remember { mutableStateOf(FabSizeState.NORMAL) }
   
   val fabTransitionDef = transitionDefinition<FabSizeState> {
      /* ---- States from Previous Slide ---- */
      
      /* ---- Transitions from Previous Slide ---- */
   }
   
   val transitionState = transition(
      definition = fabTransitionDef,
      initState = fabSizeState.value,
      toState = if (fabSizeState.value == FabSizeState.NORMAL) 
      				FabSizeState.EXPLODED 
      			else FabSizeState.NORMAL
   )
   
   FabButton(fabSizeState = fabSizeState, transitionState = transitionState)
}

Transitions - 5

@Composable
fun ExplodingFabButton() {
   /* From Previous Slides */
}

@Composable
fun FabButton(fabSizeState: MutableState<FabSizeState>, 
   transitionState: TransitionState
) {
   FloatingActionButton(
      onClick = { },
      modifier = Modifier.size(transitionState[fabSizeKey].dp)
   ) {
      Icon(asset = Icons.Default.Add)
   }
}

Transitions - Examples

https://joebirch.co/

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

Animation Inspector

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

https://github.com/wajahatkarim3/droidcon2020

WajahatKarim

wajahatkarim.com/subscribe

wajahatkarim.com

Jetpack Compose for Games & Animations - Alpha6

By Wajahat Karim

Jetpack Compose for Games & Animations - Alpha6

In this talk, we will see how to develop various animations in Jetpack Compose

  • 3,079