Create a Stunning Animated Navigation Bar in Jetpack Compose

Libraries|Apr 22, 2023|Last edited: Apr 22, 2023
  • Jetpack Compose UI
  • Library
  • Navigation
  • App
  • Create a Stunning Animated Navigation Bar in Jetpack Compose
    type
    Post
    status
    Published
    date
    Apr 22, 2023
    slug
    implementing-animated-navigation-bar-android-jetpack-compose
    summary
    Elevate your Android app's user experience by creating an animated navigation bar with Jetpack Compose using the AnimatedNavigationBar library by exyte
    tags
    Jetpack Compose UI
    Library
    Navigation
    App
    category
    Libraries
    icon
    password
    Navigation bars are an essential part of almost every mobile application. They help users navigate between different sections of the app with ease. With this Jetpack Compose Library by exyte, creating a custom animated navigation bar for your Android app has become easier and prettier than ever. In this blog, we will guide you through the process of implementing an animated navigation bar in Jetpack Compose and provide you with a step-by-step explanation of the code.
     

    About AnimatedNavigationBar

     
    AnimatedNavigationBar is a Jetpack Compose library that provides a variety of preset animations for navigation bars. It is designed to make it simple to implement engaging and interactive navigation bars in Android applications.
    The library includes different animation types, such as Straight, Parabolic, and Teleport, to create a captivating user experience.
     

    Sample App

     
    To get a better understanding of how to use AnimatedNavigationBar and create custom buttons, you can explore the sample project provided by the library.
     
    This project showcases the usage of different animations, built-in animatable tab buttons, and custom buttons in a demo app.
     
     

    Getting Started

     
    dependencies { implementation("com.exyte:animated-navigation-bar:1.0.0") }
     
    Next, we will dive into the main component of the library: the AnimatedNavigationBar composable function. This function is responsible for creating the navigation bar with a moving ball and indent to indicate the selected item.

    Creating the AnimatedNavigationBar

     
    To create the AnimatedNavigationBar, you will need to define a few key parameters. First, you need to remember an Int to store the current selection:
     
    var selectedIndex by remember { mutableStateOf(0) }
    Then, pass your buttons to the AnimatedNavigationBar:
     
    AnimatedNavigationBar(selectedIndex = selectedIndex) { Button1() Button2() Button3() }
     

    Built-in Animatable Tab Buttons

     
    The AnimatedNavigationBar library includes two built-in button types, DropletButton and WiggleButton, which can be used out-of-the-box.
     
    Moreover, the Example project provides a super custom ColorButton type. Feel free to use these buttons in your projects or create your own custom buttons.
     

    Implementing Different Animations

     
    To implement different animations for the balls of the navigation bar, you can use the BallAnimation interface and its implementing classes: Straight, Parabolic, and Teleport. Each class defines a unique animation for the ball, allowing you to customize the behavior of the navigation bar.
     

    Source Code Explanation: AnimatedNavigationBar Library

     
    @Composable fun AnimatedNavigationBar( modifier: Modifier = Modifier, selectedIndex: Int, barColor: Color = Color.White, ballColor: Color = Color.Black, cornerRadius: ShapeCornerRadius = shapeCornerRadius(0f), ballAnimation: BallAnimation = Parabolic(tween(300)), indentAnimation: IndentAnimation = Height(tween(300)), content: @Composable () -> Unit, ) { ... }
     
    The provided code is related to the implementation of an animated navigation bar in Android. The AnimatedNavigationBar composable function is the main component responsible for creating the navigation bar with a moving ball and indent to indicate the selected item.
     
    @Stable class Straight( private val animationSpec: AnimationSpec<Offset> ) : BallAnimation { ... }
     
    @Stable class Parabolic( private val animationSpec: FiniteAnimationSpec<Float> = spring(), private val maxHeight: Dp = 100.dp, ) : BallAnimation { ... }
     
    @Stable class Teleport( private val animationSpec: AnimationSpec<Float> = spring() ) : BallAnimation { ... }
     
    The com.exyte.animatednavbar.animation.balltrajectory package contains classes that define different types of animations for the balls of the navigation bar. The BallAnimation interface defines the basic structure of an animation. The BallAnimInfo data class is used to hold the parameters of the ball animation. The Straight, Parabolic, and Teleport classes implement different types of animations for the balls.
     
    @Stable fun Float.toDp(density: Density): Dp = with(density) { this@toDp.toDp() } @Stable fun Dp.toPxf(density: Density): Float = with(density) { this@toPxf.toPx() } @Stable @Composable fun Dp.toPxf(): Float = toPxf(LocalDensity.current) @Stable @Composable fun Float.toDp() = this.toDp(LocalDensity.current)
     
    The file utils.kt contains utility functions for converting between different units of measurement in Compose.
    The toDp function converts a float value to a Dp value using the provided density. The toPxf function converts a Dp value to a float value in pixels, again using the provided density.
    The toPxf and toDp functions without the density parameter are Composable functions that use the LocalDensity to get the current density.
     
    fun lerp(start: Float, stop: Float, fraction: Float) = (start * (1 - fraction) + stop * fraction)|
     
    The lerp function in this code is a utility function that performs a linear interpolation between two values based on a given fraction. It takes in three parameters:
    The function returns the interpolated value.
    This function is used in the Height class in the animatednavbar.animation.indentshape package to calculate the yIndent value for the indent shape animation.
     
    @OptIn(ExperimentalFoundationApi::class) @Composable fun DropletButton( modifier: Modifier = Modifier, isSelected: Boolean, onClick: () -> Unit, icon: Int, contentDescription: String? = null, iconColor: Color = Color.LightGray, dropletColor: Color = Color.Red, size: Dp = 20.dp, animationSpec: AnimationSpec<Float> = remember { tween(300) } ) { Box( modifier = modifier.noRippleClickable { onClick() } ) { val density = LocalDensity.current val dropletButtonParams = animateDropletButtonAsState( isSelected = isSelected, animationSpec = animationSpec, size = size.toPxf(density) ) ... } }
     
    @Composable fun WiggleButton( modifier: Modifier = Modifier, isSelected: Boolean, onClick: () -> Unit, @DrawableRes icon: Int, @DrawableRes backgroundIcon: Int, contentDescription: String? = null, backgroundIconColor: Color = Color.White, wiggleColor: Color = Color.Blue, outlineColor: Color = Color.LightGray, iconSize: Dp = 25.dp, enterExitAnimationSpec: AnimationSpec<Float> = spring(), wiggleAnimationSpec: AnimationSpec<Float> = spring(dampingRatio = 0.6f, stiffness = 35f) ) { Box( modifier = modifier .noRippleClickable { onClick() } ) { ... } }
     
    The DropletButton is a button with an icon and a droplet-shaped background. It supports animations and selection states. The animateDropletButtonAsState() function uses produceState to animate the button's background, scale, and radius properties based on the button's selection state.
    The WiggleButton is a button with an icon and a wiggle part background. It also supports animations and selection states. The animateWiggleButtonAsState() function uses produceState to animate the button's scale, alpha, and radius properties based on the button's selection state.
    Both buttons use similar animation techniques, such as animateFloatAsState, tween, and spring. The WiggleButton also uses a set of interpolators to calculate the scale, alpha, and radius properties.
     

    Source Code Explanation: AnimatedNavigationBar Sample App

     
    class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) setContent { val systemUiController: SystemUiController = rememberSystemUiController() SideEffect { systemUiController.isStatusBarVisible = false systemUiController.isNavigationBarVisible = false } Box( modifier = Modifier .fillMaxSize() .background(ElectricViolet) ) { Column( modifier = Modifier.align(Alignment.BottomCenter) ) { ColorButtonNavBar() DropletButtonNavBar() WiggleButtonNavBar() } } } } } @Composable fun ColorButtonNavBar() { var selectedItem by remember { mutableStateOf(0) } var prevSelectedIndex by remember { mutableStateOf(0) } AnimatedNavigationBar( modifier = Modifier .padding(horizontal = 8.dp, vertical = 60.dp) .height(85.dp), selectedIndex = selectedItem, ballColor = Color.White, cornerRadius = shapeCornerRadius(25.dp), ballAnimation = Straight( spring(dampingRatio = 0.6f, stiffness = Spring.StiffnessVeryLow) ), indentAnimation = StraightIndent( indentWidth = 56.dp, indentHeight = 15.dp, animationSpec = tween(1000) ) ) { colorButtons.forEachIndexed { index, it -> ColorButton( modifier = Modifier.fillMaxSize(), prevSelectedIndex = prevSelectedIndex, selectedIndex = selectedItem, index = index, onClick = { prevSelectedIndex = selectedItem selectedItem = index }, icon = it.icon, contentDescription = stringResource(id = it.description), animationType = it.animationType, background = it.animationType.background ) } } } @Composable fun DropletButtonNavBar() { var selectedItem by remember { mutableStateOf(0) } AnimatedNavigationBar( modifier = Modifier .padding(horizontal = 8.dp, vertical = 40.dp) .height(85.dp), selectedIndex = selectedItem, ballColor = Color.White, cornerRadius = shapeCornerRadius(25.dp), ballAnimation = Parabolic(tween(Duration, easing = LinearOutSlowInEasing)), indentAnimation = Height( indentWidth = 56.dp, indentHeight = 15.dp, animationSpec = tween( DoubleDuration, easing = { OvershootInterpolator().getInterpolation(it) }) ) ) { dropletButtons.forEachIndexed { index, it -> DropletButton( modifier = Modifier.fillMaxSize(), isSelected = selectedItem == index, onClick = { selectedItem = index }, icon = it.icon, dropletColor = Purple, animationSpec = tween(durationMillis = Duration, easing = LinearEasing) ) } } } @Composable fun WiggleButtonNavBar() { var selectedItem by remember { mutableStateOf(0) } AnimatedNavigationBar( modifier = Modifier .padding(horizontal = 8.dp, vertical = 40.dp) .height(85.dp), selectedIndex = selectedItem, ballColor = Color.White, cornerRadius = shapeCornerRadius(25.dp), ballAnimation = Teleport(tween(Duration, easing = LinearEasing)), indentAnimation = Height( indentWidth = 56.dp, indentHeight = 15.dp, animationSpec = tween( DoubleDuration, easing = { OvershootInterpolator().getInterpolation(it) }) ) ) { wiggleButtonItems.forEachIndexed { index, it -> WiggleButton( modifier = Modifier.fillMaxSize(), isSelected = selectedItem == index, onClick = { selectedItem = index }, icon = it.icon, backgroundIcon = it.backgroundIcon, wiggleColor = LightPurple, outlineColor = RoyalPurple, contentDescription = stringResource(id = it.description), enterExitAnimationSpec = tween(durationMillis = Duration, easing = LinearEasing), wiggleAnimationSpec = spring(dampingRatio = .45f, stiffness = 35f) ) } } } const val Duration = 500 const val DoubleDuration = 1000
     
    The code is the implementation of a navigation bar in Android using Jetpack Compose. It defines three navigation bars with different animation types: color buttons, droplet buttons, and wiggle buttons.
     
    The main method is AnimatedNavigationBar, which is responsible for drawing the navigation bar with the selected animation. Each navigation bar is defined using a list of buttons, where each button is created using a specific composable function, such as ColorButton or DropletButton.
     
    The animation for each button is defined using an animation object, such as Straight, Parabolic, or Teleport. Each animation object defines how the button should move when it is selected or unselected.
     
    The code also sets the status bar and navigation bar to be invisible to provide a full-screen experience.
     
    @Stable data class WiggleButtonItem( @DrawableRes val backgroundIcon: Int, @DrawableRes val icon: Int, var isSelected: Boolean, @StringRes val description: Int, val animationType: ColorButtonAnimation = BellColorButton( tween(500), background = ButtonBackground(R.drawable.plus) ), ) @Stable data class Item( @DrawableRes val icon: Int, var isSelected: Boolean, @StringRes val description: Int, val animationType: ColorButtonAnimation = BellColorButton( tween(500), background = ButtonBackground(R.drawable.plus) ), ) val wiggleButtonItems = listOf( WiggleButtonItem( icon = R.drawable.outline_favorite, backgroundIcon = R.drawable.favorite, isSelected = false, description = R.string.Heart, ), ... ) val dropletButtons = listOf( Item( icon = R.drawable.home, isSelected = false, description = R.string.Home ), ... ) val colorButtons = listOf( Item( icon = R.drawable.outline_home, isSelected = true, description = R.string.Home, animationType = BellColorButton( animationSpec = spring(dampingRatio = 0.7f, stiffness = 20f), background = ButtonBackground( icon = R.drawable.circle_background, offset = DpOffset(2.5.dp, 3.dp) ), ) ), ... )
     
    This code defines the data structures for the items in the navigation bar. There are three lists of items, each containing different types of items with different animations: wiggleButtonItems, dropletButtons, and colorButtons. Each item has an icon, a description, and an animation type. WiggleButtonItem additionally has a background icon. The animation types are defined using classes that implement the ColorButtonAnimation interface and define the animation spec and background icon offset.
     
    val ElectricViolet = Color(0xFF7D26FE) val Purple = Color(0xFF742DF6) val LightPurple = Color(0xFFBCA1E7) val RoyalPurple = Color(0xFF64419F) val LightGrey = Color(0xFFB1B1B1)
     
    This code defines a simple color palette for the app, with five colors: ElectricViolet, Purple, LightPurple, RoyalPurple, and LightGrey.

    Reference

     
     
    Building a Compose Multiplatform Image Gallery App for Android and iOSKDoctor: Your Ultimate Troubleshooting Companion for the Kotlin Multiplatform Ecosystem