Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/flipcash/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ androidComponents {
}

dependencies {
implementation(libs.bundles.haze)
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
// KMP shared core — consumed transitively via :libs:encryption:base58 but declared
// explicitly so the app's own code can reference com.flipcash.shared.* directly.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flipcash.app.core.AppRoute
import dev.chrisbanes.haze.HazeState
import com.flipcash.app.core.navigation.NavBarButton
import com.flipcash.app.core.navigation.NavBarConfig
import com.flipcash.app.core.navigation.asNavBarTab
Expand Down Expand Up @@ -43,6 +44,7 @@ import com.getcode.theme.CodeTheme
internal fun AppNavigationBar(
navigator: CodeNavigator,
modifier: Modifier = Modifier,
hazeState: HazeState? = null,
) {
// Selection follows the base of the backstack (the tab "home"), so it stays correct while a
// sheet/modal sits on top and is right on launch. The top route only gates visibility.
Expand Down Expand Up @@ -78,6 +80,7 @@ internal fun AppNavigationBar(
// Tab bar semantics: swap the current screen (single backstack).
button.destinationRoute()?.let(navigator::replaceAll)
},
hazeState = hazeState,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.NavKey
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
import androidx.navigation3.scene.OverlayScene
import androidx.navigation3.scene.Scene
import androidx.navigation3.scene.SinglePaneSceneStrategy
Expand Down Expand Up @@ -130,12 +134,16 @@ internal fun NewAppContent(
deepLink: () -> DeepLink?,
onPendingAction: (DeeplinkAction) -> Unit = {},
) {
val hazeState = rememberHazeState()
CodeScaffold(
bottomBar = {
AppNavigationBar(navigator = codeNavigator)
AppNavigationBar(navigator = codeNavigator, hazeState = hazeState)
}
) { padding ->
CompositionLocalProvider(LocalTabBarPadding provides padding) {
// Mark the nav content as the haze source so the frosted tab bar blurs what scrolls
// beneath it.
Box(modifier = Modifier.hazeSource(hazeState)) {
AppNavHost(
navigator = codeNavigator,
resultStateRegistry = resultStateRegistry,
Expand Down Expand Up @@ -199,6 +207,7 @@ internal fun NewAppContent(
onPendingAction = onPendingAction,
),
)
}
}
}
}
1 change: 1 addition & 0 deletions apps/flipcash/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ android {
}

dependencies {
implementation(libs.bundles.haze)
testImplementation(kotlin("test"))
testImplementation(libs.bundles.unit.testing)
testImplementation(libs.robolectric)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
Expand All @@ -28,6 +29,7 @@ import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.border
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
Expand All @@ -48,6 +50,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
Expand All @@ -64,6 +67,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.zIndex
import com.flipcash.app.core.navigation.NavBarButton
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.HazeBlurStyle
import dev.chrisbanes.haze.blur.HazeColorEffect
import dev.chrisbanes.haze.blur.blurEffect
import dev.chrisbanes.haze.hazeEffect
import com.flipcash.app.core.navigation.NavBarConfig
import com.flipcash.app.theme.FlipcashThemeWrapper
import com.flipcash.core.R
Expand Down Expand Up @@ -127,6 +135,7 @@ fun NavigationBar(
state: NavigationBarState,
onButtonClick: (NavBarButton) -> Unit = {},
onOrderChanged: ((List<NavBarButton>) -> Unit)? = null,
hazeState: HazeState? = null,
) {
val reorderState = onOrderChanged?.let {
rememberLongPressDraggableState(
Expand All @@ -142,7 +151,7 @@ fun NavigationBar(
}

if (state.isNewUi) {
NavigationBarV2(state, onButtonClick, modifier, reorderState, onOrderChanged)
NavigationBarV2(state, onButtonClick, modifier, reorderState, onOrderChanged, hazeState)
return
}

Expand Down Expand Up @@ -237,6 +246,7 @@ private fun NavigationBarV2(
modifier: Modifier = Modifier,
reorderState: LongPressDraggableState? = null,
onOrderChanged: ((List<NavBarButton>) -> Unit)? = null,
hazeState: HazeState? = null,
) {
val order = state.config.order
if (order.isEmpty()) return
Expand All @@ -247,11 +257,40 @@ private fun NavigationBarV2(
.takeIf { it >= 0 && it <= order.lastIndex }
?: state.config.order.indexOf(NavBarButton.Wallet)

// Frost the pill over whatever content scrolls beneath it, iOS "liquid glass" style: a wide blur
// plus a strong tint toward the BACKGROUND colour (not black) at high alpha. Over empty/dark
// content the pill just reads as the background (a subtle glass, not a black blob); over the
// vibrant cards the high alpha mutes their colour toward that same neutral dark. A faint bright
// rim gives the glass edge. Haze can only blur Compose-layer pixels, so on the scanner tab (a
// camera SurfaceView) fall back to the opaque pill. `clip` must precede `hazeEffect` to bound the
// blur to the pill shape, not its bounding box.
// Tint toward a grey lifted off the (near-black) background so the pill reads as a light frosted
// glass sitting ABOVE the dark content, not the background tone itself.
val glassTint = lerp(CodeTheme.colors.background, Color.White, 0.18f)
val liquidGlass = HazeBlurStyle(
blurRadius = 32.dp,
backgroundColor = CodeTheme.colors.background,
colorEffect = HazeColorEffect.tint(glassTint.copy(alpha = 0.72f)),
)
// Same clip + rim on every tab; only the fill differs. Haze frosts the content beneath — including
// the scanner's live camera, since its PreviewView runs in COMPATIBLE mode (a TextureView drawn in
// the Compose layer, not a SurfaceView hole). Fall back to a near-opaque fill of the same
// lifted-grey tint only when no HazeState is supplied.
val pillFill = if (hazeState != null) {
Modifier.hazeEffect(hazeState) { blurEffect { style = liquidGlass } }
} else {
Modifier.background(glassTint.copy(alpha = 0.9f), CircleShape)
}
val pillBackground = Modifier
.clip(CircleShape)
.then(pillFill)
.border(CodeTheme.dimens.border, Color.White.copy(alpha = 0.08f), CircleShape)

BoxWithConstraints(
modifier = Modifier
.fillMaxWidth()
.then(modifier)
.background(Color.Black.copy(alpha = 0.62f), CircleShape)
.then(pillBackground)
.padding(CodeTheme.dimens.grid.x1),
) {
val itemWidth = maxWidth / order.size
Expand Down Expand Up @@ -282,7 +321,12 @@ private fun NavigationBarV2(
.weight(1f)
.height(itemHeight)
.clip(CircleShape)
.clickable { onButtonClick(button) },
// No ripple — the sliding selection pill is the touch feedback; a Material
// ripple here just competes with it.
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
) { onButtonClick(button) },
contentAlignment = Alignment.Center,
) {
Image(
Expand Down
Loading