refactor: animate bottom nav icons

This commit is contained in:
Zane Schepke
2025-10-03 00:09:34 -04:00
parent 9ea608beb5
commit 9735f59abb
4 changed files with 73 additions and 13 deletions
+2 -1
View File
@@ -69,7 +69,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
signingConfig = signingConfigs.getByName(Constants.RELEASE)
signingConfig = signingConfigs.getByName("debug")
resValue("string", "provider", "\"${Constants.APP_NAME}.provider\"")
}
@@ -161,6 +161,7 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.storage)
implementation("androidx.compose.animation:animation-graphics:1.9.2")
implementation("androidx.navigation3:navigation3-runtime:1.0.0-alpha10")
implementation("androidx.navigation3:navigation3-ui:1.0.0-alpha10")
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
@@ -0,0 +1,44 @@
package com.zaneschepke.wireguardautotunnel.ui.common.animations
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.vector.ImageVector
@Composable
fun AnimatedFloatIcon(
inactiveIcon: ImageVector,
activeIcon: ImageVector,
isSelected: Boolean,
modifier: Modifier,
) {
val transition = updateTransition(isSelected, label = null)
val scale by
transition.animateFloat(transitionSpec = { tween(250, easing = FastOutSlowInEasing) }) {
selected ->
if (selected) 1.2f else 1f
}
val rotation by
animateFloatAsState(targetValue = 0f, animationSpec = tween(400, easing = LinearEasing))
Crossfade(targetState = isSelected, animationSpec = tween(250, easing = FastOutSlowInEasing)) {
selected ->
Icon(
imageVector = if (selected) activeIcon else inactiveIcon,
contentDescription = null,
modifier = modifier.scale(scale).rotate(rotation),
)
}
}
@@ -8,6 +8,7 @@ import androidx.compose.material.icons.filled.QuestionMark
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.QuestionMark
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.Home
@@ -80,13 +81,20 @@ sealed class Route : NavKey {
enum class Tab(
val startRoute: Route,
val titleRes: Int,
val icon: androidx.compose.ui.graphics.vector.ImageVector,
val inactiveIcon: androidx.compose.ui.graphics.vector.ImageVector,
val activeIcon: androidx.compose.ui.graphics.vector.ImageVector,
val index: Int,
) {
TUNNELS(Route.Tunnels, R.string.tunnels, Icons.Rounded.Home, 0),
AUTOTUNNEL(Route.AutoTunnel, R.string.auto_tunnel, Icons.Rounded.Bolt, 1),
SETTINGS(Route.Settings, R.string.settings, Icons.Rounded.Settings, 2),
SUPPORT(Route.Support, R.string.support, Icons.Rounded.QuestionMark, 3);
TUNNELS(Route.Tunnels, R.string.tunnels, Icons.Outlined.Home, Icons.Filled.Home, 0),
AUTOTUNNEL(Route.AutoTunnel, R.string.auto_tunnel, Icons.Outlined.Bolt, Icons.Filled.Bolt, 1),
SETTINGS(Route.Settings, R.string.settings, Icons.Outlined.Settings, Icons.Filled.Settings, 2),
SUPPORT(
Route.Support,
R.string.support,
Icons.Outlined.QuestionMark,
Icons.Filled.QuestionMark,
3,
);
companion object {
fun fromRoute(route: Route): Tab =
@@ -1,5 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.navigation.components
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@@ -10,8 +12,8 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.common.animations.AnimatedFloatIcon
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
@@ -28,6 +30,7 @@ fun BottomNavbar(isAutoTunnelActive: Boolean, currentTab: Tab, onTabSelected: (T
val interactionSource = remember { MutableInteractionSource() }
val isSelected = currentTab == tab
val hasBadge = tab == Tab.AUTOTUNNEL && isAutoTunnelActive
IconButton(
onClick = { onTabSelected(tab) },
colors =
@@ -54,15 +57,19 @@ fun BottomNavbar(isAutoTunnelActive: Boolean, currentTab: Tab, onTabSelected: (T
)
}
) {
Icon(
imageVector = tab.icon,
contentDescription = stringResource(tab.titleRes),
AnimatedFloatIcon(
activeIcon = tab.activeIcon,
inactiveIcon = tab.inactiveIcon,
isSelected = isSelected,
modifier = Modifier.size(24.dp),
)
}
} else {
Icon(
imageVector = tab.icon,
contentDescription = stringResource(tab.titleRes),
AnimatedFloatIcon(
activeIcon = tab.activeIcon,
inactiveIcon = tab.inactiveIcon,
isSelected = isSelected,
modifier = Modifier.size(24.dp),
)
}
}