From 2d0bf4cb1c502fafeb898daf17010b006d84c27a Mon Sep 17 00:00:00 2001 From: jonasgaudian <43753916+jonasgaudian@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:13:08 +0100 Subject: [PATCH] implement glassmorphism design across UI components --- app/build.gradle.kts | 1 + .../translator/view/composable/AppFabMenu.kt | 18 +--- .../view/composable/AppTabLayout.kt | 8 +- .../view/composable/AppTopAppBar.kt | 24 ++++-- .../view/composable/BottomNavigationBar.kt | 22 ++--- .../view/composable/ComponentLibrary.kt | 86 +++++++------------ gradle/libs.versions.toml | 2 + 7 files changed, 71 insertions(+), 90 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 38997d2..2d3f6e6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -130,6 +130,7 @@ dependencies { implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime implementation(libs.androidx.room.ktx) implementation(libs.core.ktx) + implementation(libs.androidx.compose.foundation.layout) ksp(libs.room.compiler) // CHANGED: Use ksp instead of implementation // Networking diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/AppFabMenu.kt b/app/src/main/java/eu/gaudian/translator/view/composable/AppFabMenu.kt index fd5575d..72a1872 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/AppFabMenu.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/AppFabMenu.kt @@ -35,7 +35,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate -import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource @@ -159,14 +159,14 @@ private fun MenuItem( ) { Surface( modifier = Modifier - .shadow(elevation = 8.dp, shape = ComponentDefaults.DefaultShape) + .glassmorphic(shape = RoundedCornerShape(16.dp), alpha = 0.4f) .clickable( onClick = onClick, interactionSource = remember { MutableInteractionSource() }, indication = null ), shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.surfaceContainer + color = Color.Transparent // Allow glassmorphic modifier to handle color ) { Row( modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), @@ -196,16 +196,4 @@ private fun MenuItem( ) } } -} - -@Preview -@Composable -fun MenuItemPreview() { - @Suppress("HardCodedStringLiteral") - MenuItem( - text = "Menu Item", - imageVector = AppIcons.Add, - painter = null, - onClick = {} - ) } \ No newline at end of file diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/AppTabLayout.kt b/app/src/main/java/eu/gaudian/translator/view/composable/AppTabLayout.kt index 5c78241..0a0078a 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/AppTabLayout.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/AppTabLayout.kt @@ -69,10 +69,8 @@ fun AppTabLayout( .fillMaxWidth() .padding(vertical = 8.dp, horizontal = 8.dp) .height(56.dp) - .background( - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), - shape = ComponentDefaults.CardShape - ) + // Replace background with glassmorphic extension + .glassmorphic(shape = ComponentDefaults.CardShape, alpha = 0.3f) ) { val tabWidth = maxWidth / tabs.size @@ -89,7 +87,7 @@ fun AppTabLayout( .fillMaxHeight() .padding(4.dp) .background( - color = MaterialTheme.colorScheme.primary, + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f), // Glassy indicator shape = RoundedCornerShape(12.dp) ) ) diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/AppTopAppBar.kt b/app/src/main/java/eu/gaudian/translator/view/composable/AppTopAppBar.kt index 1da881e..d2a5ccb 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/AppTopAppBar.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/AppTopAppBar.kt @@ -25,6 +25,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -41,16 +43,23 @@ fun AppTopAppBar( onNavigateBack: (() -> Unit)? = null, navigationIcon: @Composable (() -> Unit)? = null, actions: @Composable RowScope.() -> Unit = {}, - colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(), + colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent, + scrolledContainerColor = Color.Transparent + ), hintContent: Hint? = null ) { val sheetState = rememberModalBottomSheetState() var showBottomSheet by remember { mutableStateOf(false) } - TopAppBar( - modifier = modifier.height(56.dp), - windowInsets = WindowInsets(0.dp), - colors = colors, + Surface( + modifier = modifier.glassmorphic(shape = RectangleShape, alpha = 0.2f), + color = Color.Transparent + ) { + TopAppBar( + modifier = Modifier.height(56.dp), + windowInsets = WindowInsets(0.dp), + colors = colors, title = { Box( modifier = Modifier.fillMaxHeight(), @@ -102,8 +111,9 @@ fun AppTopAppBar( // No navigation icon } }, - actions = actions - ) + actions = actions + ) + } if (showBottomSheet) { HintBottomSheet( diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt b/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt index 373271f..93416ff 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar @@ -28,6 +29,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalDensity @@ -100,24 +102,25 @@ fun BottomNavigationBar( targetOffsetY = { it } ) ) { - val baseHeight = if (showLabels) 80.dp else 56.dp val density = LocalDensity.current val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() } val height = baseHeight + navBarDp NavigationBar( - modifier = modifier.height(height), - containerColor = MaterialTheme.colorScheme.surface, // Cleaner look than surfaceVariant - tonalElevation = 8.dp, // Slight elevation for depth + modifier = modifier + .height(height) + // Apply glassmorphism on the top corners + .glassmorphic(shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), alpha = 0.35f), + containerColor = Color.Transparent, // Let the glass shine through + tonalElevation = 0.dp, ) { screens.forEach { screen -> val isSelected = screen == selectedItem val title = stringResource(id = screen.title) - // 1. Spring Animation for the Icon Scale val scale by animateFloatAsState( - targetValue = if (isSelected) 1.2f else 1.0f, // Subtle pop effect + targetValue = if (isSelected) 1.2f else 1.0f, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow @@ -129,7 +132,7 @@ fun BottomNavigationBar( selected = isSelected, onClick = { if (!isSelected) { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) // 2. Tactile feedback + haptic.performHapticFeedback(HapticFeedbackType.LongPress) onItemSelected(screen) } }, @@ -145,17 +148,16 @@ fun BottomNavigationBar( } } else null, icon = { - // 3. Crossfade between Outlined and Filled icons Crossfade(targetState = isSelected, label = "iconFade") { selected -> Icon( imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon, contentDescription = title, - modifier = Modifier.scale(scale) // Apply the spring scale + modifier = Modifier.scale(scale) ) } }, colors = NavigationBarItemDefaults.colors( - indicatorColor = MaterialTheme.colorScheme.primaryContainer, + indicatorColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f), // Glassy indicator selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer, selectedTextColor = MaterialTheme.colorScheme.primary, unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/ComponentLibrary.kt b/app/src/main/java/eu/gaudian/translator/view/composable/ComponentLibrary.kt index 3fed94a..61576c5 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/ComponentLibrary.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/ComponentLibrary.kt @@ -5,6 +5,8 @@ package eu.gaudian.translator.view.composable import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -43,6 +45,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.composed import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.shadow @@ -55,49 +58,51 @@ import androidx.compose.ui.unit.dp import eu.gaudian.translator.R import eu.gaudian.translator.ui.theme.ThemePreviews import eu.gaudian.translator.ui.theme.semanticColors -import eu.gaudian.translator.view.composable.ComponentDefaults.DefaultElevation object ComponentDefaults { - // Sizing val DefaultButtonHeight = 48.dp val CardPadding = 8.dp - - // Elevation val DefaultElevation = 0.dp val NoElevation = 0.dp - - // Borders val DefaultBorderWidth = 1.dp - - // Shapes val DefaultCornerRadius = 16.dp - val CardClipRadius = 8.dp + val CardClipRadius = 16.dp // Increased slightly for softer glass look val NoRounding = 0.dp val DefaultShape = RoundedCornerShape(DefaultCornerRadius) val CardClipShape = RoundedCornerShape(CardClipRadius) val CardShape = RoundedCornerShape(DefaultCornerRadius) val NoShape = RoundedCornerShape(NoRounding) - // Opacity Levels const val ALPHA_HIGH = 0.6f - const val ALPHA_MEDIUM = 0.5f - const val ALPHA_LOW = 0.3f + const val ALPHA_MEDIUM = 0.4f + const val ALPHA_LOW = 0.2f // Adjusted for glass } - - /** - * A styled card container for displaying content with a consistent floating look. - * - * @param modifier The modifier to be applied to the card. - * @param content The content to be displayed inside the card. + * Standard Glassmorphism Modifier */ +fun Modifier.glassmorphic( + shape: Shape = ComponentDefaults.DefaultShape, + alpha: Float = ComponentDefaults.ALPHA_LOW, + borderAlpha: Float = 0.15f +): Modifier = composed { + this + .shadow(elevation = 8.dp, shape = shape, spotColor = Color.Black.copy(alpha = 0.05f)) + .clip(shape) + .background(MaterialTheme.colorScheme.surface.copy(alpha = alpha)) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = borderAlpha), + shape = shape + ) +} + @Composable fun AppCard( modifier: Modifier = Modifier, title: String? = null, - icon: ImageVector? = null, // New optional icon parameter + icon: ImageVector? = null, text: String? = null, expandable: Boolean = false, initiallyExpanded: Boolean = false, @@ -110,25 +115,17 @@ fun AppCard( label = "Chevron Rotation" ) - // Check if we need to render the header row - // Updated to include icon in the check val hasHeader = title != null || text != null || expandable || icon != null Surface( modifier = modifier .fillMaxWidth() - .shadow( - DefaultElevation, - shape = ComponentDefaults.CardShape - ) - .clip(ComponentDefaults.CardClipShape) - // Animate height changes when expanding/collapsing + .glassmorphic(shape = ComponentDefaults.CardShape, alpha = 0.25f) .animateContentSize(), shape = ComponentDefaults.CardShape, - color = MaterialTheme.colorScheme.surfaceContainer + color = Color.Transparent // Let glassmorphic handle the background ) { Column { - // --- Header Row --- if (hasHeader) { Row( modifier = Modifier @@ -137,7 +134,6 @@ fun AppCard( .padding(ComponentDefaults.CardPadding), verticalAlignment = Alignment.CenterVertically ) { - // 1. Optional Icon on the left if (icon != null) { Icon( imageVector = icon, @@ -148,7 +144,6 @@ fun AppCard( Spacer(modifier = Modifier.width(16.dp)) } - // 2. Title and Text Column Column(modifier = Modifier.weight(1f)) { if (!title.isNullOrBlank()) { Text( @@ -157,12 +152,9 @@ fun AppCard( color = MaterialTheme.colorScheme.onSurface ) } - - // Only show spacer if both title and text exist if (!title.isNullOrBlank() && !text.isNullOrBlank()) { Spacer(Modifier.size(4.dp)) } - if (!text.isNullOrBlank()) { Text( text = text, @@ -172,7 +164,6 @@ fun AppCard( } } - // 3. Expand Chevron (Far right) if (expandable) { Icon( imageVector = AppIcons.ArrowDropDown, @@ -184,15 +175,12 @@ fun AppCard( } } - // --- Content Area --- if (!expandable || isExpanded) { Column( modifier = Modifier.padding( start = ComponentDefaults.CardPadding, end = ComponentDefaults.CardPadding, bottom = ComponentDefaults.CardPadding, - // If we have a header, remove the top padding so content sits closer to the title. - // If no header (legacy behavior), keep the top padding. top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding ), content = content @@ -304,31 +292,27 @@ fun AppButton( modifier: Modifier? = Modifier, enabled: Boolean = true, shape: Shape? = null, - colors: ButtonColors = ButtonDefaults.buttonColors(), - elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), + colors: ButtonColors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) // Glassy primary + ), + elevation: ButtonElevation? = ButtonDefaults.buttonElevation(defaultElevation = 0.dp), border: BorderStroke? = null, contentPadding: PaddingValues = ButtonDefaults.ContentPadding, interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit ) { - val m = modifier ?: Modifier.height(ComponentDefaults.DefaultButtonHeight) val s = shape ?: ComponentDefaults.DefaultShape Button( onClick = onClick, - modifier = m, + modifier = m.border(1.dp, MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.2f), s), enabled = enabled, shape = s, colors = colors, elevation = elevation, border = border, - contentPadding = PaddingValues( - start = 8.dp, // More horizontal padding - end = 8.dp, - top = 8.dp, // Default vertical padding - bottom = 8.dp - ), + contentPadding = PaddingValues(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 8.dp), interactionSource = interactionSource ) { content() @@ -368,11 +352,7 @@ fun AppOutlinedButton( ) } -@Preview -@Composable -fun PrimaryButtonWithIconPreview() { - PrimaryButton(onClick = { }, text = stringResource(R.string.primary_with_icon), icon = AppIcons.Add) -} + /** * The secondary button for less prominent actions. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b06cd35..08e570f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,6 +43,7 @@ truth = "1.4.5" zstdJni = "1.5.7-7" composeMarkdown = "0.5.8" jitpack = "1.0.10" +foundationLayoutVersion = "1.10.3" [libraries] @@ -103,6 +104,7 @@ hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", ve hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.3.0" } mockk = { module = "io.mockk:mockk", version = "1.14.9" } compose-markdown = { module = "com.github.jeziellago:compose-markdown", version.ref = "composeMarkdown" } +androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayoutVersion" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }