Compare commits

..

1 Commits

Author SHA1 Message Date
jonasgaudian
2d0bf4cb1c implement glassmorphism design across UI components 2026-02-16 11:13:08 +01:00
10 changed files with 91 additions and 96 deletions

View File

@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-02-16T10:13:39.492968600Z">
<DropdownSelection timestamp="2026-02-15T19:51:37.987601800Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=RFCW11ML1WV" />
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\jonas\.android\avd\Medium_Phone_28.avd" />
</handle>
</Target>
</DropdownSelection>

View File

@@ -6,6 +6,7 @@ import java.util.Locale
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.hilt.android)
id("kotlin-parcelize")
@@ -61,8 +62,11 @@ android {
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi"
)
}
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
}
buildFeatures {
compose = true
viewBinding = false
@@ -126,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

View File

@@ -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),
@@ -197,15 +197,3 @@ private fun MenuItem(
}
}
}
@Preview
@Composable
fun MenuItemPreview() {
@Suppress("HardCodedStringLiteral")
MenuItem(
text = "Menu Item",
imageVector = AppIcons.Add,
painter = null,
onClick = {}
)
}

View File

@@ -69,10 +69,8 @@ fun <T : TabItem> 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 <T : TabItem> AppTabLayout(
.fillMaxHeight()
.padding(4.dp)
.background(
color = MaterialTheme.colorScheme.primary,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f), // Glassy indicator
shape = RoundedCornerShape(12.dp)
)
)

View File

@@ -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,14 +43,21 @@ 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) }
Surface(
modifier = modifier.glassmorphic(shape = RectangleShape, alpha = 0.2f),
color = Color.Transparent
) {
TopAppBar(
modifier = modifier.height(56.dp),
modifier = Modifier.height(56.dp),
windowInsets = WindowInsets(0.dp),
colors = colors,
title = {
@@ -104,6 +113,7 @@ fun AppTopAppBar(
},
actions = actions
)
}
if (showBottomSheet) {
HintBottomSheet(

View File

@@ -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,

View File

@@ -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.

View File

@@ -1,6 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
id("androidx.navigation.safeargs.kotlin") version "2.9.7" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "2.3.10"

View File

@@ -21,4 +21,13 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.dependency.useConstraints=false
android.defaults.buildfeatures.resvalues=true
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.enableAppCompileTimeRClass=false
android.usesSdkInManifest.disallowed=false
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
android.r8.optimizedResourceShrinking=false
android.builtInKotlin=false
android.newDsl=false

View File

@@ -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" }