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
.