diff --git a/Jetsnack/app/build.gradle.kts b/Jetsnack/app/build.gradle.kts index 0fca21037b..da2eae6c3c 100644 --- a/Jetsnack/app/build.gradle.kts +++ b/Jetsnack/app/build.gradle.kts @@ -86,6 +86,7 @@ android { kotlin { compilerOptions { jvmTarget = JvmTarget.fromTarget("17") + freeCompilerArgs.add("-opt-in=androidx.compose.foundation.style.ExperimentalFoundationStyleApi") } } compileOptions { @@ -106,6 +107,8 @@ android { } dependencies { + implementation(libs.androidx.compose.ui.text.google.fonts) + implementation(libs.androidx.graphics.shapes) val composeBom = platform(libs.androidx.compose.bom) implementation(composeBom) androidTestImplementation(composeBom) @@ -144,4 +147,5 @@ dependencies { implementation(libs.androidx.glance.appwidget) implementation(libs.androidx.glance.preview) + implementation(libs.androidx.startup) } diff --git a/Jetsnack/app/src/main/AndroidManifest.xml b/Jetsnack/app/src/main/AndroidManifest.xml index 73c8131195..9854a96115 100644 --- a/Jetsnack/app/src/main/AndroidManifest.xml +++ b/Jetsnack/app/src/main/AndroidManifest.xml @@ -25,6 +25,10 @@ android:theme="@style/Theme.Jetsnack" android:enableOnBackInvokedCallback="true" tools:targetApi="33"> + , val type: CollectionType = CollectionType.Normal) -enum class CollectionType { Normal, Highlight } +enum class CollectionType { Normal, Highlight, Card } /** * A fake repo @@ -61,6 +61,7 @@ private val popular = SnackCollection( private val wfhFavs = tastyTreats.copy( id = Random.nextLong(), name = "WFH favourites", + type = CollectionType.Card, ) private val newlyAdded = popular.copy( diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/MainActivity.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/MainActivity.kt index d2a1e96131..a6622c1af5 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/MainActivity.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/MainActivity.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalComposeUiApi::class) + package com.example.jetsnack.ui import android.appwidget.AppWidgetManager @@ -23,6 +25,8 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi +import androidx.compose.ui.ComposeUiFlags +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.lifecycle.lifecycleScope import com.example.jetsnack.widget.RecentOrdersWidgetReceiver @@ -33,6 +37,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) + ComposeUiFlags.isMediaQueryIntegrationEnabled = true if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { lifecycleScope.launch(Dispatchers.Default) { setWidgetPreviews() diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/SnackSharedElementKey.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/SnackSharedElementKey.kt index c07ed5d027..811911f080 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/SnackSharedElementKey.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/SnackSharedElementKey.kt @@ -22,8 +22,7 @@ enum class SnackSharedElementType { Bounds, Image, Title, - Tagline, - Background, + Price, } object FilterSharedElementKey diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Button.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Button.kt index 794bd3f1d7..16e4ac12f2 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Button.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Button.kt @@ -14,102 +14,247 @@ * limitations under the License. */ +@file:OptIn(ExperimentalFoundationStyleApi::class, ExperimentalMediaQueryApi::class) + package com.example.jetsnack.ui.components import android.content.res.Configuration.UI_MODE_NIGHT_YES -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.FocusInteraction +import androidx.compose.foundation.interaction.HoverInteraction import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ProvideTextStyle -import androidx.compose.material3.Text -import androidx.compose.material3.ripple +import androidx.compose.foundation.style.ExperimentalFoundationStyleApi +import androidx.compose.foundation.style.Style +import androidx.compose.foundation.style.rememberUpdatedStyleState +import androidx.compose.foundation.style.styleable import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalMediaQueryApi import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.UiMediaScope +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.theme.LoadingState +import com.example.jetsnack.ui.theme.loadingState +import com.example.jetsnack.ui.utils.UiMediaScopeWrapper +import androidx.compose.material3.Button @Composable -fun JetsnackButton( +fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, + style: Style = Style, enabled: Boolean = true, + loadingState: LoadingState = LoadingState.Loaded, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - shape: Shape = ButtonShape, - border: BorderStroke? = null, - backgroundGradient: List = JetsnackTheme.colors.interactivePrimary, - disabledBackgroundGradient: List = JetsnackTheme.colors.interactiveSecondary, - contentColor: Color = JetsnackTheme.colors.textInteractive, - disabledContentColor: Color = JetsnackTheme.colors.textHelp, - contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit, ) { - JetsnackSurface( - shape = shape, - color = Color.Transparent, - contentColor = if (enabled) contentColor else disabledContentColor, - border = border, + val styleState = rememberUpdatedStyleState(interactionSource) { + it.isEnabled = enabled + it.loadingState = loadingState + } + Row( modifier = modifier - .clip(shape) - .background( - Brush.horizontalGradient( - colors = if (enabled) backgroundGradient else disabledBackgroundGradient, - ), + .semantics( + properties = { + role = Role.Button + }, ) + .styleable(styleState, JetsnackTheme.styles.buttonStyle, style) .clickable( - onClick = onClick, enabled = enabled, - role = Role.Button, + onClick = onClick, interactionSource = interactionSource, indication = null, ), - ) { - ProvideTextStyle( - value = MaterialTheme.typography.labelLarge, + content = content, + verticalAlignment = Alignment.CenterVertically, + ) +} + +@Preview +@Preview("dark theme", "rectangle", uiMode = UI_MODE_NIGHT_YES) +@Composable +private fun ButtonPreview() { + UiMediaScopeWrapper(keyboardKind = UiMediaScope.KeyboardKind.Virtual, UiMediaScope.PointerPrecision.Blunt) { + Button(onClick = {}) { + Text(text = "Demo") + } + } +} + +@Preview +@Preview("dark theme", "rectangle", uiMode = UI_MODE_NIGHT_YES) +@Composable +private fun ButtonPreviewLoading() { + UiMediaScopeWrapper(keyboardKind = UiMediaScope.KeyboardKind.Virtual, UiMediaScope.PointerPrecision.Blunt) { + Button( + onClick = {}, + enabled = true, + loadingState = LoadingState.Loading, ) { - Row( - Modifier - .defaultMinSize( - minWidth = ButtonDefaults.MinWidth, - minHeight = ButtonDefaults.MinHeight, - ) - .indication(interactionSource, ripple()) - .padding(contentPadding), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - content = content, - ) + Text(text = "Demo") + } + } +} + +@Preview +@Preview("dark theme", "rectangle", uiMode = UI_MODE_NIGHT_YES) +@Composable +private fun ButtonPreviewDisabled() { + UiMediaScopeWrapper(keyboardKind = UiMediaScope.KeyboardKind.Virtual, UiMediaScope.PointerPrecision.Blunt) { + Button( + onClick = {}, + enabled = false, + ) { + Text(text = "Demo") } } } -private val ButtonShape = RoundedCornerShape(percent = 50) +@Preview +@Preview("dark theme", "rectangle", uiMode = UI_MODE_NIGHT_YES) +@Composable +private fun ButtonPreviewPressed() { + UiMediaScopeWrapper(keyboardKind = UiMediaScope.KeyboardKind.Virtual, UiMediaScope.PointerPrecision.Blunt) { + val interactionSource = remember { MutableInteractionSource() } + LaunchedEffect(interactionSource) { + interactionSource.emit(PressInteraction.Press(Offset.Zero)) + } + Button( + onClick = {}, + interactionSource = interactionSource, + ) { + Text(text = "Demo") + } + } +} -@Preview("default", "round") -@Preview("dark theme", "round", uiMode = UI_MODE_NIGHT_YES) -@Preview("large font", "round", fontScale = 2f) +@Preview +@Preview("dark theme", "rectangle", uiMode = UI_MODE_NIGHT_YES) @Composable -private fun ButtonPreview() { - JetsnackTheme { - JetsnackButton(onClick = {}) { +private fun ButtonPreviewHovered() { + UiMediaScopeWrapper(keyboardKind = UiMediaScope.KeyboardKind.Virtual, UiMediaScope.PointerPrecision.Blunt) { + val interactionSource = remember { MutableInteractionSource() } + LaunchedEffect(interactionSource) { + interactionSource.emit(HoverInteraction.Enter()) + } + Button( + onClick = {}, + interactionSource = interactionSource, + ) { + Text(text = "Demo") + } + } +} + +@Preview +@Preview("dark theme", "rectangle", uiMode = UI_MODE_NIGHT_YES) +@Composable +private fun ButtonPreviewFocused() { + UiMediaScopeWrapper(keyboardKind = UiMediaScope.KeyboardKind.Virtual, UiMediaScope.PointerPrecision.Blunt) { + val interactionSource = remember { MutableInteractionSource() } + LaunchedEffect(interactionSource) { + interactionSource.emit(FocusInteraction.Focus()) + } + Button( + onClick = {}, + interactionSource = interactionSource, + ) { + Text(text = "Demo") + } + } +} + +@Preview +@Preview("dark theme", "rectangle", uiMode = UI_MODE_NIGHT_YES) +@Composable +private fun ButtonPreviewHoveredFocused() { + UiMediaScopeWrapper(keyboardKind = UiMediaScope.KeyboardKind.Virtual, UiMediaScope.PointerPrecision.Blunt) { + val interactionSource = remember { MutableInteractionSource() } + LaunchedEffect(interactionSource) { + interactionSource.emit(HoverInteraction.Enter()) + interactionSource.emit(FocusInteraction.Focus()) + } + Button( + onClick = {}, + interactionSource = interactionSource, + ) { + Text(text = "Demo") + } + } +} + +@Preview +@Preview("dark theme", "rectangle", uiMode = UI_MODE_NIGHT_YES) +@Composable +private fun ButtonPreviewPressedFocused() { + UiMediaScopeWrapper(keyboardKind = UiMediaScope.KeyboardKind.Virtual, UiMediaScope.PointerPrecision.Blunt) { + val interactionSource = remember { MutableInteractionSource() } + LaunchedEffect(interactionSource) { + interactionSource.emit(FocusInteraction.Focus()) + interactionSource.emit(PressInteraction.Press(Offset.Zero)) + } + Button( + onClick = {}, + interactionSource = interactionSource, + ) { + Text(text = "Demo") + } + } +} + +// TODO this state is broken visually +@Preview +@Preview("dark theme", "rectangle", uiMode = UI_MODE_NIGHT_YES) +@Composable +private fun ButtonPreviewPressedHovered() { + UiMediaScopeWrapper(keyboardKind = UiMediaScope.KeyboardKind.Virtual, UiMediaScope.PointerPrecision.Blunt) { + val interactionSource = remember { MutableInteractionSource() } + LaunchedEffect(interactionSource) { + interactionSource.emit(PressInteraction.Press(Offset.Zero)) + interactionSource.emit(HoverInteraction.Enter()) + } + Button( + onClick = {}, + interactionSource = interactionSource, + ) { + Text(text = "Demo") + } + } +} + +@Preview +@Composable +private fun ButtonDesktopPreview() { + UiMediaScopeWrapper(keyboardKind = UiMediaScope.KeyboardKind.Physical, UiMediaScope.PointerPrecision.Fine) { + Button( + onClick = {}, + ) { + Text(text = "Demo") + } + } +} + +@Preview +@Composable +private fun ButtonDesktopPreviewDisabled() { + UiMediaScopeWrapper(keyboardKind = UiMediaScope.KeyboardKind.Physical, UiMediaScope.PointerPrecision.Fine) { + Button( + onClick = {}, + enabled = false, + ) { Text(text = "Demo") } } @@ -120,9 +265,12 @@ private fun ButtonPreview() { @Preview("large font", "rectangle", fontScale = 2f) @Composable private fun RectangleButtonPreview() { - JetsnackTheme { - JetsnackButton( - onClick = {}, shape = RectangleShape, + UiMediaScopeWrapper(keyboardKind = UiMediaScope.KeyboardKind.Virtual, UiMediaScope.PointerPrecision.Blunt) { + Button( + onClick = {}, + style = { + shape(RoundedCornerShape(4.dp)) + }, ) { Text(text = "Demo") } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Card.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Card.kt index 8507472164..a50e321c32 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Card.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Card.kt @@ -14,39 +14,39 @@ * limitations under the License. */ +@file:OptIn(ExperimentalFoundationStyleApi::class) + package com.example.jetsnack.ui.components import android.content.res.Configuration -import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.foundation.style.ExperimentalFoundationStyleApi +import androidx.compose.foundation.style.Style +import androidx.compose.foundation.style.StyleState +import androidx.compose.foundation.style.rememberUpdatedStyleState +import androidx.compose.foundation.style.then import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.example.jetsnack.ui.theme.JetsnackTheme @Composable fun JetsnackCard( modifier: Modifier = Modifier, - shape: Shape = MaterialTheme.shapes.medium, - color: Color = JetsnackTheme.colors.uiBackground, - contentColor: Color = JetsnackTheme.colors.textPrimary, - border: BorderStroke? = null, - elevation: Dp = 4.dp, - content: @Composable () -> Unit, + style: Style = Style, + interactionSource: InteractionSource = remember { MutableInteractionSource() }, + styleState: StyleState = rememberUpdatedStyleState(interactionSource), + content: @Composable BoxScope.() -> Unit, ) { - JetsnackSurface( + Surface( modifier = modifier, - shape = shape, - color = color, - contentColor = contentColor, - elevation = elevation, - border = border, + style = JetsnackTheme.styles.cardStyle then style, + styleState = styleState, content = content, ) } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Divider.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Divider.kt index 089313675a..a135fa9740 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Divider.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Divider.kt @@ -14,36 +14,29 @@ * limitations under the License. */ +@file:OptIn(ExperimentalFoundationStyleApi::class) + package com.example.jetsnack.ui.components import android.content.res.Configuration import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size -import androidx.compose.material3.HorizontalDivider +import androidx.compose.foundation.style.ExperimentalFoundationStyleApi +import androidx.compose.foundation.style.Style +import androidx.compose.foundation.style.styleable import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.theme.JetsnackTheme.Companion.LocalJetsnackTheme @Composable -fun JetsnackDivider( - modifier: Modifier = Modifier, - color: Color = JetsnackTheme.colors.uiBorder.copy(alpha = DividerAlpha), - thickness: Dp = 1.dp, -) { - HorizontalDivider( - modifier = modifier, - color = color, - thickness = thickness, - ) +fun JetsnackDivider(modifier: Modifier = Modifier, style: Style = Style) { + Box(modifier = modifier.styleable(null, LocalJetsnackTheme.current.styles.dividerStyle, style)) } -private const val DividerAlpha = 0.12f - @Preview("default", showBackground = true) @Preview("dark theme", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true) @Composable diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Filters.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Filters.kt index 30121ea109..36aff1055a 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Filters.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Filters.kt @@ -14,18 +14,23 @@ * limitations under the License. */ -@file:OptIn(ExperimentalSharedTransitionApi::class) +@file:OptIn( + ExperimentalSharedTransitionApi::class, + ExperimentalFoundationStyleApi::class, + ExperimentalMediaQueryApi::class +) package com.example.jetsnack.ui.components import android.content.res.Configuration +import androidx.compose.ui.ExperimentalMediaQueryApi import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope -import androidx.compose.animation.animateColorAsState -import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.FocusInteraction +import androidx.compose.foundation.interaction.HoverInteraction import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -34,18 +39,22 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.selection.toggleable -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.style.ExperimentalFoundationStyleApi +import androidx.compose.foundation.style.Style +import androidx.compose.foundation.style.rememberUpdatedStyleState +import androidx.compose.foundation.style.styleable +import androidx.compose.foundation.style.then import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.UiMediaScope +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -54,6 +63,11 @@ import com.example.jetsnack.R import com.example.jetsnack.model.Filter import com.example.jetsnack.ui.FilterSharedElementKey import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.theme.colors +import com.example.jetsnack.ui.theme.shapes +import com.example.jetsnack.ui.theme.typography +import com.example.jetsnack.ui.utils.JetsnackThemeWrapper +import com.example.jetsnack.ui.utils.UiMediaScopeWrapper @Composable fun FilterBar( @@ -61,13 +75,14 @@ fun FilterBar( onShowFilters: () -> Unit, filterScreenVisible: Boolean, sharedTransitionScope: SharedTransitionScope, + modifier: Modifier = Modifier, ) { with(sharedTransitionScope) { LazyRow( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(start = 12.dp, end = 8.dp), - modifier = Modifier.heightIn(min = 56.dp), + modifier = modifier.heightIn(min = 56.dp), ) { item { AnimatedVisibility(visible = !filterScreenVisible) { @@ -82,81 +97,69 @@ fun FilterBar( ) { Icon( painterResource(R.drawable.ic_filter_list), - tint = JetsnackTheme.colors.brand, + tint = JetsnackTheme.colors.iconPrimary, contentDescription = stringResource(R.string.label_filters), - modifier = Modifier.diagonalGradientBorder( - colors = JetsnackTheme.colors.interactiveSecondary, - shape = CircleShape, - ), + modifier = Modifier + .styleable(null) { + contentPaddingHorizontal(2.dp) + minHeight(32.dp) + border(3.dp, Brush.linearGradient(colors.interactiveSecondary)) + shape(RoundedCornerShape(50)) + }, ) } } } items(filters) { filter -> - FilterChip(filter = filter, shape = MaterialTheme.shapes.small) + FilterChip( + filter = filter, + style = Style { + shape(shapes.small) + }, + ) } } } } @Composable -fun FilterChip(filter: Filter, modifier: Modifier = Modifier, shape: Shape = MaterialTheme.shapes.small) { +fun FilterChip( + filter: Filter, + modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + style: Style = Style, +) { + val (selected, setSelected) = filter.enabled - val backgroundColor by animateColorAsState( - if (selected) JetsnackTheme.colors.brandSecondary else JetsnackTheme.colors.uiBackground, - label = "background color", - ) - val border = Modifier.fadeInDiagonalGradientBorder( - showBorder = !selected, - colors = JetsnackTheme.colors.interactiveSecondary, - shape = shape, - ) - val textColor by animateColorAsState( - if (selected) Color.Black else JetsnackTheme.colors.textSecondary, - label = "text color", + val styleState = rememberUpdatedStyleState( + interactionSource, + { + it.isSelected = selected + }, ) - JetsnackSurface( - modifier = modifier, - color = backgroundColor, - contentColor = textColor, - shape = shape, - elevation = 2.dp, + Surface( + modifier = modifier + .toggleable( + value = selected, + onValueChange = setSelected, + interactionSource = interactionSource, + indication = null, + ), + style = JetsnackTheme.styles.filterChipStyle then style, + styleState = styleState, ) { - val interactionSource = remember { MutableInteractionSource() } - - val pressed by interactionSource.collectIsPressedAsState() - val backgroundPressed = - if (pressed) { - Modifier.offsetGradientBackground( - JetsnackTheme.colors.interactiveSecondary, - 200f, - 0f, - ) - } else { - Modifier.background(Color.Transparent) - } - Box( - modifier = Modifier - .toggleable( - value = selected, - onValueChange = setSelected, - interactionSource = interactionSource, - indication = null, - ) - .then(backgroundPressed) - .then(border), - ) { - Text( - text = filter.name, - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - modifier = Modifier.padding( - horizontal = 20.dp, - vertical = 6.dp, - ), - ) - } + Text( + text = filter.name, + style = { + textStyleWithFontFamilyFix(typography.labelSmall) + }, + maxLines = 1, + modifier = Modifier.padding( + horizontal = 20.dp, + vertical = 6.dp, + ), + ) } } @@ -165,7 +168,7 @@ fun FilterChip(filter: Filter, modifier: Modifier = Modifier, shape: Shape = Mat @Preview("large font", fontScale = 2f) @Composable private fun FilterDisabledPreview() { - JetsnackTheme { + JetsnackThemeWrapper { FilterChip(Filter(name = "Demo", enabled = false), Modifier.padding(4.dp)) } } @@ -175,7 +178,82 @@ private fun FilterDisabledPreview() { @Preview("large font", fontScale = 2f) @Composable private fun FilterEnabledPreview() { - JetsnackTheme { + JetsnackThemeWrapper { FilterChip(Filter(name = "Demo", enabled = true)) } } + +@Preview("hovered focused") +@Preview("dark theme hovered focused", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun FilterPreviewHoveredFocused() { + UiMediaScopeWrapper(keyboardKind = UiMediaScope.KeyboardKind.Virtual, pointerPrecision = UiMediaScope.PointerPrecision.Blunt) { + val interactionSource = remember { MutableInteractionSource() } + LaunchedEffect(interactionSource) { + interactionSource.emit(HoverInteraction.Enter()) + interactionSource.emit(FocusInteraction.Focus()) + } + JetsnackThemeWrapper { + FilterChip( + filter = Filter(name = "Demo"), + interactionSource = interactionSource, + ) + } + } +} + +@Preview("pressed focused") +@Preview("dark theme pressed focused", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun FilterPreviewPressedFocused() { + UiMediaScopeWrapper(keyboardKind = UiMediaScope.KeyboardKind.Virtual, pointerPrecision = UiMediaScope.PointerPrecision.Blunt) { + val interactionSource = remember { MutableInteractionSource() } + LaunchedEffect(interactionSource) { + interactionSource.emit(FocusInteraction.Focus()) + interactionSource.emit(PressInteraction.Press(Offset.Zero)) + } + JetsnackThemeWrapper { + FilterChip( + filter = Filter(name = "Demo"), + interactionSource = interactionSource, + ) + } + } +} + +@Preview("pressed hovered") +@Preview("dark theme pressed hovered", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun FilterPreviewPressedHovered() { + UiMediaScopeWrapper(keyboardKind = UiMediaScope.KeyboardKind.Virtual, pointerPrecision = UiMediaScope.PointerPrecision.Blunt) { + val interactionSource = remember { MutableInteractionSource() } + LaunchedEffect(interactionSource) { + interactionSource.emit(PressInteraction.Press(Offset.Zero)) + interactionSource.emit(HoverInteraction.Enter()) + } + JetsnackThemeWrapper { + FilterChip( + filter = Filter(name = "Demo"), + interactionSource = interactionSource, + ) + } + } +} + +@Preview("pressed") +@Preview("dark theme pressed", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun FilterPreviewPressed() { + UiMediaScopeWrapper(keyboardKind = UiMediaScope.KeyboardKind.Virtual, pointerPrecision = UiMediaScope.PointerPrecision.Blunt) { + val interactionSource = remember { MutableInteractionSource() } + LaunchedEffect(interactionSource) { + interactionSource.emit(PressInteraction.Press(Offset.Zero)) + } + JetsnackThemeWrapper { + FilterChip( + filter = Filter(name = "Demo"), + interactionSource = interactionSource, + ) + } + } +} diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Gradient.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Gradient.kt index 1a7fba80c4..ff061e82ce 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Gradient.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Gradient.kt @@ -16,11 +16,8 @@ package com.example.jetsnack.ui.components -import androidx.compose.animation.animateColorAsState -import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.BlendMode @@ -32,52 +29,10 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -fun Modifier.diagonalGradientTint(colors: List, blendMode: BlendMode) = drawWithContent { +fun Modifier.contentTintDiagonalGradient(colors: List, blendMode: BlendMode) = drawWithContent { drawContent() drawRect( brush = Brush.linearGradient(colors), blendMode = blendMode, ) } - -fun Modifier.offsetGradientBackground(colors: List, width: Float, offset: Float = 0f) = background( - Brush.horizontalGradient( - colors = colors, - startX = -offset, - endX = width - offset, - tileMode = TileMode.Mirror, - ), -) - -fun Modifier.offsetGradientBackground(colors: List, width: Density.() -> Float, offset: Density.() -> Float = { 0f }) = drawBehind { - val actualOffset = offset() - - drawRect( - Brush.horizontalGradient( - colors = colors, - startX = -actualOffset, - endX = width() - actualOffset, - tileMode = TileMode.Mirror, - ), - ) -} - -fun Modifier.diagonalGradientBorder(colors: List, borderSize: Dp = 2.dp, shape: Shape) = border( - width = borderSize, - brush = Brush.linearGradient(colors), - shape = shape, -) - -fun Modifier.fadeInDiagonalGradientBorder(showBorder: Boolean, colors: List, borderSize: Dp = 2.dp, shape: Shape) = composed { - val animatedColors = List(colors.size) { i -> - animateColorAsState( - if (showBorder) colors[i] else colors[i].copy(alpha = 0f), - label = "animated color", - ).value - } - diagonalGradientBorder( - colors = animatedColors, - borderSize = borderSize, - shape = shape, - ) -} diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/GradientTintedIconButton.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/GradientTintedIconButton.kt index 39ebb9070e..00dd18b548 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/GradientTintedIconButton.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/GradientTintedIconButton.kt @@ -14,25 +14,23 @@ * limitations under the License. */ +@file:OptIn(ExperimentalFoundationStyleApi::class) + package com.example.jetsnack.ui.components import android.content.res.Configuration import androidx.annotation.DrawableRes -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.style.ExperimentalFoundationStyleApi +import androidx.compose.foundation.style.Style +import androidx.compose.foundation.style.rememberUpdatedStyleState +import androidx.compose.foundation.style.then import androidx.compose.material3.Icon -import androidx.compose.material3.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -45,53 +43,25 @@ fun JetsnackGradientTintedIconButton( onClick: () -> Unit, contentDescription: String?, modifier: Modifier = Modifier, - colors: List = JetsnackTheme.colors.interactiveSecondary, + style: Style = Style, ) { val interactionSource = remember { MutableInteractionSource() } - // This should use a layer + srcIn but needs investigation - val border = Modifier.fadeInDiagonalGradientBorder( - showBorder = true, - colors = JetsnackTheme.colors.interactiveSecondary, - shape = CircleShape, - ) - val pressed by interactionSource.collectIsPressedAsState() - val background = if (pressed) { - Modifier.offsetGradientBackground(colors, 200f, 0f) - } else { - Modifier.background(JetsnackTheme.colors.uiBackground) - } - val blendMode = if (JetsnackTheme.colors.isDark) BlendMode.Darken else BlendMode.Plus - val modifierColor = if (pressed) { - Modifier.diagonalGradientTint( - colors = listOf( - JetsnackTheme.colors.textSecondary, - JetsnackTheme.colors.textSecondary, - ), - blendMode = blendMode, - ) - } else { - Modifier.diagonalGradientTint( - colors = colors, - blendMode = blendMode, - ) - } + val styleState = rememberUpdatedStyleState(interactionSource) Surface( + style = JetsnackTheme.styles.gradientIconButtonStyle then style, + styleState = styleState, modifier = modifier .clickable( onClick = onClick, interactionSource = interactionSource, indication = null, - ) - .clip(CircleShape) - .then(border) - .then(background), - color = Color.Transparent, + ), ) { Icon( painter = painterResource(id = iconResourceId), contentDescription = contentDescription, - modifier = modifierColor, + tint = JetsnackTheme.colors.textPrimary, ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Grid.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Grid.kt deleted file mode 100644 index 0fd9f27839..0000000000 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Grid.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.jetsnack.ui.components - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.Layout - -/** - * A simple grid which lays elements out vertically in evenly sized [columns]. - */ -@Composable -fun VerticalGrid(modifier: Modifier = Modifier, columns: Int = 2, content: @Composable () -> Unit) { - Layout( - content = content, - modifier = modifier, - ) { measurables, constraints -> - val itemWidth = constraints.maxWidth / columns - // Keep given height constraints, but set an exact width - val itemConstraints = constraints.copy( - minWidth = itemWidth, - maxWidth = itemWidth, - ) - // Measure each item with these constraints - val placeables = measurables.map { it.measure(itemConstraints) } - // Track each columns height so we can calculate the overall height - val columnHeights = Array(columns) { 0 } - placeables.forEachIndexed { index, placeable -> - val column = index % columns - columnHeights[column] += placeable.height - } - val height = (columnHeights.maxOrNull() ?: constraints.minHeight) - .coerceAtMost(constraints.maxHeight) - layout( - width = constraints.maxWidth, - height = height, - ) { - // Track the Y co-ord per column we have placed up to - val columnY = Array(columns) { 0 } - placeables.forEachIndexed { index, placeable -> - val column = index % columns - placeable.placeRelative( - x = column * itemWidth, - y = columnY[column], - ) - columnY[column] += placeable.height - } - } - } -} diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/QuantitySelector.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/QuantitySelector.kt index 4952bd2768..539f89e2f4 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/QuantitySelector.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/QuantitySelector.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalFoundationStyleApi::class) + package com.example.jetsnack.ui.components import android.content.res.Configuration.UI_MODE_NIGHT_YES @@ -21,8 +23,7 @@ import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.foundation.style.ExperimentalFoundationStyleApi import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment @@ -37,15 +38,19 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.jetsnack.R import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.theme.colors +import com.example.jetsnack.ui.theme.typography @Composable fun QuantitySelector(count: Int, decreaseItemCount: () -> Unit, increaseItemCount: () -> Unit, modifier: Modifier = Modifier) { Row(modifier = modifier) { Text( text = stringResource(R.string.quantity), - style = MaterialTheme.typography.titleMedium, - color = JetsnackTheme.colors.textSecondary, - fontWeight = FontWeight.Normal, + style = { + textStyleWithFontFamilyFix(typography.labelLarge) + contentColor(colors.textSecondary) + fontWeight(FontWeight.Normal) + }, modifier = Modifier .padding(end = 18.dp) .align(Alignment.CenterVertically), @@ -63,10 +68,12 @@ fun QuantitySelector(count: Int, decreaseItemCount: () -> Unit, increaseItemCoun ) { Text( text = "$it", - style = MaterialTheme.typography.titleSmall, - fontSize = 18.sp, - color = JetsnackTheme.colors.textPrimary, - textAlign = TextAlign.Center, + style = { + textStyleWithFontFamilyFix(typography.labelLarge) + fontSize(18.sp) + contentColor(colors.textPrimary) + textAlign(TextAlign.Center) + }, modifier = Modifier.widthIn(min = 24.dp), ) } @@ -85,7 +92,7 @@ fun QuantitySelector(count: Int, decreaseItemCount: () -> Unit, increaseItemCoun @Composable fun QuantitySelectorPreview() { JetsnackTheme { - JetsnackSurface { + Surface { QuantitySelector(1, {}, {}) } } @@ -95,7 +102,7 @@ fun QuantitySelectorPreview() { @Composable fun QuantitySelectorPreviewRtl() { JetsnackTheme { - JetsnackSurface { + Surface { CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { QuantitySelector(1, {}, {}) } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt index 028ee2b1ce..73a2e14385 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt @@ -14,7 +14,10 @@ * limitations under the License. */ -@file:OptIn(ExperimentalSharedTransitionApi::class) +@file:OptIn( + ExperimentalSharedTransitionApi::class, ExperimentalFoundationStyleApi::class, ExperimentalMaterial3ExpressiveApi::class, + ExperimentalMediaQueryApi::class, +) package com.example.jetsnack.ui.components @@ -25,50 +28,58 @@ import androidx.compose.animation.EnterExitState import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.border 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.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.style.ExperimentalFoundationStyleApi +import androidx.compose.foundation.style.Style +import androidx.compose.foundation.style.then +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalMediaQueryApi import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.shadow.Shadow import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.graphics.shapes.Morph +import androidx.graphics.shapes.RoundedPolygon import coil.compose.AsyncImage import coil.request.ImageRequest import com.example.jetsnack.R @@ -83,20 +94,22 @@ import com.example.jetsnack.ui.SnackSharedElementType import com.example.jetsnack.ui.snackdetail.nonSpatialExpressiveSpring import com.example.jetsnack.ui.snackdetail.snackDetailBoundsTransform import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.theme.colors +import com.example.jetsnack.ui.theme.typography +import com.example.jetsnack.ui.utils.JetsnackThemeWrapper +import com.example.jetsnack.ui.utils.SnackPolygons +import com.example.jetsnack.ui.utils.UiMediaScopeWrapper +import com.example.jetsnack.ui.utils.asShape +import com.example.jetsnack.ui.utils.formatPrice +import com.example.jetsnack.ui.utils.sharedBoundsRevealWithShapeMorph -private val HighlightCardWidth = 170.dp -private val HighlightCardPadding = 16.dp -private val Density.cardWidthWithPaddingPx - get() = (HighlightCardWidth + HighlightCardPadding).toPx() +private val normalTextStyle = Style { + textStyleWithFontFamilyFix(typography.titleSmall) + textAlign(TextAlign.Center) +} @Composable -fun SnackCollection( - snackCollection: SnackCollection, - onSnackClick: (Long, String) -> Unit, - modifier: Modifier = Modifier, - index: Int = 0, - highlight: Boolean = true, -) { +fun SnackCollection(snackCollection: SnackCollection, onSnackClick: (Long, String) -> Unit, modifier: Modifier = Modifier) { Column(modifier = modifier) { Row( verticalAlignment = Alignment.CenterVertically, @@ -106,8 +119,10 @@ fun SnackCollection( ) { Text( text = snackCollection.name, - style = MaterialTheme.typography.titleLarge, - color = JetsnackTheme.colors.brand, + style = { + textStyleWithFontFamilyFix(typography.bodyLarge) + contentColor(colors.textPrimary) + }, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier @@ -120,151 +135,64 @@ fun SnackCollection( ) { Icon( painter = painterResource(id = R.drawable.ic_arrow_back), - tint = JetsnackTheme.colors.brand, + tint = JetsnackTheme.colors.textPrimary, contentDescription = null, ) } } - if (highlight && snackCollection.type == CollectionType.Highlight) { - HighlightedSnacks(snackCollection.id, index, snackCollection.snacks, onSnackClick) - } else { - Snacks(snackCollection.id, snackCollection.snacks, onSnackClick) - } - } -} - -@Composable -private fun HighlightedSnacks( - snackCollectionId: Long, - index: Int, - snacks: List, - onSnackClick: (Long, String) -> Unit, - modifier: Modifier = Modifier, -) { - val rowState = rememberLazyListState() - val cardWidthWithPaddingPx = with(LocalDensity.current) { cardWidthWithPaddingPx } - - val scrollProvider = { - // Simple calculation of scroll distance for homogenous item types with the same width. - val offsetFromStart = cardWidthWithPaddingPx * rowState.firstVisibleItemIndex - offsetFromStart + rowState.firstVisibleItemScrollOffset - } - - val gradient = when ((index / 2) % 2) { - 0 -> JetsnackTheme.colors.gradient6_1 - else -> JetsnackTheme.colors.gradient6_2 - } - - LazyRow( - state = rowState, - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(start = 24.dp, end = 24.dp), - ) { - itemsIndexed(snacks) { index, snack -> - HighlightSnackItem( - snackCollectionId = snackCollectionId, - snack = snack, - onSnackClick = onSnackClick, - index = index, - gradient = gradient, - scrollProvider = scrollProvider, - ) - } - } -} - -@Composable -private fun Snacks(snackCollectionId: Long, snacks: List, onSnackClick: (Long, String) -> Unit, modifier: Modifier = Modifier) { - LazyRow( - modifier = modifier, - contentPadding = PaddingValues(start = 12.dp, end = 12.dp), - ) { - items(snacks) { snack -> - SnackItem(snack, snackCollectionId, onSnackClick) - } - } -} - -@Composable -fun SnackItem(snack: Snack, snackCollectionId: Long, onSnackClick: (Long, String) -> Unit, modifier: Modifier = Modifier) { - JetsnackSurface( - shape = MaterialTheme.shapes.medium, - modifier = modifier.padding( - start = 4.dp, - end = 4.dp, - bottom = 8.dp, - ), - - ) { - val sharedTransitionScope = LocalSharedTransitionScope.current - ?: throw IllegalStateException("No sharedTransitionScope found") - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current - ?: throw IllegalStateException("No animatedVisibilityScope found") - - with(sharedTransitionScope) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .clickable(onClick = { - onSnackClick(snack.id, snackCollectionId.toString()) - }) - .padding(8.dp), - ) { - SnackImage( - imageRes = snack.imageRes, - elevation = 1.dp, - contentDescription = null, - modifier = Modifier - .size(120.dp) - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = snackCollectionId.toString(), - type = SnackSharedElementType.Image, - ), - ), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = snackDetailBoundsTransform, - ), - ) - Text( - text = snack.name, - style = MaterialTheme.typography.titleMedium, - color = JetsnackTheme.colors.textSecondary, - modifier = Modifier - .padding(top = 8.dp) - .wrapContentWidth() - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = snackCollectionId.toString(), - type = SnackSharedElementType.Title, - ), - ), - animatedVisibilityScope = animatedVisibilityScope, - enter = fadeIn(nonSpatialExpressiveSpring()), - exit = fadeOut(nonSpatialExpressiveSpring()), - resizeMode = SharedTransitionScope.ResizeMode.scaleToBounds(), - boundsTransform = snackDetailBoundsTransform, - ), - ) + LazyRow( + state = rememberLazyListState(), + modifier = modifier.padding(bottom = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(start = 24.dp, end = 24.dp), + ) { + itemsIndexed(snacks, key = { _, item -> item.id }) { _, snack -> + when (snackCollection.type) { + CollectionType.Normal -> + SnackItem( + snackCollectionId = snackCollection.id, + snack = snack, + onSnackClick = onSnackClick, + showTagLine = false, + imageShape = SnackPolygons.snackItemPolygonRounded, + style = JetsnackTheme.styles.normalCardStyle, + snackTextStyle = normalTextStyle, + imageAspectRatio = 4f / 3f, + ) + CollectionType.Highlight -> + SnackItem( + snackCollectionId = snackCollection.id, + snack = snack, + onSnackClick = onSnackClick, + showAddButton = true, + style = JetsnackTheme.styles.highlightGlowCardStyle, + ) + CollectionType.Card -> + SnackItem( + snackCollectionId = snackCollection.id, + snack = snack, + onSnackClick = onSnackClick, + style = JetsnackTheme.styles.plainCardStyle, + imageAspectRatio = 16 / 9f, + ) + } } } } } @Composable -private fun HighlightSnackItem( +private fun SnackItem( snackCollectionId: Long, snack: Snack, - onSnackClick: (Long, String) -> Unit, - index: Int, - gradient: List, - scrollProvider: () -> Float, modifier: Modifier = Modifier, + style: Style = Style, + snackTextStyle: Style = Style, + imageShape: RoundedPolygon = SnackPolygons.snackItemPolygon, + imageAspectRatio: Float = 1f, + showTagLine: Boolean = true, + showAddButton: Boolean = false, + onSnackClick: (Long, String) -> Unit, ) { val sharedTransitionScope = LocalSharedTransitionScope.current ?: throw IllegalStateException("No Scope found") @@ -279,11 +207,14 @@ private fun HighlightSnackItem( EnterExitState.PostExit -> 20.dp } } + val interactionSource = remember { MutableInteractionSource() } JetsnackCard( - elevation = 0.dp, - shape = RoundedCornerShape(roundedCornerAnimation), + style = Style { + shape(RoundedCornerShape(roundedCornerAnimation)) + } then style, + interactionSource = interactionSource, modifier = modifier - .padding(bottom = 16.dp) + .wrapContentHeight() .sharedBounds( sharedContentState = rememberSharedContentState( key = SnackSharedElementKey( @@ -302,137 +233,150 @@ private fun HighlightSnackItem( enter = fadeIn(), exit = fadeOut(), ) - .size( - width = HighlightCardWidth, - height = 250.dp, - ) - .border( - 1.dp, - JetsnackTheme.colors.uiBorder.copy(alpha = 0.12f), - RoundedCornerShape(roundedCornerAnimation), - ), - - ) { - Column( - modifier = Modifier - .clickable(onClick = { + .clickable( + onClick = { onSnackClick( snack.id, snackCollectionId.toString(), ) - }) - .fillMaxSize(), - + }, + interactionSource = interactionSource, + indication = null, + ), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), ) { - Box( - modifier = Modifier - .height(160.dp) - .fillMaxWidth(), + val sharedContentState = rememberSharedContentState( + key = SnackSharedElementKey( + snackId = snack.id, + origin = snackCollectionId.toString(), + type = SnackSharedElementType.Image, + ), + ) + val targetRoundedPolygon = SnackPolygons.snackDetailPolygon + val morph = remember { Morph(imageShape, targetRoundedPolygon) } + val progress = animatedVisibilityScope.transition.animateFloat( + transitionSpec = { + tween(300, easing = LinearEasing) + }, ) { - Box( - modifier = Modifier - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = snackCollectionId.toString(), - type = SnackSharedElementType.Background, - ), - ), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = snackDetailBoundsTransform, - enter = fadeIn(nonSpatialExpressiveSpring()), - exit = fadeOut(nonSpatialExpressiveSpring()), - resizeMode = SharedTransitionScope.ResizeMode.scaleToBounds(), - ) - .height(100.dp) - .fillMaxWidth() - .offsetGradientBackground( - colors = gradient, - width = { - // The Cards show a gradient which spans 6 cards and - // scrolls with parallax. - 6 * cardWidthWithPaddingPx - }, - offset = { - val left = index * cardWidthWithPaddingPx - val gradientOffset = left - (scrollProvider() / 3f) - gradientOffset - }, - ), - ) + when (it) { + EnterExitState.PreEnter -> 1f + EnterExitState.Visible -> 0f + EnterExitState.PostExit -> 1f + } + }.value - SnackImage( - imageRes = snack.imageRes, - contentDescription = null, - modifier = Modifier - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = snackCollectionId.toString(), - type = SnackSharedElementType.Image, - ), - ), - animatedVisibilityScope = animatedVisibilityScope, - exit = fadeOut(nonSpatialExpressiveSpring()), - enter = fadeIn(nonSpatialExpressiveSpring()), - boundsTransform = snackDetailBoundsTransform, - ) - .align(Alignment.BottomCenter) - .size(120.dp), - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = snack.name, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.titleLarge, - color = JetsnackTheme.colors.textSecondary, + SnackImage( + imageRes = snack.imageRes, + contentDescription = null, + style = Style { + val shape = if (sharedContentState.isMatchFound) { + morph.asShape(progress) + } else { + imageShape.asShape() + } + shape(shape) + }, modifier = Modifier - .padding(horizontal = 16.dp) - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = snackCollectionId.toString(), - type = SnackSharedElementType.Title, - ), - ), + .sharedBoundsRevealWithShapeMorph( + sharedContentState = sharedContentState, + sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, - enter = fadeIn(nonSpatialExpressiveSpring()), - exit = fadeOut(nonSpatialExpressiveSpring()), boundsTransform = snackDetailBoundsTransform, - resizeMode = SharedTransitionScope.ResizeMode.scaleToBounds(), + targetShape = targetRoundedPolygon, + restingShape = imageShape, ) - .wrapContentWidth(), + .fillMaxWidth() + .aspectRatio(imageAspectRatio), ) - Spacer(modifier = Modifier.height(4.dp)) - - Text( - text = snack.tagline, - style = MaterialTheme.typography.bodyLarge, - color = JetsnackTheme.colors.textHelp, - modifier = Modifier - .padding(horizontal = 16.dp) - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = snackCollectionId.toString(), - type = SnackSharedElementType.Tagline, - ), - ), - animatedVisibilityScope = animatedVisibilityScope, - enter = fadeIn(nonSpatialExpressiveSpring()), - exit = fadeOut(nonSpatialExpressiveSpring()), - boundsTransform = snackDetailBoundsTransform, - resizeMode = SharedTransitionScope.ResizeMode.scaleToBounds(), + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = snack.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = Style { + textStyleWithFontFamilyFix(typography.titleLarge) + contentColor(colors.textSecondary) + textAlign(TextAlign.Start) + } then snackTextStyle, + modifier = Modifier + .padding(horizontal = 16.dp) + .sharedBounds( + rememberSharedContentState( + key = SnackSharedElementKey( + snackId = snack.id, + origin = snackCollectionId.toString(), + type = SnackSharedElementType.Title, + ), + ), + animatedVisibilityScope = animatedVisibilityScope, + enter = fadeIn(nonSpatialExpressiveSpring()), + exit = fadeOut(nonSpatialExpressiveSpring()), + boundsTransform = snackDetailBoundsTransform, + resizeMode = SharedTransitionScope.ResizeMode.scaleToBounds(), + ) + .fillMaxWidth(), ) - .wrapContentWidth(), - ) + if (showTagLine) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = formatPrice(snack.price), + style = { + textStyleWithFontFamilyFix(typography.bodyLarge) + contentColor(colors.textHelp) + }, + modifier = Modifier + .padding(horizontal = 16.dp) + .sharedBounds( + rememberSharedContentState( + key = SnackSharedElementKey( + snackId = snack.id, + origin = snackCollectionId.toString(), + type = SnackSharedElementType.Price, + ), + ), + animatedVisibilityScope = animatedVisibilityScope, + enter = fadeIn(nonSpatialExpressiveSpring()), + exit = fadeOut(nonSpatialExpressiveSpring()), + boundsTransform = snackDetailBoundsTransform, + resizeMode = SharedTransitionScope.ResizeMode.scaleToBounds(), + ) + .wrapContentWidth(), + ) + } + } + if (showAddButton) { + Button( + onClick = { + }, + style = { + shape(CircleShape) + size(36.dp) + background(colors.brand) + dropShadow(Shadow(0.dp)) + contentPadding(8.dp) + externalPadding(8.dp) + }, + modifier = Modifier, + ) { + Icon( + painterResource(R.drawable.ic_add), + modifier = Modifier.size(24.dp), + tint = JetsnackTheme.colors.textSecondary, + contentDescription = stringResource(R.string.add_to_cart_content_description), + ) + } + } + } } } } @@ -451,14 +395,14 @@ fun SnackImage( imageRes: Int, contentDescription: String?, modifier: Modifier = Modifier, - elevation: Dp = 0.dp, + style: Style = Style, ) { - JetsnackSurface( - elevation = elevation, - shape = CircleShape, + Surface( + style = Style { + shape(CircleShape) + } then style, modifier = modifier, ) { - AsyncImage( model = ImageRequest.Builder(LocalContext.current) .data(imageRes) @@ -476,17 +420,56 @@ fun SnackImage( @Preview("dark theme", uiMode = UI_MODE_NIGHT_YES) @Preview("large font", fontScale = 2f) @Composable -fun SnackCardPreview() { +fun SnackCardPreviewCard() { val snack = snacks.first() - JetsnackPreviewWrapper { - HighlightSnackItem( - snackCollectionId = 1, - snack = snack, - onSnackClick = { _, _ -> }, - index = 0, - gradient = JetsnackTheme.colors.gradient6_1, - scrollProvider = { 0f }, - ) + JetsnackThemeWrapper { + UiMediaScopeWrapper { + SnackItem( + snackCollectionId = 1, + snack = snack, + style = JetsnackTheme.styles.normalCardStyle, + showTagLine = false, + onSnackClick = { _, _ -> }, + ) + } + } +} + +@Preview("default") +@Preview("dark theme", uiMode = UI_MODE_NIGHT_YES) +@Preview("large font", fontScale = 2f) +@Composable +fun SnackCardPreviewHighlight() { + val snack = snacks.first() + JetsnackThemeWrapper { + UiMediaScopeWrapper { + SnackItem( + snackCollectionId = 1, + snack = snack, + style = JetsnackTheme.styles.highlightGlowCardStyle, + showAddButton = true, + onSnackClick = { _, _ -> }, + ) + } + } +} + +@Preview("default") +@Preview("dark theme", uiMode = UI_MODE_NIGHT_YES) +@Preview("large font", fontScale = 2f) +@Composable +fun SnackCardPreviewPlain() { + val snack = snacks.first() + JetsnackThemeWrapper { + UiMediaScopeWrapper { + SnackItem( + snackCollectionId = 1, + snack = snack, + showTagLine = false, + style = JetsnackTheme.styles.normalCardStyle, + onSnackClick = { _, _ -> }, + ) + } } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Surface.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Surface.kt index 1104c51f1e..7c0533da90 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Surface.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Surface.kt @@ -14,85 +14,36 @@ * limitations under the License. */ +@file:OptIn(ExperimentalFoundationStyleApi::class) + package com.example.jetsnack.ui.components -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box -import androidx.compose.material3.LocalContentColor +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.style.ExperimentalFoundationStyleApi +import androidx.compose.foundation.style.Style +import androidx.compose.foundation.style.StyleState +import androidx.compose.foundation.style.rememberUpdatedStyleState +import androidx.compose.foundation.style.styleable import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.compositeOver -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex import com.example.jetsnack.ui.theme.JetsnackTheme -import kotlin.math.ln /** * An alternative to [androidx.compose.material3.Surface] utilizing * [com.example.jetsnack.ui.theme.JetsnackColors] */ @Composable -fun JetsnackSurface( +fun Surface( modifier: Modifier = Modifier, - shape: Shape = RectangleShape, - color: Color = JetsnackTheme.colors.uiBackground, - contentColor: Color = JetsnackTheme.colors.textSecondary, - border: BorderStroke? = null, - elevation: Dp = 0.dp, - content: @Composable () -> Unit, + style: Style = Style, + styleState: StyleState = rememberUpdatedStyleState(null), + content: @Composable BoxScope.() -> Unit, ) { Box( modifier = modifier - .shadow(elevation = elevation, shape = shape, clip = false) - .zIndex(elevation.value) - .then(if (border != null) Modifier.border(border, shape) else Modifier) - .background( - color = getBackgroundColorForElevation(color, elevation), - shape = shape, - ) - .clip(shape), - ) { - CompositionLocalProvider(LocalContentColor provides contentColor, content = content) - } -} - -@Composable -private fun getBackgroundColorForElevation(color: Color, elevation: Dp): Color { - return if (elevation > 0.dp // && https://issuetracker.google.com/issues/161429530 - // JetsnackTheme.colors.isDark //&& - // color == JetsnackTheme.colors.uiBackground + .styleable(styleState, JetsnackTheme.styles.surfaceStyle, style), ) { - color.withElevation(elevation) - } else { - color + content() } } - -/** - * Applies a [Color.White] overlay to this color based on the [elevation]. This increases visibility - * of elevation for surfaces in a dark theme. - * - * TODO: Remove when public https://issuetracker.google.com/155181601 - */ -private fun Color.withElevation(elevation: Dp): Color { - val foreground = calculateForeground(elevation) - return foreground.compositeOver(this) -} - -/** - * @return the alpha-modified [Color.White] to overlay on top of the surface color to produce - * the resultant color. - */ -private fun calculateForeground(elevation: Dp): Color { - val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f - return Color.White.copy(alpha = alpha) -} diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Text.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Text.kt new file mode 100644 index 0000000000..b37c4208d0 --- /dev/null +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Text.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.components + +import androidx.compose.foundation.style.ExperimentalFoundationStyleApi +import androidx.compose.foundation.style.Style +import androidx.compose.foundation.style.StyleScope +import androidx.compose.foundation.style.styleable +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.TextAutoSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import com.example.jetsnack.ui.theme.JetsnackTheme + +// Workaround for b/492528450 - setting textStyle currently doesn't set fontFamily. +@ExperimentalFoundationStyleApi +fun StyleScope.textStyleWithFontFamilyFix(value: TextStyle) { + textStyle(value) + value.fontFamily?.let { fontFamily(it) } +} + +@ExperimentalFoundationStyleApi +@Composable +fun Text( + text: String, + modifier: Modifier = Modifier, + style: Style = Style, + onTextLayout: ((TextLayoutResult) -> Unit)? = null, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + autoSize: TextAutoSize? = null, +) { + BasicText( + text = text, + modifier = modifier.styleable(null, JetsnackTheme.styles.defaultTextStyle, style), + onTextLayout = onTextLayout, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + autoSize = autoSize, + ) +} diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/DestinationBar.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/DestinationBar.kt index b706ea8bb3..63d5c3f8d8 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/DestinationBar.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/DestinationBar.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -@file:OptIn(ExperimentalSharedTransitionApi::class) +@file:OptIn(ExperimentalSharedTransitionApi::class, ExperimentalFoundationStyleApi::class) package com.example.jetsnack.ui.home @@ -25,11 +25,11 @@ import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.style.ExperimentalFoundationStyleApi import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -40,14 +40,19 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.example.jetsnack.R import com.example.jetsnack.ui.LocalNavAnimatedVisibilityScope import com.example.jetsnack.ui.LocalSharedTransitionScope import com.example.jetsnack.ui.components.JetsnackDivider import com.example.jetsnack.ui.components.JetsnackPreviewWrapper +import com.example.jetsnack.ui.components.Text +import com.example.jetsnack.ui.components.textStyleWithFontFamilyFix import com.example.jetsnack.ui.snackdetail.spatialExpressiveSpring import com.example.jetsnack.ui.theme.AlphaNearOpaque import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.theme.colors +import com.example.jetsnack.ui.theme.typography @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -67,14 +72,15 @@ fun DestinationBar(modifier: Modifier = Modifier) { ), ) { TopAppBar( - windowInsets = WindowInsets(0, 0, 0, 0), title = { Row { Text( text = "Delivery to 1600 Amphitheater Way", - style = MaterialTheme.typography.titleMedium, - color = JetsnackTheme.colors.textSecondary, - textAlign = TextAlign.Center, + style = { + textStyleWithFontFamilyFix(typography.titleSmall) + contentColor(colors.textPrimary) + textAlign(TextAlign.Center) + }, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier @@ -87,7 +93,7 @@ fun DestinationBar(modifier: Modifier = Modifier) { ) { Icon( painter = painterResource(id = R.drawable.ic_expand_more), - tint = JetsnackTheme.colors.brand, + tint = JetsnackTheme.colors.brandSecondary, contentDescription = stringResource(R.string.label_select_delivery), ) @@ -99,6 +105,7 @@ fun DestinationBar(modifier: Modifier = Modifier) { .copy(alpha = AlphaNearOpaque), titleContentColor = JetsnackTheme.colors.textSecondary, ), + modifier = Modifier.height(48.dp), ) JetsnackDivider() } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Feed.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Feed.kt index 10e3125ce5..3b10fd85a9 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Feed.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Feed.kt @@ -28,9 +28,9 @@ import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsTopHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed @@ -47,8 +47,8 @@ import com.example.jetsnack.model.SnackCollection import com.example.jetsnack.model.SnackRepo import com.example.jetsnack.ui.components.FilterBar import com.example.jetsnack.ui.components.JetsnackDivider -import com.example.jetsnack.ui.components.JetsnackSurface import com.example.jetsnack.ui.components.SnackCollection +import com.example.jetsnack.ui.components.Surface import com.example.jetsnack.ui.theme.JetsnackTheme @Composable @@ -70,7 +70,7 @@ private fun Feed( onSnackClick: (Long, String) -> Unit, modifier: Modifier = Modifier, ) { - JetsnackSurface(modifier = modifier.fillMaxSize()) { + Surface(modifier = modifier.fillMaxSize()) { var filtersVisible by remember { mutableStateOf(false) } @@ -112,7 +112,7 @@ private fun SnackCollectionList( item { Spacer( Modifier.windowInsetsTopHeight( - WindowInsets.statusBars.add(WindowInsets(top = 56.dp)), + WindowInsets.systemBars, ), ) FilterBar( @@ -120,17 +120,19 @@ private fun SnackCollectionList( sharedTransitionScope = sharedTransitionScope, filterScreenVisible = filtersVisible, onShowFilters = onFiltersSelected, + modifier = Modifier.padding(top = 48.dp), ) } itemsIndexed(snackCollections) { index, snackCollection -> if (index > 0) { - JetsnackDivider(thickness = 2.dp) + JetsnackDivider(style = { + height(1.dp) + }) } SnackCollection( snackCollection = snackCollection, onSnackClick = onSnackClick, - index = index, ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/FilterScreen.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/FilterScreen.kt index f2c0dcabc4..efe286b9ad 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/FilterScreen.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/FilterScreen.kt @@ -14,10 +14,11 @@ * limitations under the License. */ -@file:OptIn(ExperimentalLayoutApi::class, ExperimentalSharedTransitionApi::class) +@file:OptIn(ExperimentalLayoutApi::class, ExperimentalSharedTransitionApi::class, ExperimentalMaterial3Api::class) package com.example.jetsnack.ui.home +import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope @@ -43,13 +44,16 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.style.Style +import androidx.compose.foundation.style.styleable import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider +import androidx.compose.material3.SliderColors import androidx.compose.material3.SliderDefaults -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -59,6 +63,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -71,7 +76,11 @@ import com.example.jetsnack.model.Filter import com.example.jetsnack.model.SnackRepo import com.example.jetsnack.ui.FilterSharedElementKey import com.example.jetsnack.ui.components.FilterChip +import com.example.jetsnack.ui.components.Text +import com.example.jetsnack.ui.components.textStyleWithFontFamilyFix import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.theme.colors +import com.example.jetsnack.ui.theme.typography @Composable fun FilterScreen(sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope, onDismiss: () -> Unit) { @@ -131,6 +140,7 @@ fun FilterScreen(sharedTransitionScope: SharedTransitionScope, animatedVisibilit Icon( painter = painterResource(id = R.drawable.ic_close), contentDescription = stringResource(id = R.string.close), + tint = JetsnackTheme.colors.textInteractive, ) } Text( @@ -139,8 +149,10 @@ fun FilterScreen(sharedTransitionScope: SharedTransitionScope, animatedVisibilit .fillMaxWidth() .fillMaxHeight() .padding(top = 8.dp, end = 48.dp), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge, + style = { + textAlign(TextAlign.Center) + textStyleWithFontFamilyFix(typography.titleLarge) + }, ) val resetEnabled = sortState != defaultFilter @@ -156,10 +168,14 @@ fun FilterScreen(sharedTransitionScope: SharedTransitionScope, animatedVisibilit Text( text = stringResource(id = R.string.reset), - style = MaterialTheme.typography.bodyMedium, - fontWeight = fontWeight, - color = JetsnackTheme.colors.uiBackground - .copy(alpha = if (!resetEnabled) 0.38f else 1f), + style = { + textStyleWithFontFamilyFix(typography.bodyMedium) + fontWeight(fontWeight) + contentColor( + colors.uiBackground + .copy(alpha = if (!resetEnabled) 0.38f else 1f), + ) + }, ) } } @@ -245,25 +261,37 @@ fun MaxCalories(sliderPosition: Float, onValueChanged: (Float) -> Unit) { FilterTitle(text = stringResource(id = R.string.max_calories)) Text( text = stringResource(id = R.string.per_serving), - style = MaterialTheme.typography.bodyMedium, - color = JetsnackTheme.colors.brand, + style = { + textStyleWithFontFamilyFix(typography.bodyMedium) + contentColor(colors.textSecondary) + }, modifier = Modifier.padding(top = 5.dp, start = 10.dp), ) } + val interactionSource = remember { MutableInteractionSource() } Slider( value = sliderPosition, onValueChange = { newValue -> onValueChanged(newValue) }, + thumb = { + SliderDefaults.Thumb( + interactionSource = interactionSource, + colors = SliderDefaults.colors(thumbColor = JetsnackTheme.colors.brand), + enabled = true, + ) + }, + track = { + Box( + modifier = Modifier.fillMaxWidth().height(4.dp).styleable(null) { + background(Brush.horizontalGradient(colors.gradient1)) + }, + ) + }, valueRange = 0f..300f, steps = 5, modifier = Modifier .fillMaxWidth(), - colors = SliderDefaults.colors( - thumbColor = JetsnackTheme.colors.brand, - activeTrackColor = JetsnackTheme.colors.brand, - inactiveTrackColor = JetsnackTheme.colors.iconInteractive, - ), ) } @@ -271,8 +299,10 @@ fun MaxCalories(sliderPosition: Float, onValueChanged: (Float) -> Unit) { fun FilterTitle(text: String) { Text( text = text, - style = MaterialTheme.typography.titleLarge, - color = JetsnackTheme.colors.brand, + style = { + textStyleWithFontFamilyFix(typography.titleLarge) + contentColor(colors.textPrimary) + }, modifier = Modifier.padding(bottom = 8.dp), ) } @@ -285,11 +315,18 @@ fun SortOption(text: String, @DrawableRes icon: Int?, onClickOption: () -> Unit, .selectable(selected) { onClickOption() }, ) { if (icon != null) { - Icon(painter = painterResource(id = icon), contentDescription = null) + Icon( + painter = painterResource(id = icon), + contentDescription = null, + tint = JetsnackTheme.colors.textInteractive, + ) } Text( text = text, - style = MaterialTheme.typography.titleMedium, + style = { + contentColor(colors.textInteractive) + textStyleWithFontFamilyFix(typography.titleMedium) + }, modifier = Modifier .padding(start = 10.dp) .weight(1f), @@ -298,13 +335,14 @@ fun SortOption(text: String, @DrawableRes icon: Int?, onClickOption: () -> Unit, Icon( painter = painterResource(id = R.drawable.ic_check), contentDescription = null, - tint = JetsnackTheme.colors.brand, + tint = JetsnackTheme.colors.textInteractive, ) } } } @Preview("filter screen") +@Preview(uiMode = UI_MODE_NIGHT_YES) @Composable fun FilterScreenPreview() { JetsnackTheme { diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt index 28d1f4b927..4a922c53d3 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMediaQueryApi::class) + package com.example.jetsnack.ui.home import androidx.annotation.DrawableRes @@ -29,7 +31,6 @@ import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Spacer @@ -37,21 +38,22 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.style.styleable import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalMediaQueryApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.Layout @@ -59,11 +61,12 @@ import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.mediaQuery import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalLocale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp import androidx.core.os.ConfigurationCompat @@ -75,12 +78,16 @@ import androidx.navigation.compose.composable import androidx.navigation.navDeepLink import com.example.jetsnack.R import com.example.jetsnack.ui.LocalNavAnimatedVisibilityScope -import com.example.jetsnack.ui.components.JetsnackSurface +import com.example.jetsnack.ui.components.Surface +import com.example.jetsnack.ui.components.Text +import com.example.jetsnack.ui.components.textStyleWithFontFamilyFix import com.example.jetsnack.ui.home.cart.Cart import com.example.jetsnack.ui.home.search.Search import com.example.jetsnack.ui.snackdetail.nonSpatialExpressiveSpring import com.example.jetsnack.ui.snackdetail.spatialExpressiveSpring import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.theme.colors +import com.example.jetsnack.ui.theme.typography import java.util.Locale fun NavGraphBuilder.composableWithCompositionLocal( @@ -166,16 +173,21 @@ fun JetsnackBottomBar( currentRoute: String, navigateToRoute: (String) -> Unit, modifier: Modifier = Modifier, - color: Color = JetsnackTheme.colors.iconPrimary, - contentColor: Color = JetsnackTheme.colors.iconInteractive, + color: Color = JetsnackTheme.colors.brandLight, + contentColor: Color = JetsnackTheme.colors.iconPrimary, ) { val routes = remember { tabs.map { it.route } } val currentSection = tabs.first { it.route == currentRoute } - JetsnackSurface( + Surface( modifier = modifier, - color = color, - contentColor = contentColor, + style = { + background(color) + contentColor(contentColor) + if (mediaQuery { windowWidth > 600.dp }) { + contentPaddingHorizontal(100.dp) + } + }, ) { val springSpec = spatialExpressiveSpring() JetsnackBottomNavLayout( @@ -187,15 +199,15 @@ fun JetsnackBottomBar( ) { val configuration = LocalConfiguration.current val currentLocale: Locale = - ConfigurationCompat.getLocales(configuration).get(0) ?: Locale.getDefault() + ConfigurationCompat.getLocales(configuration).get(0) ?: LocalLocale.current.platformLocale tabs.forEach { section -> val selected = section == currentSection val tint by animateColorAsState( if (selected) { - JetsnackTheme.colors.iconInteractive - } else { JetsnackTheme.colors.iconInteractiveInactive + } else { + JetsnackTheme.colors.iconInteractiveInactive.copy(alpha = 0.9f) }, label = "tint", ) @@ -213,15 +225,18 @@ fun JetsnackBottomBar( text = { Text( text = text, - color = tint, - style = MaterialTheme.typography.labelLarge, + style = { + contentColor(tint) + textStyleWithFontFamilyFix(typography.labelLarge) + }, maxLines = 1, ) }, selected = selected, onSelected = { navigateToRoute(section.route) }, animSpec = springSpec, - modifier = BottomNavigationItemPadding + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) .clip(BottomNavIndicatorShape), ) } @@ -397,16 +412,15 @@ private fun MeasureScope.placeTextAndIcon( } @Composable -private fun JetsnackBottomNavIndicator( - strokeWidth: Dp = 2.dp, - color: Color = JetsnackTheme.colors.iconInteractive, - shape: Shape = BottomNavIndicatorShape, -) { +private fun JetsnackBottomNavIndicator() { Spacer( modifier = Modifier .fillMaxSize() - .then(BottomNavigationItemPadding) - .border(strokeWidth, color, shape), + .styleable(null) { + shape(BottomNavIndicatorShape) + border(3.dp, Brush.linearGradient(colors.interactiveMask)) + externalPadding(horizontal = 16.dp, vertical = 8.dp) + }, ) } @@ -414,7 +428,6 @@ private val TextIconSpacing = 2.dp private val BottomNavHeight = 56.dp private val BottomNavLabelTransformOrigin = TransformOrigin(0f, 0.5f) private val BottomNavIndicatorShape = RoundedCornerShape(percent = 50) -private val BottomNavigationItemPadding = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) @Preview @Composable diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Profile.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Profile.kt index 799fa28f3e..33e5d8ca83 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Profile.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Profile.kt @@ -25,8 +25,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -36,7 +34,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.example.jetsnack.R +import com.example.jetsnack.ui.components.Text +import com.example.jetsnack.ui.components.textStyleWithFontFamilyFix import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.theme.colors +import com.example.jetsnack.ui.theme.shapes +import com.example.jetsnack.ui.theme.typography @Composable fun Profile(modifier: Modifier = Modifier) { @@ -54,15 +57,19 @@ fun Profile(modifier: Modifier = Modifier) { Spacer(Modifier.height(24.dp)) Text( text = stringResource(R.string.work_in_progress), - style = MaterialTheme.typography.titleMedium, - textAlign = TextAlign.Center, + style = { + textStyleWithFontFamilyFix(typography.titleMedium) + textAlign(TextAlign.Center) + }, modifier = Modifier.fillMaxWidth(), ) Spacer(Modifier.height(16.dp)) Text( text = stringResource(R.string.grab_beverage), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, + style = { + textStyleWithFontFamilyFix(typography.bodyMedium) + textAlign(TextAlign.Center) + }, modifier = Modifier.fillMaxWidth(), ) } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/Cart.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/Cart.kt index 6268546131..80c4108c47 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/Cart.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/Cart.kt @@ -14,16 +14,19 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMediaQueryApi::class) + package com.example.jetsnack.ui.home.cart import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.util.Log import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState 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 import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -34,9 +37,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsTopHeight import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth @@ -45,18 +50,19 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.Text +import androidx.compose.material3.SwipeToDismissBoxState +import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalMediaQueryApi import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.LastBaseline -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -71,19 +77,27 @@ import com.example.jetsnack.R import com.example.jetsnack.model.OrderLine import com.example.jetsnack.model.SnackCollection import com.example.jetsnack.model.SnackRepo -import com.example.jetsnack.ui.components.JetsnackButton +import com.example.jetsnack.ui.components.Button import com.example.jetsnack.ui.components.JetsnackDivider -import com.example.jetsnack.ui.components.JetsnackSurface import com.example.jetsnack.ui.components.QuantitySelector import com.example.jetsnack.ui.components.SnackCollection import com.example.jetsnack.ui.components.SnackImage +import com.example.jetsnack.ui.components.Text +import com.example.jetsnack.ui.components.textStyleWithFontFamilyFix import com.example.jetsnack.ui.home.DestinationBar import com.example.jetsnack.ui.snackdetail.nonSpatialExpressiveSpring import com.example.jetsnack.ui.snackdetail.spatialExpressiveSpring import com.example.jetsnack.ui.theme.AlphaNearOpaque import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.theme.colors +import com.example.jetsnack.ui.theme.shapes +import com.example.jetsnack.ui.theme.typography +import com.example.jetsnack.ui.utils.JetsnackThemeWrapper +import com.example.jetsnack.ui.utils.UiMediaScopeWrapper import com.example.jetsnack.ui.utils.formatPrice +import kotlin.math.abs import kotlin.math.roundToInt +import androidx.compose.foundation.layout.BoxWithConstraints @Composable fun Cart( @@ -114,20 +128,20 @@ fun Cart( onSnackClick: (Long, String) -> Unit, modifier: Modifier = Modifier, ) { - JetsnackSurface(modifier = modifier.fillMaxSize()) { - Box(modifier = Modifier.fillMaxSize()) { - CartContent( - orderLines = orderLines, - removeSnack = removeSnack, - increaseItemCount = increaseItemCount, - decreaseItemCount = decreaseItemCount, - inspiredByCart = inspiredByCart, - onSnackClick = onSnackClick, - modifier = Modifier.align(Alignment.TopCenter), - ) - DestinationBar(modifier = Modifier.align(Alignment.TopCenter)) - CheckoutBar(modifier = Modifier.align(Alignment.BottomCenter)) - } + com.example.jetsnack.ui.components.Surface( + modifier = modifier.fillMaxSize(), + ) { + CartContent( + orderLines = orderLines, + removeSnack = removeSnack, + increaseItemCount = increaseItemCount, + decreaseItemCount = decreaseItemCount, + inspiredByCart = inspiredByCart, + onSnackClick = onSnackClick, + modifier = Modifier.align(Alignment.TopCenter), + ) + DestinationBar(modifier = Modifier.align(Alignment.TopCenter)) + CheckoutBar(modifier = Modifier.align(Alignment.BottomCenter)) } } @@ -141,7 +155,7 @@ private fun CartContent( onSnackClick: (Long, String) -> Unit, modifier: Modifier = Modifier, ) { - val resources = LocalContext.current.resources + val resources = LocalResources.current val snackCountFormattedString = remember(orderLines.size, resources) { resources.getQuantityString( R.plurals.cart_order_count, @@ -154,30 +168,34 @@ private fun CartContent( item(key = "title") { Spacer( Modifier.windowInsetsTopHeight( - WindowInsets.statusBars.add(WindowInsets(top = 56.dp)), + WindowInsets.systemBars.add(WindowInsets(top = 56.dp)), ), ) Text( text = stringResource(R.string.cart_order_header, snackCountFormattedString), - style = MaterialTheme.typography.titleLarge, - color = JetsnackTheme.colors.brand, + style = { + textStyleWithFontFamilyFix(typography.titleLarge) + contentColor(colors.textPrimary) + }, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier .heightIn(min = 56.dp) - .padding(horizontal = 24.dp, vertical = 4.dp) + .padding(horizontal = 24.dp) .wrapContentHeight(), ) } items(orderLines, key = { it.snack.id }) { orderLine -> + val swipeDismissState = rememberSwipeToDismissBoxState() SwipeDismissItem( + dismissState = swipeDismissState, modifier = Modifier.animateItem( fadeInSpec = itemAnimationSpecFade, fadeOutSpec = itemAnimationSpecFade, placementSpec = itemPlacementSpec, ), - background = { progress -> - SwipeDismissItemBackground(progress) + background = { + SwipeDismissItemBackground(swipeDismissState) }, ) { CartItem( @@ -209,7 +227,6 @@ private fun CartContent( ), snackCollection = inspiredByCart, onSnackClick = onSnackClick, - highlight = false, ) Spacer(Modifier.height(56.dp)) } @@ -217,38 +234,51 @@ private fun CartContent( } @Composable -private fun SwipeDismissItemBackground(progress: Float) { - Column( +private fun SwipeDismissItemBackground(swipeDismissState: SwipeToDismissBoxState) { + BoxWithConstraints( modifier = Modifier .background(JetsnackTheme.colors.uiBackground) - .fillMaxWidth() - .fillMaxHeight(), - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.Center, + .fillMaxSize(), ) { - // Set 4.dp padding only if progress is less than halfway - val padding: Dp by animateDpAsState( - if (progress < 0.5f) 4.dp else 0.dp, label = "padding", - ) - BoxWithConstraints( - Modifier - .fillMaxWidth(progress), + val progress = remember(swipeDismissState, constraints.maxWidth) { + derivedStateOf { + if (constraints.maxWidth > 0) { + val offset = try { + swipeDismissState.requireOffset() + } catch ( _ : IllegalStateException) { + 0f + } + (abs(offset) / constraints.maxWidth).coerceIn(0f, 1f) + } else { + 0f + } + } + }.value + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.Center, ) { + // Set 4.dp padding only if progress is less than halfway + val padding: Dp by animateDpAsState( + if (progress < 0.5f) 4.dp else 0.dp, label = "padding", + ) + Surface( modifier = Modifier - .padding(padding) - .fillMaxWidth() - .height(maxWidth) - .align(Alignment.Center), + .fillMaxHeight() + .fillMaxWidth(progress) + .padding(padding), shape = RoundedCornerShape(percent = ((1 - progress) * 100).roundToInt()), color = JetsnackTheme.colors.error, ) { Box( - modifier = Modifier.fillMaxSize(), + modifier = Modifier, contentAlignment = Alignment.Center, ) { // Icon must be visible while in this width range - if (progress in 0.125f..0.475f) { + if (progress in 0.125f..0.4f) { // Icon alpha decreases as it is about to disappear val iconAlpha: Float by animateFloatAsState( if (progress > 0.4f) 0.5f else 1f, label = "icon alpha", @@ -257,23 +287,25 @@ private fun SwipeDismissItemBackground(progress: Float) { Icon( painter = painterResource(id = R.drawable.ic_delete_forever), modifier = Modifier - .size(32.dp) + .size(28.dp) .graphicsLayer(alpha = iconAlpha), tint = JetsnackTheme.colors.uiBackground, contentDescription = null, ) } /*Text opacity increases as the text is supposed to appear in - the screen*/ + the screen*/ val textAlpha by animateFloatAsState( - if (progress > 0.5f) 1f else 0.5f, label = "text alpha", + if (progress > 0.4f) 1f else 0.5f, label = "text alpha", ) - if (progress > 0.5f) { + if (progress > 0.40f) { Text( text = stringResource(id = R.string.remove_item), - style = MaterialTheme.typography.titleMedium, - color = JetsnackTheme.colors.uiBackground, - textAlign = TextAlign.Center, + style = { + textStyleWithFontFamilyFix(typography.titleMedium) + contentColor(colors.uiBackground) + textAlign(TextAlign.Center) + }, modifier = Modifier .graphicsLayer( alpha = textAlpha, @@ -301,7 +333,7 @@ fun CartItem( .fillMaxWidth() .clickable { onSnackClick(snack.id, "cart") } .background(JetsnackTheme.colors.uiBackground) - .padding(horizontal = 24.dp), + .padding(horizontal = 20.dp), ) { Row( modifier = Modifier.fillMaxWidth(), @@ -309,27 +341,30 @@ fun CartItem( SnackImage( imageRes = snack.imageRes, contentDescription = null, - modifier = Modifier - .padding(vertical = 16.dp) - .size(100.dp), + style = { + shape(shapes.small) + externalPaddingVertical(8.dp) + size(100.dp) + }, ) Column( modifier = Modifier - .weight(1f) .padding(start = 16.dp), ) { Row(modifier = Modifier.fillMaxWidth()) { Text( text = snack.name, - style = MaterialTheme.typography.titleMedium, - color = JetsnackTheme.colors.textSecondary, + style = { + textStyleWithFontFamilyFix(typography.bodyLarge) + contentColor(colors.textSecondary) + }, modifier = Modifier .weight(1f) .padding(top = 16.dp, end = 16.dp), ) IconButton( onClick = { removeSnack(snack.id) }, - modifier = Modifier.padding(top = 12.dp), + modifier = Modifier.offset(x = 12.dp), ) { Icon( painter = painterResource(id = R.drawable.ic_close), @@ -340,8 +375,10 @@ fun CartItem( } Text( text = snack.tagline, - style = MaterialTheme.typography.bodyLarge, - color = JetsnackTheme.colors.textHelp, + style = { + textStyleWithFontFamilyFix(typography.bodySmall) + contentColor(colors.textHelp) + }, modifier = Modifier.padding(end = 16.dp), ) Spacer(Modifier.height(8.dp)) @@ -350,8 +387,10 @@ fun CartItem( ) { Text( text = formatPrice(snack.price), - style = MaterialTheme.typography.titleMedium, - color = JetsnackTheme.colors.textPrimary, + style = { + textStyleWithFontFamilyFix(typography.titleMedium) + contentColor(colors.textPrimary) + }, modifier = Modifier .weight(1f) .padding(end = 16.dp) @@ -375,8 +414,10 @@ fun SummaryItem(subtotal: Long, shippingCosts: Long, modifier: Modifier = Modifi Column(modifier) { Text( text = stringResource(R.string.cart_summary_header), - style = MaterialTheme.typography.titleLarge, - color = JetsnackTheme.colors.brand, + style = { + textStyleWithFontFamilyFix(typography.titleLarge) + contentColor(colors.textPrimary) + }, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier @@ -387,7 +428,9 @@ fun SummaryItem(subtotal: Long, shippingCosts: Long, modifier: Modifier = Modifi Row(modifier = Modifier.padding(horizontal = 24.dp)) { Text( text = stringResource(R.string.cart_subtotal_label), - style = MaterialTheme.typography.bodyLarge, + style = { + textStyleWithFontFamilyFix(typography.bodyLarge) + }, modifier = Modifier .weight(1f) .wrapContentWidth(Alignment.Start) @@ -395,14 +438,18 @@ fun SummaryItem(subtotal: Long, shippingCosts: Long, modifier: Modifier = Modifi ) Text( text = formatPrice(subtotal), - style = MaterialTheme.typography.bodyLarge, + style = { + textStyleWithFontFamilyFix(typography.bodyLarge) + }, modifier = Modifier.alignBy(LastBaseline), ) } Row(modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) { Text( text = stringResource(R.string.cart_shipping_label), - style = MaterialTheme.typography.bodyLarge, + style = { + textStyleWithFontFamilyFix(typography.bodyLarge) + }, modifier = Modifier .weight(1f) .wrapContentWidth(Alignment.Start) @@ -410,7 +457,9 @@ fun SummaryItem(subtotal: Long, shippingCosts: Long, modifier: Modifier = Modifi ) Text( text = formatPrice(shippingCosts), - style = MaterialTheme.typography.bodyLarge, + style = { + textStyleWithFontFamilyFix(typography.bodyLarge) + }, modifier = Modifier.alignBy(LastBaseline), ) } @@ -419,7 +468,9 @@ fun SummaryItem(subtotal: Long, shippingCosts: Long, modifier: Modifier = Modifi Row(modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) { Text( text = stringResource(R.string.cart_total_label), - style = MaterialTheme.typography.bodyLarge, + style = { + textStyleWithFontFamilyFix(typography.bodyLarge) + }, modifier = Modifier .weight(1f) .padding(end = 16.dp) @@ -428,7 +479,9 @@ fun SummaryItem(subtotal: Long, shippingCosts: Long, modifier: Modifier = Modifi ) Text( text = formatPrice(subtotal + shippingCosts), - style = MaterialTheme.typography.titleMedium, + style = { + textStyleWithFontFamilyFix(typography.titleMedium) + }, modifier = Modifier.alignBy(LastBaseline), ) } @@ -439,25 +492,35 @@ fun SummaryItem(subtotal: Long, shippingCosts: Long, modifier: Modifier = Modifi @Composable private fun CheckoutBar(modifier: Modifier = Modifier) { Column( - modifier.background( - JetsnackTheme.colors.uiBackground.copy(alpha = AlphaNearOpaque), - ), + modifier + .background( + JetsnackTheme.colors.uiBackground.copy(alpha = AlphaNearOpaque), + ) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { }, + ), ) { JetsnackDivider() Row { Spacer(Modifier.weight(1f)) - JetsnackButton( + Button( onClick = { /* todo */ }, - shape = RectangleShape, + style = { + shape(RoundedCornerShape(4.dp)) + }, modifier = Modifier - .padding(horizontal = 12.dp, vertical = 8.dp) - .weight(1f), + .widthIn(max = 200.dp) + .padding(horizontal = 12.dp, vertical = 8.dp), ) { Text( text = stringResource(id = R.string.cart_checkout), modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Left, + style = { + textAlign(TextAlign.Left) + }, maxLines = 1, ) } @@ -470,14 +533,16 @@ private fun CheckoutBar(modifier: Modifier = Modifier) { @Preview("large font", fontScale = 2f) @Composable private fun CartPreview() { - JetsnackTheme { - Cart( - orderLines = SnackRepo.getCart(), - removeSnack = {}, - increaseItemCount = {}, - decreaseItemCount = {}, - inspiredByCart = SnackRepo.getInspiredByCart(), - onSnackClick = { _, _ -> }, - ) + JetsnackThemeWrapper { + UiMediaScopeWrapper { + Cart( + orderLines = SnackRepo.getCart(), + removeSnack = {}, + increaseItemCount = {}, + decreaseItemCount = {}, + inspiredByCart = SnackRepo.getInspiredByCart(), + onSnackClick = { _, _ -> }, + ) + } } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/SwipeDismissItem.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/SwipeDismissItem.kt index 3e806fdc50..74d463a929 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/SwipeDismissItem.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/SwipeDismissItem.kt @@ -23,6 +23,7 @@ import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxState import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable @@ -35,13 +36,12 @@ import androidx.compose.ui.Modifier @Composable fun SwipeDismissItem( modifier: Modifier = Modifier, + dismissState: SwipeToDismissBoxState = rememberSwipeToDismissBoxState(), enter: EnterTransition = expandVertically(), exit: ExitTransition = shrinkVertically(), - background: @Composable (progress: Float) -> Unit, + background: @Composable () -> Unit, content: @Composable (isDismissed: Boolean) -> Unit, ) { - // Hold the current state from the Swipe to Dismiss composable - val dismissState = rememberSwipeToDismissBoxState() // Boolean value used for hiding the item if the current state is dismissed val isDismissed = dismissState.currentValue == SwipeToDismissBoxValue.EndToStart @@ -55,7 +55,7 @@ fun SwipeDismissItem( modifier = modifier, state = dismissState, enableDismissFromStartToEnd = false, - backgroundContent = { background(dismissState.progress) }, + backgroundContent = { background() }, content = { content(isDismissed) }, ) } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Categories.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Categories.kt index dba32a0023..85eefa5595 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Categories.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Categories.kt @@ -14,31 +14,50 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMediaQueryApi::class) + package com.example.jetsnack.ui.home.search import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.style.Style +import androidx.compose.foundation.style.rememberUpdatedStyleState +import androidx.compose.foundation.style.styleable +import androidx.compose.foundation.style.then import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalMediaQueryApi +import androidx.compose.ui.LocalUiMediaScope import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.shadow.Shadow import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.VerticalAlignmentLine +import androidx.compose.ui.mediaQuery import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp @@ -46,100 +65,105 @@ import com.example.jetsnack.R import com.example.jetsnack.model.SearchCategory import com.example.jetsnack.model.SearchCategoryCollection import com.example.jetsnack.ui.components.SnackImage -import com.example.jetsnack.ui.components.VerticalGrid +import com.example.jetsnack.ui.components.Text +import com.example.jetsnack.ui.components.textStyleWithFontFamilyFix import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.theme.colors +import com.example.jetsnack.ui.theme.typography import kotlin.math.max @Composable fun SearchCategories(categories: List) { - LazyColumn { - itemsIndexed(categories) { index, collection -> - SearchCategoryCollection(collection, index) - } + val itemSize = if (mediaQuery { windowWidth > 600.dp }) { + 250.dp + } else { + 150.dp } - Spacer(Modifier.height(8.dp)) -} -@Composable -private fun SearchCategoryCollection(collection: SearchCategoryCollection, index: Int, modifier: Modifier = Modifier) { - Column(modifier) { - Text( - text = collection.name, - style = MaterialTheme.typography.titleLarge, - color = JetsnackTheme.colors.textPrimary, - modifier = Modifier - .heightIn(min = 56.dp) - .padding(horizontal = 24.dp, vertical = 4.dp) - .wrapContentHeight(), - ) - VerticalGrid(Modifier.padding(horizontal = 16.dp)) { - val gradient = when (index % 2) { - 0 -> JetsnackTheme.colors.gradient2_2 - else -> JetsnackTheme.colors.gradient2_3 + LazyVerticalGrid( + columns = GridCells.Adaptive(itemSize), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp), + ) { + categories.forEachIndexed { index, collection -> + item(span = { GridItemSpan(maxLineSpan) }) { + Text( + text = collection.name, + style = { + textStyleWithFontFamilyFix(typography.titleLarge) + contentColor(colors.textSecondary) + }, + modifier = Modifier + .heightIn(min = 56.dp) + .padding(horizontal = 8.dp, vertical = 4.dp) + .wrapContentHeight(), + ) + } + val borderColor = when (index % 2) { + 0 -> Color(0xFF8BDEBE) + else -> Color(0xffFFC8A4) } - collection.categories.forEach { category -> + items(collection.categories, key = { + it.name + }) { category -> SearchCategory( category = category, - gradient = gradient, - modifier = Modifier.padding(8.dp), + modifier = Modifier.padding(4.dp), + style = { + border(1.dp, borderColor) + }, ) } } - Spacer(Modifier.height(4.dp)) } } private val MinImageSize = 134.dp -private val CategoryShape = RoundedCornerShape(10.dp) +private val CategoryShape = RoundedCornerShape(24.dp) private const val CategoryTextProportion = 0.55f @Composable -private fun SearchCategory(category: SearchCategory, gradient: List, modifier: Modifier = Modifier) { - Layout( +private fun SearchCategory(category: SearchCategory, modifier: Modifier = Modifier, style: Style = Style) { + val interactionSource = remember { MutableInteractionSource() } + val styleState = rememberUpdatedStyleState(interactionSource) + Row( + verticalAlignment = Alignment.CenterVertically, modifier = modifier - .aspectRatio(1.45f) - .shadow(elevation = 3.dp, shape = CategoryShape) - .clip(CategoryShape) - .background(Brush.horizontalGradient(gradient)) - .clickable { /* todo */ }, - content = { - Text( - text = category.name, - style = MaterialTheme.typography.titleMedium, - color = JetsnackTheme.colors.textSecondary, - modifier = Modifier - .padding(4.dp) - .padding(start = 8.dp), + .aspectRatio(1.85f) + .styleable( + styleState, + Style { + shape(CategoryShape) + clip(true) + background(colors.uiBackground) + dropShadow(Shadow(color = Color(0xffE5E1E2), radius = 8.dp)) + } then style, ) - SnackImage( - imageRes = category.imageRes, - contentDescription = null, - modifier = Modifier.fillMaxSize(), - ) - }, - ) { measurables, constraints -> - // Text given a set proportion of width (which is determined by the aspect ratio) - val textWidth = (constraints.maxWidth * CategoryTextProportion).toInt() - val textPlaceable = measurables[0].measure(Constraints.fixedWidth(textWidth)) - - // Image is sized to the larger of height of item, or a minimum value - // i.e. may appear larger than item (but clipped to the item bounds) - val imageSize = max(MinImageSize.roundToPx(), constraints.maxHeight) - val imagePlaceable = measurables[1].measure(Constraints.fixed(imageSize, imageSize)) - layout( - width = constraints.maxWidth, - height = constraints.minHeight, - ) { - textPlaceable.placeRelative( - x = 0, - y = (constraints.maxHeight - textPlaceable.height) / 2, // centered - ) - imagePlaceable.placeRelative( - // image is placed to end of text i.e. will overflow to the end (but be clipped) - x = textWidth, - y = (constraints.maxHeight - imagePlaceable.height) / 2, // centered - ) - } + .clickable( + interactionSource = interactionSource, + indication = ripple(), + ) { /* todo */ }, + ) { + Text( + text = category.name, + style = { + textStyleWithFontFamilyFix(typography.titleSmall) + contentColor(colors.textPrimary) + }, + modifier = Modifier + .padding(4.dp) + .padding(start = 8.dp) + .weight(1f, fill = true), + ) + SnackImage( + imageRes = category.imageRes, + contentDescription = null, + style = { + shape(RoundedCornerShape(topStartPercent = 48)) + }, + modifier = Modifier.fillMaxSize() + .weight(1f) + .defaultMinSize(minWidth = MinImageSize), + ) } } @@ -154,7 +178,9 @@ private fun SearchCategoryPreview() { name = "Desserts", imageRes = R.drawable.desserts, ), - gradient = JetsnackTheme.colors.gradient3_2, + style = { + border(1.dp, Color(0xFF8BDEBE)) + }, ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Results.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Results.kt index 05aebc129f..8a7b3217a0 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Results.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Results.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -34,8 +33,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -47,11 +44,15 @@ import androidx.compose.ui.unit.dp import com.example.jetsnack.R import com.example.jetsnack.model.Snack import com.example.jetsnack.model.snacks -import com.example.jetsnack.ui.components.JetsnackButton +import com.example.jetsnack.ui.components.Button import com.example.jetsnack.ui.components.JetsnackDivider -import com.example.jetsnack.ui.components.JetsnackSurface import com.example.jetsnack.ui.components.SnackImage +import com.example.jetsnack.ui.components.Surface +import com.example.jetsnack.ui.components.Text +import com.example.jetsnack.ui.components.textStyleWithFontFamilyFix import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.theme.colors +import com.example.jetsnack.ui.theme.typography import com.example.jetsnack.ui.utils.formatPrice @Composable @@ -59,8 +60,10 @@ fun SearchResults(searchResults: List, onSnackClick: (Long, String) -> Un Column { Text( text = stringResource(R.string.search_count, searchResults.size), - style = MaterialTheme.typography.titleLarge, - color = JetsnackTheme.colors.textPrimary, + style = { + textStyleWithFontFamilyFix(typography.titleLarge) + contentColor(colors.textPrimary) + }, modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp), ) LazyColumn { @@ -100,25 +103,33 @@ private fun SearchResult(snack: Snack, onSnackClick: (Long, String) -> Unit, sho ) { Text( text = snack.name, - style = MaterialTheme.typography.titleMedium, - color = JetsnackTheme.colors.textSecondary, + style = { + textStyleWithFontFamilyFix(typography.titleMedium) + contentColor(colors.textSecondary) + }, ) Text( text = snack.tagline, - style = MaterialTheme.typography.bodyLarge, - color = JetsnackTheme.colors.textHelp, + style = { + textStyleWithFontFamilyFix(typography.bodyLarge) + contentColor(colors.textHelp) + }, ) Spacer(Modifier.height(8.dp)) Text( text = formatPrice(snack.price), - style = MaterialTheme.typography.titleMedium, - color = JetsnackTheme.colors.textPrimary, + style = { + textStyleWithFontFamilyFix(typography.titleMedium) + contentColor(colors.textPrimary) + }, ) } - JetsnackButton( + Button( onClick = { /* todo */ }, - shape = CircleShape, - contentPadding = PaddingValues(0.dp), + style = { + shape(CircleShape) + contentPadding(0.dp) + }, modifier = Modifier.size(36.dp), ) { Icon( @@ -146,15 +157,19 @@ fun NoResults(query: String, modifier: Modifier = Modifier) { Spacer(Modifier.height(24.dp)) Text( text = stringResource(R.string.search_no_matches, query), - style = MaterialTheme.typography.titleMedium, - textAlign = TextAlign.Center, + style = { + textStyleWithFontFamilyFix(typography.titleMedium) + textAlign(TextAlign.Center) + }, modifier = Modifier.fillMaxWidth(), ) Spacer(Modifier.height(16.dp)) Text( text = stringResource(R.string.search_no_matches_retry), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, + style = { + textStyleWithFontFamilyFix(typography.bodyMedium) + textAlign(TextAlign.Center) + }, modifier = Modifier.fillMaxWidth(), ) } @@ -166,7 +181,7 @@ fun NoResults(query: String, modifier: Modifier = Modifier) { @Composable private fun SearchResultPreview() { JetsnackTheme { - JetsnackSurface { + Surface { SearchResult( snack = snacks[0], onSnackClick = { _, _ -> }, diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt index 33e84c51e7..23d6559c3a 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt @@ -34,8 +34,6 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable @@ -59,12 +57,17 @@ import com.example.jetsnack.model.SearchSuggestionGroup import com.example.jetsnack.model.Snack import com.example.jetsnack.model.SnackRepo import com.example.jetsnack.ui.components.JetsnackDivider -import com.example.jetsnack.ui.components.JetsnackSurface +import com.example.jetsnack.ui.components.Surface +import com.example.jetsnack.ui.components.Text +import com.example.jetsnack.ui.components.textStyleWithFontFamilyFix import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.theme.colors +import com.example.jetsnack.ui.theme.shapes +import com.example.jetsnack.ui.theme.typography @Composable fun Search(onSnackClick: (Long, String) -> Unit, modifier: Modifier = Modifier, state: SearchState = rememberSearchState()) { - JetsnackSurface(modifier = modifier.fillMaxSize()) { + Surface(modifier = modifier.fillMaxSize()) { Column { Spacer(modifier = Modifier.statusBarsPadding()) SearchBar( @@ -76,7 +79,6 @@ fun Search(onSnackClick: (Long, String) -> Unit, modifier: Modifier = Modifier, searching = state.searching, ) JetsnackDivider() - LaunchedEffect(state.query.text) { state.searching = true state.searchResults = SearchRepo.search(state.query.text) @@ -169,13 +171,15 @@ private fun SearchBar( searching: Boolean, modifier: Modifier = Modifier, ) { - JetsnackSurface( - color = JetsnackTheme.colors.uiFloated, - contentColor = JetsnackTheme.colors.textSecondary, - shape = MaterialTheme.shapes.small, + Surface( + style = { + background(colors.uiFloated) + contentColor(colors.textSecondary) + shape(shapes.small) + }, modifier = modifier .fillMaxWidth() - .height(56.dp) + .height(72.dp) .padding(horizontal = 24.dp, vertical = 8.dp), ) { Box(Modifier.fillMaxSize()) { @@ -239,7 +243,10 @@ private fun SearchHint() { Spacer(Modifier.width(8.dp)) Text( text = stringResource(R.string.search_jetsnack), - color = JetsnackTheme.colors.textHelp, + style = { + textStyleWithFontFamilyFix(typography.bodyLarge) + contentColor(colors.textHelp) + }, ) } } @@ -250,7 +257,7 @@ private fun SearchHint() { @Composable private fun SearchBarPreview() { JetsnackTheme { - JetsnackSurface { + Surface { SearchBar( query = TextFieldValue(""), onQueryChange = { }, diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Suggestions.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Suggestions.kt index 62b9ff40c1..8ebe6ba34b 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Suggestions.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Suggestions.kt @@ -26,8 +26,6 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,8 +33,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.example.jetsnack.model.SearchRepo import com.example.jetsnack.model.SearchSuggestionGroup -import com.example.jetsnack.ui.components.JetsnackSurface +import com.example.jetsnack.ui.components.Surface +import com.example.jetsnack.ui.components.Text +import com.example.jetsnack.ui.components.textStyleWithFontFamilyFix import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.theme.colors +import com.example.jetsnack.ui.theme.typography @Composable fun SearchSuggestions(suggestions: List, onSuggestionSelect: (String) -> Unit) { @@ -63,8 +65,10 @@ fun SearchSuggestions(suggestions: List, onSuggestionSele private fun SuggestionHeader(name: String, modifier: Modifier = Modifier) { Text( text = name, - style = MaterialTheme.typography.titleLarge, - color = JetsnackTheme.colors.textPrimary, + style = { + textStyleWithFontFamilyFix(typography.titleLarge) + contentColor(colors.textPrimary) + }, modifier = modifier .heightIn(min = 56.dp) .padding(horizontal = 24.dp, vertical = 4.dp) @@ -76,7 +80,9 @@ private fun SuggestionHeader(name: String, modifier: Modifier = Modifier) { private fun Suggestion(suggestion: String, onSuggestionSelect: (String) -> Unit, modifier: Modifier = Modifier) { Text( text = suggestion, - style = MaterialTheme.typography.titleMedium, + style = { + textStyleWithFontFamilyFix(typography.titleMedium) + }, modifier = modifier .heightIn(min = 48.dp) .clickable { onSuggestionSelect(suggestion) } @@ -91,7 +97,7 @@ private fun Suggestion(suggestion: String, onSuggestionSelect: (String) -> Unit, @Composable fun PreviewSuggestions() { JetsnackTheme { - JetsnackSurface { + Surface { SearchSuggestions( suggestions = SearchRepo.getSuggestions(), onSuggestionSelect = { }, diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt index 32cf6dd0c5..3e8f4244a0 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt @@ -14,7 +14,10 @@ * limitations under the License. */ -@file:OptIn(ExperimentalSharedTransitionApi::class, ExperimentalAnimationApi::class) +@file:OptIn( + ExperimentalSharedTransitionApi::class, ExperimentalAnimationApi::class, ExperimentalMediaQueryApi::class, + ExperimentalMaterial3ExpressiveApi::class, +) package com.example.jetsnack.ui.snackdetail @@ -26,11 +29,8 @@ import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn @@ -47,6 +47,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -56,16 +58,18 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.key @@ -74,13 +78,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalMediaQueryApi import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.TileMode import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource @@ -95,6 +95,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.lerp import androidx.compose.ui.unit.sp import androidx.compose.ui.util.lerp +import androidx.graphics.shapes.Morph import com.example.jetsnack.R import com.example.jetsnack.model.Snack import com.example.jetsnack.model.SnackCollection @@ -103,28 +104,35 @@ import com.example.jetsnack.ui.LocalNavAnimatedVisibilityScope import com.example.jetsnack.ui.LocalSharedTransitionScope import com.example.jetsnack.ui.SnackSharedElementKey import com.example.jetsnack.ui.SnackSharedElementType -import com.example.jetsnack.ui.components.JetsnackButton +import com.example.jetsnack.ui.components.Button import com.example.jetsnack.ui.components.JetsnackDivider -import com.example.jetsnack.ui.components.JetsnackPreviewWrapper -import com.example.jetsnack.ui.components.JetsnackSurface import com.example.jetsnack.ui.components.QuantitySelector import com.example.jetsnack.ui.components.SnackCollection import com.example.jetsnack.ui.components.SnackImage +import com.example.jetsnack.ui.components.Surface +import com.example.jetsnack.ui.components.Text +import com.example.jetsnack.ui.components.textStyleWithFontFamilyFix import com.example.jetsnack.ui.theme.JetsnackTheme -import com.example.jetsnack.ui.theme.Neutral8 +import com.example.jetsnack.ui.theme.colors +import com.example.jetsnack.ui.theme.typography +import com.example.jetsnack.ui.utils.JetsnackThemeWrapper +import com.example.jetsnack.ui.utils.SnackPolygons +import com.example.jetsnack.ui.utils.UiMediaScopeWrapper +import com.example.jetsnack.ui.utils.asShape import com.example.jetsnack.ui.utils.formatPrice +import com.example.jetsnack.ui.utils.sharedBoundsRevealWithShapeMorph import kotlin.math.max import kotlin.math.min private val BottomBarHeight = 56.dp private val TitleHeight = 128.dp -private val GradientScroll = 180.dp +private val GradientScroll = 120.dp private val ImageOverlap = 115.dp -private val MinTitleOffset = 56.dp +private val MinTitleOffset = 14.dp private val MinImageOffset = 12.dp private val MaxTitleOffset = ImageOverlap + MinTitleOffset + GradientScroll private val ExpandedImageSize = 300.dp -private val CollapsedImageSize = 150.dp +private val CollapsedImageSize = 130.dp private val HzPadding = Modifier.padding(horizontal = 24.dp) fun spatialExpressiveSpring() = spring( @@ -161,6 +169,7 @@ fun SnackDetail(snackId: Long, origin: String, upPress: () -> Unit) { Box( Modifier .clip(RoundedCornerShape(roundedCornerAnim)) + .windowInsetsPadding(WindowInsets.systemBars) .sharedBounds( rememberSharedContentState( key = SnackSharedElementKey( @@ -180,7 +189,6 @@ fun SnackDetail(snackId: Long, origin: String, upPress: () -> Unit) { .background(color = JetsnackTheme.colors.uiBackground), ) { val scroll = rememberScrollState(0) - Header(snack.id, origin = origin) Body(related, scroll) Title(snack, origin) { scroll.value } Image(snackId, origin, snack.imageRes) { scroll.value } @@ -190,64 +198,6 @@ fun SnackDetail(snackId: Long, origin: String, upPress: () -> Unit) { } } -@Composable -private fun Header(snackId: Long, origin: String) { - val sharedTransitionScope = LocalSharedTransitionScope.current - ?: throw IllegalArgumentException("No Scope found") - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current - ?: throw IllegalArgumentException("No Scope found") - - with(sharedTransitionScope) { - val brushColors = JetsnackTheme.colors.tornado1 - - val infiniteTransition = rememberInfiniteTransition(label = "background") - val targetOffset = with(LocalDensity.current) { - 1000.dp.toPx() - } - val offset by infiniteTransition.animateFloat( - initialValue = 0f, - targetValue = targetOffset, - animationSpec = infiniteRepeatable( - tween(50000, easing = LinearEasing), - repeatMode = RepeatMode.Reverse, - ), - label = "offset", - ) - Spacer( - modifier = Modifier - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snackId, - origin = origin, - type = SnackSharedElementType.Background, - ), - ), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = snackDetailBoundsTransform, - enter = fadeIn(nonSpatialExpressiveSpring()), - exit = fadeOut(nonSpatialExpressiveSpring()), - resizeMode = SharedTransitionScope.ResizeMode.scaleToBounds(), - ) - .height(280.dp) - .fillMaxWidth() - .blur(40.dp) - .drawWithCache { - val brushSize = 400f - val brush = Brush.linearGradient( - colors = brushColors, - start = Offset(offset, offset), - end = Offset(offset + brushSize, offset + brushSize), - tileMode = TileMode.Mirror, - ) - onDrawBehind { - drawRect(brush) - } - }, - ) - } -} - @Composable private fun SharedTransitionScope.Up(upPress: () -> Unit) { val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current @@ -265,13 +215,13 @@ private fun SharedTransitionScope.Up(upPress: () -> Unit) { exit = scaleOut(tween(20)), ) .background( - color = Neutral8.copy(alpha = 0.32f), + color = JetsnackTheme.colors.textPrimary.copy(alpha = 0.32f), shape = CircleShape, ), ) { Icon( painter = painterResource(id = R.drawable.ic_arrow_back), - tint = JetsnackTheme.colors.iconInteractive, + tint = JetsnackTheme.colors.uiBackground, contentDescription = stringResource(R.string.label_back), ) } @@ -296,7 +246,7 @@ private fun Body(related: List, scroll: ScrollState) { ) { Spacer(Modifier.height(GradientScroll)) Spacer(Modifier.height(ImageOverlap)) - JetsnackSurface( + Surface( Modifier .fillMaxWidth() .padding(top = 16.dp), @@ -305,21 +255,25 @@ private fun Body(related: List, scroll: ScrollState) { Spacer(Modifier.height(TitleHeight)) Text( text = stringResource(R.string.detail_header), - style = MaterialTheme.typography.labelSmall, - color = JetsnackTheme.colors.textHelp, - modifier = HzPadding, + style = { + textStyleWithFontFamilyFix(typography.headlineMedium) + contentColor(colors.textHelp) + contentPaddingHorizontal(24.dp) + }, ) Spacer(Modifier.height(16.dp)) var seeMore by remember { mutableStateOf(true) } with(sharedTransitionScope) { Text( text = stringResource(R.string.detail_placeholder), - style = MaterialTheme.typography.bodyLarge, - color = JetsnackTheme.colors.textHelp, + style = { + textStyleWithFontFamilyFix(typography.bodyLarge) + contentColor(colors.textHelp) + contentPaddingHorizontal(24.dp) + }, maxLines = if (seeMore) 5 else Int.MAX_VALUE, overflow = TextOverflow.Ellipsis, - modifier = HzPadding.skipToLookaheadSize(), - + modifier = Modifier.skipToLookaheadSize(), ) } val textButton = if (seeMore) { @@ -330,32 +284,39 @@ private fun Body(related: List, scroll: ScrollState) { Text( text = textButton, - style = MaterialTheme.typography.labelLarge, - textAlign = TextAlign.Center, - color = JetsnackTheme.colors.textLink, + style = { + textStyleWithFontFamilyFix(typography.labelLarge) + contentColor(colors.textLink) + textAlign(TextAlign.Center) + contentPadding(16.dp) + }, modifier = Modifier .heightIn(20.dp) - .fillMaxWidth() - .padding(top = 15.dp) .clickable { seeMore = !seeMore } + .align(Alignment.CenterHorizontally) + .wrapContentSize() .skipToLookaheadSize(), ) Spacer(Modifier.height(40.dp)) Text( text = stringResource(R.string.ingredients), - style = MaterialTheme.typography.labelSmall, - color = JetsnackTheme.colors.textHelp, - modifier = HzPadding, + style = { + textStyleWithFontFamilyFix(typography.labelSmall) + contentColor(colors.textHelp) + contentPaddingHorizontal(24.dp) + }, ) Spacer(Modifier.height(4.dp)) Text( text = stringResource(R.string.ingredients_list), - style = MaterialTheme.typography.bodyLarge, - color = JetsnackTheme.colors.textHelp, - modifier = HzPadding, + style = { + textStyleWithFontFamilyFix(typography.bodyLarge) + contentColor(colors.textHelp) + contentPaddingHorizontal(24.dp) + }, ) Spacer(Modifier.height(16.dp)) @@ -366,7 +327,6 @@ private fun Body(related: List, scroll: ScrollState) { SnackCollection( snackCollection = snackCollection, onSnackClick = { _, _ -> }, - highlight = false, ) } } @@ -410,9 +370,11 @@ private fun Title(snack: Snack, origin: String, scrollProvider: () -> Int) { Spacer(Modifier.height(16.dp)) Text( text = snack.name, - fontStyle = FontStyle.Italic, - style = MaterialTheme.typography.headlineMedium, - color = JetsnackTheme.colors.textSecondary, + style = { + textStyleWithFontFamilyFix(typography.headlineMedium) + contentColor(colors.textSecondary) + fontStyle(FontStyle.Italic) + }, modifier = HzPadding .sharedBounds( rememberSharedContentState( @@ -429,31 +391,35 @@ private fun Title(snack: Snack, origin: String, scrollProvider: () -> Int) { ) Text( text = snack.tagline, - fontStyle = FontStyle.Italic, - style = MaterialTheme.typography.titleSmall, - fontSize = 20.sp, - color = JetsnackTheme.colors.textHelp, + style = { + textStyleWithFontFamilyFix(typography.titleSmall) + fontStyle(FontStyle.Italic) + contentColor(colors.textHelp) + fontSize(20.sp) + }, modifier = HzPadding - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snack.id, - origin = origin, - type = SnackSharedElementType.Tagline, - ), - ), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = snackDetailBoundsTransform, - ) .wrapContentWidth(), ) Spacer(Modifier.height(4.dp)) with(animatedVisibilityScope) { Text( text = formatPrice(snack.price), - style = MaterialTheme.typography.titleLarge, - color = JetsnackTheme.colors.textPrimary, + style = { + textStyleWithFontFamilyFix(typography.titleLarge) + contentColor(colors.textPrimary) + }, modifier = HzPadding + .sharedBounds( + rememberSharedContentState( + key = SnackSharedElementKey( + snackId = snack.id, + origin = origin, + type = SnackSharedElementType.Price, + ), + ), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = snackDetailBoundsTransform, + ) .animateEnterExit( enter = fadeIn() + slideInVertically { -it / 3 }, exit = fadeOut() + slideOutVertically { -it / 3 }, @@ -482,7 +448,7 @@ private fun Image( CollapsingImageLayout( collapseFractionProvider = collapseFractionProvider, - modifier = HzPadding.statusBarsPadding(), + modifier = Modifier, ) { val sharedTransitionScope = LocalSharedTransitionScope.current ?: throw IllegalStateException("No sharedTransitionScope found") @@ -490,25 +456,54 @@ private fun Image( ?: throw IllegalStateException("No animatedVisibilityScope found") with(sharedTransitionScope) { + val targetRoundedPolygon = SnackPolygons.snackItemPolygon + val restingRoundedPolygon = SnackPolygons.snackDetailPolygon + val morph = remember { + Morph(restingRoundedPolygon, targetRoundedPolygon) + } + val progress = LocalNavAnimatedVisibilityScope.current?.transition?.animateFloat( + transitionSpec = { + tween(300, easing = LinearEasing) + }, + ) { + when (it) { + EnterExitState.PreEnter -> 1f + EnterExitState.Visible -> 0f + EnterExitState.PostExit -> 1f + } + }?.value ?: 0f + + val sharedContentState = rememberSharedContentState( + key = SnackSharedElementKey( + snackId = snackId, + origin = origin, + type = SnackSharedElementType.Image, + ), + ) + SnackImage( imageRes = imageRes, contentDescription = null, + style = { + val shape = if (sharedContentState.isMatchFound) { + morph.asShape(progress) + } else { + restingRoundedPolygon.asShape() + } + shape(shape) + }, modifier = Modifier - .sharedBounds( - rememberSharedContentState( - key = SnackSharedElementKey( - snackId = snackId, - origin = origin, - type = SnackSharedElementType.Image, - ), - ), + .aspectRatio(1.3f) + .fillMaxSize() + .padding(4.dp) + .sharedBoundsRevealWithShapeMorph( + sharedContentState = sharedContentState, + sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, - exit = fadeOut(), - enter = fadeIn(), boundsTransform = snackDetailBoundsTransform, - ) - .fillMaxSize(), - + restingShape = restingRoundedPolygon, + targetShape = targetRoundedPolygon, + ), ) } } @@ -553,7 +548,7 @@ private fun CartBottomBar(modifier: Modifier = Modifier) { LocalNavAnimatedVisibilityScope.current ?: throw IllegalStateException("No Shared scope") with(sharedTransitionScope) { with(animatedVisibilityScope) { - JetsnackSurface( + Surface( modifier = modifier .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 4f) .animateEnterExit( @@ -581,15 +576,21 @@ private fun CartBottomBar(modifier: Modifier = Modifier) { decreaseItemCount = { if (count > 0) updateCount(count - 1) }, increaseItemCount = { updateCount(count + 1) }, ) - Spacer(Modifier.width(16.dp)) - JetsnackButton( + Spacer(Modifier.weight(1f)) + Button( onClick = { /* todo */ }, - modifier = Modifier.weight(1f), + modifier = Modifier + .widthIn(max = 200.dp), + style = { + externalPadding(4.dp) + }, ) { Text( text = stringResource(R.string.add_to_cart), modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, + style = { + textAlign(TextAlign.Center) + }, maxLines = 1, ) } @@ -605,11 +606,13 @@ private fun CartBottomBar(modifier: Modifier = Modifier) { @Preview("large font", fontScale = 2f) @Composable private fun SnackDetailPreview() { - JetsnackPreviewWrapper { - SnackDetail( - snackId = 1L, - origin = "details", - upPress = { }, - ) + JetsnackThemeWrapper { + UiMediaScopeWrapper { + SnackDetail( + snackId = 1L, + origin = "details", + upPress = { }, + ) + } } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Color.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Color.kt index eb99c98163..28e3cefb47 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Color.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Color.kt @@ -16,59 +16,99 @@ package com.example.jetsnack.ui.theme +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color -val Shadow11 = Color(0xff001787) -val Shadow10 = Color(0xff00119e) -val Shadow9 = Color(0xff0009b3) -val Shadow8 = Color(0xff0200c7) -val Shadow7 = Color(0xff0e00d7) -val Shadow6 = Color(0xff2a13e4) -val Shadow5 = Color(0xff4b30ed) -val Shadow4 = Color(0xff7057f5) -val Shadow3 = Color(0xff9b86fa) -val Shadow2 = Color(0xffc8bbfd) -val Shadow1 = Color(0xffded6fe) -val Shadow0 = Color(0xfff4f2ff) - -val Ocean11 = Color(0xff005687) -val Ocean10 = Color(0xff006d9e) -val Ocean9 = Color(0xff0087b3) -val Ocean8 = Color(0xff00a1c7) -val Ocean7 = Color(0xff00b9d7) -val Ocean6 = Color(0xff13d0e4) -val Ocean5 = Color(0xff30e2ed) -val Ocean4 = Color(0xff57eff5) -val Ocean3 = Color(0xff86f7fa) -val Ocean2 = Color(0xffbbfdfd) -val Ocean1 = Color(0xffd6fefe) -val Ocean0 = Color(0xfff2ffff) - -val Lavender11 = Color(0xff170085) -val Lavender10 = Color(0xff23009e) -val Lavender9 = Color(0xff3300b3) -val Lavender8 = Color(0xff4400c7) -val Lavender7 = Color(0xff5500d7) -val Lavender6 = Color(0xff6f13e4) -val Lavender5 = Color(0xff8a30ed) -val Lavender4 = Color(0xffa557f5) -val Lavender3 = Color(0xffc186fa) -val Lavender2 = Color(0xffdebbfd) -val Lavender1 = Color(0xffebd6fe) -val Lavender0 = Color(0xfff9f2ff) - -val Rose11 = Color(0xff7f0054) -val Rose10 = Color(0xff97005c) -val Rose9 = Color(0xffaf0060) -val Rose8 = Color(0xffc30060) -val Rose7 = Color(0xffd4005d) -val Rose6 = Color(0xffe21365) -val Rose5 = Color(0xffec3074) -val Rose4 = Color(0xfff4568b) -val Rose3 = Color(0xfff985aa) -val Rose2 = Color(0xfffdbbcf) -val Rose1 = Color(0xfffed6e2) -val Rose0 = Color(0xfffff2f6) +/** + * Jetsnack custom Color Palette + */ +@Immutable +data class JetsnackColors( + val gradient1: List, + val gradient2: List, + val gradient3: List, + val brand: Color, + val brandLight: Color, + val brandSecondary: Color, + val uiBackground: Color, + val uiBorder: Color, + val uiFloated: Color, + val interactivePrimary: List = gradient2, + val interactiveSecondary: List = gradient1, + val interactiveMask: List = gradient3, + val interactiveDisabled: Color, + val interactiveDisabledText: Color, + val textPrimary: Color = brand, + val textSecondary: Color, + val textHelp: Color, + val textInteractive: Color, + val textLink: Color, + val iconPrimary: Color = brand, + val iconSecondary: Color, + val iconInteractive: Color, + val iconInteractiveInactive: Color, + val error: Color, + val notificationBadge: Color = error, + val cardHighlightBackground: Color, + val cardHighlightBorder: Color, + val isDark: Boolean, +) +/** + * A Material [Colors] implementation which sets all colors to [debugColor] to discourage usage of + * [MaterialTheme.colorScheme] in preference to [JetsnackTheme.colors]. + */ +fun debugColors(darkTheme: Boolean, debugColor: Color = Color.Magenta) = ColorScheme( + primary = debugColor, + onPrimary = debugColor, + primaryContainer = debugColor, + onPrimaryContainer = debugColor, + inversePrimary = debugColor, + secondary = debugColor, + onSecondary = debugColor, + secondaryContainer = debugColor, + onSecondaryContainer = debugColor, + tertiary = debugColor, + onTertiary = debugColor, + tertiaryContainer = debugColor, + onTertiaryContainer = debugColor, + background = debugColor, + onBackground = debugColor, + surface = debugColor, + onSurface = debugColor, + surfaceVariant = debugColor, + onSurfaceVariant = debugColor, + surfaceTint = debugColor, + inverseSurface = debugColor, + inverseOnSurface = debugColor, + error = debugColor, + onError = debugColor, + errorContainer = debugColor, + onErrorContainer = debugColor, + outline = debugColor, + outlineVariant = debugColor, + scrim = debugColor, + surfaceBright = debugColor, + surfaceDim = debugColor, + surfaceContainer = debugColor, + surfaceContainerHigh = debugColor, + surfaceContainerHighest = debugColor, + surfaceContainerLow = debugColor, + surfaceContainerLowest = debugColor, + primaryFixed = debugColor, + primaryFixedDim = debugColor, + onPrimaryFixed = debugColor, + onPrimaryFixedVariant = debugColor, + secondaryFixed = debugColor, + secondaryFixedDim = debugColor, + onSecondaryFixed = debugColor, + onSecondaryFixedVariant = debugColor, + tertiaryFixed = debugColor, + tertiaryFixedDim = debugColor, + onTertiaryFixed = debugColor, + onTertiaryFixedVariant = debugColor, +) val Neutral8 = Color(0xff121212) val Neutral7 = Color(0xde000000) @@ -79,11 +119,106 @@ val Neutral3 = Color(0x1fffffff) val Neutral2 = Color(0x61ffffff) val Neutral1 = Color(0xbdffffff) val Neutral0 = Color(0xffffffff) - -val FunctionalRed = Color(0xffd00036) -val FunctionalRedDark = Color(0xffea6d7e) -val FunctionalGreen = Color(0xff52c41a) val FunctionalGrey = Color(0xfff6f6f6) val FunctionalDarkGrey = Color(0xff2e2e2e) const val AlphaNearOpaque = 0.95f + +// New Color Palette +val WarpCoreLight = Color(0xFF9685FF) +val WarpCoreDark = Color(0xFF311EA9) +val NebulaLight = Color(0xFFE4E1FA) +val NebulaDark = Color(0xFF3229CD) +val VastOfNightLight = Color(0xFF0E0066) +val VastOfNightDark = Color(0xFFFDFCFC) +val ScienceLight = Color(0xFF8BDEBE) +val ScienceDark = Color(0xFF297258) +val TricorderLight = Color(0xFFBEFBE3) +val TricorderDark = Color(0xFF1EBE7F) +val DeepSpaceLight = Color(0xFF005C5E) +val DeepSpaceDark = Color(0xFFA4F1F2) +val StarburstLight = Color(0xFFFFC8A4) +val StarburstDark = Color(0xFFCC9570) +val TeleportLight = Color(0xFFFFECE9) +val TeleportDark = Color(0xFFD16A5A) +val SalamanderLight = Color(0xFF544337) +val SalamanderDark = Color(0xFFF1DED1) +val RedShirtLight = Color(0xFFFFDAD6) +val RedShirtDark = Color(0xFF93000A) +val CommandLight = Color(0xFF93000A) +val CommandDark = Color(0xFFFFDAD6) +val CloudLight = Color(0xFFFCF8F9) +val CloudDark = Color(0xFF141314) +val ShuttleLight = Color(0xFF222B2B) +val ShuttleDark = Color(0xFFE3E8E8) +val OutlineLight = Color(0xFF7F7573) +val OutlineDark = Color(0xFF9A8E8C) +val OutlineVariantLight = Color(0xFFD1C4C1) +val OutlineVariantDark = Color(0xFF4E4543) +val Warp10Light = Color(0xFFFFFFFF) +val Warp10Dark = Color(0xFF0E0E0F) +val Warp5Light = Color(0xFFF7F3F3) +val Warp5Dark = Color(0xFF1C1B1C) +val WarpLight = Color(0xFFEBE7E7) +val WarpDark = Color(0xFF2A2A2A) + +val gradient1Light = listOf(NebulaLight, ScienceLight, DeepSpaceLight) +val gradient1Dark = listOf(NebulaDark, ScienceDark, DeepSpaceDark) +val gradient2Light = listOf(WarpCoreLight, StarburstLight) +val gradient2Dark = listOf(WarpCoreDark, StarburstDark) +val gradient3Light = listOf(NebulaLight, WarpCoreLight, VastOfNightLight) +val gradient3Dark = listOf(NebulaDark, WarpCoreDark, VastOfNightDark) + +internal val LightColorPalette = JetsnackColors( + brand = WarpCoreLight, + brandLight = NebulaLight, + brandSecondary = ScienceLight, + uiBackground = CloudLight, + uiBorder = OutlineLight, + uiFloated = WarpLight, + textSecondary = VastOfNightLight, + textHelp = ShuttleLight, + textInteractive = ShuttleLight, + textPrimary = DeepSpaceLight, + textLink = DeepSpaceLight, + iconPrimary = ShuttleLight, + iconSecondary = ShuttleLight, + iconInteractive = WarpCoreLight, + iconInteractiveInactive = ShuttleLight, + interactiveDisabled = WarpLight, + interactiveDisabledText = ShuttleLight, + error = CommandLight, + gradient1 = gradient1Light, + gradient2 = gradient2Light, + gradient3 = gradient3Light, + cardHighlightBackground = TeleportLight, + cardHighlightBorder = StarburstLight, + isDark = false, +) + +internal val DarkColorPalette = JetsnackColors( + brand = WarpCoreDark, + brandLight = NebulaDark, + brandSecondary = ScienceDark, + uiBackground = CloudDark, + uiBorder = OutlineDark, + uiFloated = WarpDark, + textPrimary = DeepSpaceDark, + textSecondary = VastOfNightDark, + textHelp = ShuttleDark, + textInteractive = ShuttleDark, + textLink = DeepSpaceDark, + iconPrimary = ShuttleDark, + iconSecondary = ShuttleDark, + iconInteractive = WarpCoreDark, + iconInteractiveInactive = ShuttleDark, + interactiveDisabled = WarpDark, + interactiveDisabledText = ShuttleDark, + error = CommandDark, + gradient1 = gradient1Dark, + gradient2 = gradient2Dark, + gradient3 = gradient3Dark, + cardHighlightBackground = TeleportDark, + cardHighlightBorder = StarburstDark, + isDark = true, +) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Shape.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Shape.kt index b8a05338e4..cfc2d4cd2d 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Shape.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Shape.kt @@ -21,7 +21,7 @@ import androidx.compose.material3.Shapes import androidx.compose.ui.unit.dp val Shapes = Shapes( - small = RoundedCornerShape(percent = 50), + small = RoundedCornerShape(size = 28.dp), medium = RoundedCornerShape(20.dp), large = RoundedCornerShape(0.dp), ) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Styles.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Styles.kt new file mode 100644 index 0000000000..2fb4019393 --- /dev/null +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Styles.kt @@ -0,0 +1,323 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalFoundationStyleApi::class, ExperimentalMediaQueryApi::class) + +package com.example.jetsnack.ui.theme + +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.style.ExperimentalFoundationStyleApi +import androidx.compose.foundation.style.MutableStyleState +import androidx.compose.foundation.style.Style +import androidx.compose.foundation.style.StyleScope +import androidx.compose.foundation.style.StyleStateKey +import androidx.compose.foundation.style.disabled +import androidx.compose.foundation.style.fillHeight +import androidx.compose.foundation.style.fillWidth +import androidx.compose.foundation.style.focused +import androidx.compose.foundation.style.hovered +import androidx.compose.foundation.style.pressed +import androidx.compose.foundation.style.selected +import androidx.compose.foundation.style.then +import androidx.compose.material3.LocalTextStyle +import androidx.compose.runtime.Immutable +import androidx.compose.ui.ExperimentalMediaQueryApi +import androidx.compose.ui.LocalUiMediaScope +import androidx.compose.ui.UiMediaScope +import androidx.compose.ui.UiMediaScope.ViewingDistance +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.TileMode +import com.example.jetsnack.ui.utils.ellipticalGradient +import androidx.compose.ui.graphics.shadow.Shadow +import androidx.compose.ui.mediaQuery +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import com.example.jetsnack.ui.components.textStyleWithFontFamilyFix + +@Immutable +data class Styles( + val buttonStyle: Style = Style { + shape(shapes.small) + background( + Brush.ellipticalGradient( + colors = colors.interactivePrimary, + radiusXPercent = 1.3f, + radiusYPercent = 0.7f, + centerXPercent = 0.4f, + centerYPercent = 0.5f, + ), + ) + contentColor(colors.textSecondary) + minWidth(58.dp) + if (mediaQuery { + pointerPrecision == UiMediaScope.PointerPrecision.Fine && + keyboardKind == UiMediaScope.KeyboardKind.Physical + } + ) { + contentPaddingVertical(4.dp) + contentPaddingHorizontal(8.dp) + shape(shapes.medium) + minHeight(32.dp) + textStyleWithFontFamilyFix(typography.labelMedium) + } else { + contentPaddingVertical(8.dp) + contentPaddingHorizontal(24.dp) + minHeight(40.dp) + shape(shapes.small) + textStyleWithFontFamilyFix(typography.labelLarge) + } + + dropShadow(Shadow(color = colors.brand, offset = DpOffset(x = 0.dp, y = 2.dp), radius = 6.dp)) + innerShadow(Shadow(color = colors.brand.copy(alpha = 0.5f), offset = DpOffset(x = (-6).dp, (-4).dp), radius = 8.dp)) + + hovered { + animate { + background(colors.brandLight) + dropShadow(Shadow(color = colors.brand, offset = DpOffset(x = 0.dp, y = 2.dp), radius = 6.dp)) + innerShadow(Shadow(color = colors.brand.copy(alpha = 0.5f), offset = DpOffset(x = (-6).dp, (-2).dp), radius = 8.dp)) + } + } + focused { + animate { + border(4.dp, colors.brand) + } + } + pressed { + animate { + background(colors.brand) + dropShadow(Shadow(color = colors.brand, offset = DpOffset(x = 0.dp, y = 0.dp), radius = 0.dp)) + innerShadow(Shadow(color = colors.brand, offset = DpOffset(x = (0).dp, (0).dp), radius = 0.dp)) + } + focused { + animate { + border(4.dp, colors.brand) + } + } + hovered { + // TODO this state is broken - await API changes on animation changes + // we don't want to combine these two + // so set the properties to the same + animate { + background(colors.brand) + dropShadow(Shadow(color = colors.brand, offset = DpOffset(x = 0.dp, y = 0.dp), radius = 0.dp)) + innerShadow(Shadow(color = colors.brand, offset = DpOffset(x = (0).dp, (0).dp), radius = 0.dp)) + } + } + } + + loading { + animate { + background( + Brush.ellipticalGradient( + colors = colors.interactivePrimary.reversed(), + radiusXPercent = 1.3f, + radiusYPercent = 0.7f, + centerXPercent = 0.4f, + centerYPercent = 0.5f, + ), + ) + dropShadow(Shadow(color = colors.brand, offset = DpOffset(x = 0.dp, y = 0.dp), radius = 0.dp)) + innerShadow(Shadow(color = colors.brand, offset = DpOffset(x = (0).dp, (0).dp), radius = 0.dp)) + } + } + disabled { + animate { + background(colors.interactiveDisabled) + contentColor(colors.interactiveDisabledText) + // reset shadow + dropShadow(Shadow(color = Color.Transparent, offset = DpOffset(x = 0.dp, y = 0.dp), radius = 0.dp)) + innerShadow(Shadow(color = Color.Transparent, offset = DpOffset(x = (0).dp, (0).dp), radius = 0.dp)) + } + } + }, + val cardStyle: Style = Style { + shape(shapes.medium) + background(colors.uiBackground) + contentColor(colors.textPrimary) + }, + val dividerStyle: Style = Style { + background(colors.uiBorder.copy(alpha = 0.12f)) + height(1.dp) + fillWidth() + }, + val gradientIconButtonStyle: Style = Style { + shape(CircleShape) + clip(true) + border(2.dp, Brush.linearGradient(colors.interactiveSecondary)) + background(colors.uiBackground) + contentColor(colors.textPrimary) + pressed { + animate { + background( + Brush.horizontalGradient( + colors = colors.interactiveSecondary, + startX = 0f, + endX = 200f, + tileMode = TileMode.Mirror, + ), + ) + } + } + }, + val filterChipStyle: Style = Style { + shape(shapes.small) + background(colors.uiBackground) + contentColor(colors.textInteractive) + border(2.dp, Brush.linearGradient(colors.interactiveSecondary)) + minHeight(32.dp) + textStyleWithFontFamilyFix(typography.labelSmall) + pressed { + animate { + val gradient = Brush.ellipticalGradient( + colors = colors.interactivePrimary, + radiusXPercent = 1.3f, + radiusYPercent = 0.7232f, + centerXPercent = 0.4f, + centerYPercent = 0.55f, + ) + border(2.dp, gradient) + background(gradient) + } + } + selected { + animate { + background(colors.brand) + contentColor(colors.textSecondary) + border(2.dp, colors.brand) + dropShadow(Shadow(color = colors.brand, radius = 6.dp, offset = DpOffset(0.dp, 2.dp))) + innerShadow(Shadow(color = colors.brand, offset = DpOffset((-6).dp, (-8).dp), radius = 8.dp)) + } + } + }, + val defaultTextStyle: Style = Style { + textStyleWithFontFamilyFix(LocalTextStyle.currentValue) + }, + val surfaceStyle: Style = Style { + shape(RectangleShape) + background(colors.uiBackground) + contentColor(colors.textSecondary) + clip(true) + }, + val baseSnackCardStyle : Style = Style { + textAlign(TextAlign.Center) + + // todo this animation doesn't seem to play nice + if (mediaQuery { windowWidth > 500.dp }) { + animate { + width(200.dp) + } + } else { + animate { + width(170.dp) + } + } + hovered { + animate { + scale(1.05f) + } + } + focused { + animate { + scale(1.05f) + } + } + pressed { + animate { + scale(1.05f) + } + } + }, + val highlightGlowCardStyle : Style = baseSnackCardStyle then Style { + background(colors.brandLight) + border(0.dp, colors.brandLight) + hovered { + animate { + dropShadow(Shadow(offset = DpOffset(0.dp, 2.dp), radius = 6.dp, color = colors.brand)) + innerShadow(Shadow(offset = DpOffset((-6).dp, (-2).dp), radius = 8.dp, color = colors.brand.copy(alpha = 0.5f))) + } + } + focused { + animate { + dropShadow(Shadow(offset = DpOffset(0.dp, 2.dp), radius = 6.dp, color = colors.brand)) + innerShadow(Shadow(offset = DpOffset((-6).dp, (-2).dp), radius = 8.dp, color = colors.brand.copy(alpha = 0.5f))) + } + } + }, + val normalCardStyle : Style = baseSnackCardStyle then Style { + background(Color.Transparent) + width(100.dp) + contentPadding(2.dp) + textAlign(TextAlign.Center) + pressed { + background(colors.uiFloated.copy(alpha = 0.5f)) + } + }, + val plainCardStyle : Style = baseSnackCardStyle then Style { + background(colors.cardHighlightBackground) + clip(true) + border(1.dp, colors.cardHighlightBorder) + } +) + +fun StyleScope.adaptiveFontSize(fontSize: TextUnit) { + var scaleFactor = when (LocalUiMediaScope.currentValue.viewingDistance) { + ViewingDistance.Near -> 1f + ViewingDistance.Medium -> 1.72f + ViewingDistance.Far -> 1.5f + else -> 1f + } + scaleFactor = when (LocalUiMediaScope.currentValue.pointerPrecision) { + UiMediaScope.PointerPrecision.Coarse -> scaleFactor * 1f + UiMediaScope.PointerPrecision.Blunt -> scaleFactor * 0.66f + UiMediaScope.PointerPrecision.Fine -> scaleFactor * 1f + UiMediaScope.PointerPrecision.None -> scaleFactor + else -> { + scaleFactor + } + } + fontSize(fontSize * scaleFactor) +} + +enum class LoadingState { + Loading, + Loaded, + Error, +} + +val loadingStateKey = StyleStateKey(LoadingState.Loaded) + +var MutableStyleState.loadingState + get() = this[loadingStateKey] + set(value) { + this[loadingStateKey] = value + } + +fun StyleScope.loading(value: Style) { + state(loadingStateKey, value, { key, state -> state[key] == LoadingState.Loading }) +} + +fun StyleScope.loaded(value: Style) { + state(loadingStateKey, value, { key, state -> state[key] == LoadingState.Loaded }) +} + +fun StyleScope.error(value: Style) { + state(loadingStateKey, value, { key, state -> state[key] == LoadingState.Error }) +} diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Theme.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Theme.kt index ad0a8e66c1..bbf60c7c56 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Theme.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Theme.kt @@ -14,74 +14,71 @@ * limitations under the License. */ +@file:OptIn(ExperimentalFoundationStyleApi::class) + package com.example.jetsnack.ui.theme import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.ColorScheme +import androidx.compose.foundation.style.ExperimentalFoundationStyleApi +import androidx.compose.foundation.style.StyleScope import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.material3.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.graphics.Color -private val LightColorPalette = JetsnackColors( - brand = Shadow5, - brandSecondary = Ocean3, - uiBackground = Neutral0, - uiBorder = Neutral4, - uiFloated = FunctionalGrey, - textSecondary = Neutral7, - textHelp = Neutral6, - textInteractive = Neutral0, - textLink = Ocean11, - iconSecondary = Neutral7, - iconInteractive = Neutral0, - iconInteractiveInactive = Neutral1, - error = FunctionalRed, - gradient6_1 = listOf(Shadow4, Ocean3, Shadow2, Ocean3, Shadow4), - gradient6_2 = listOf(Rose4, Lavender3, Rose2, Lavender3, Rose4), - gradient3_1 = listOf(Shadow2, Ocean3, Shadow4), - gradient3_2 = listOf(Rose2, Lavender3, Rose4), - gradient2_1 = listOf(Shadow4, Shadow11), - gradient2_2 = listOf(Ocean3, Shadow3), - gradient2_3 = listOf(Lavender3, Rose2), - tornado1 = listOf(Shadow4, Ocean3), - isDark = false, -) +@Immutable +class JetsnackTheme( + val colors: JetsnackColors = LightColorPalette, + val typography: Typography = Typography, + val shapes: Shapes = Shapes, + val styles: Styles = Styles(), +) { + companion object { + val colors: JetsnackColors + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.colors + + val typography: Typography + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.typography + + val shapes: Shapes + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.shapes + + val styles: Styles + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.styles + + val LocalJetsnackTheme: ProvidableCompositionLocal + get() = LocalJetsnackThemeInstance + } +} + +val StyleScope.colors: JetsnackColors + get() = JetsnackTheme.LocalJetsnackTheme.currentValue.colors -private val DarkColorPalette = JetsnackColors( - brand = Shadow1, - brandSecondary = Ocean2, - uiBackground = Neutral8, - uiBorder = Neutral3, - uiFloated = FunctionalDarkGrey, - textPrimary = Shadow1, - textSecondary = Neutral0, - textHelp = Neutral1, - textInteractive = Neutral7, - textLink = Ocean2, - iconPrimary = Shadow1, - iconSecondary = Neutral0, - iconInteractive = Neutral7, - iconInteractiveInactive = Neutral6, - error = FunctionalRedDark, - gradient6_1 = listOf(Shadow5, Ocean7, Shadow9, Ocean7, Shadow5), - gradient6_2 = listOf(Rose11, Lavender7, Rose8, Lavender7, Rose11), - gradient3_1 = listOf(Shadow9, Ocean7, Shadow5), - gradient3_2 = listOf(Rose8, Lavender7, Rose11), - gradient2_1 = listOf(Ocean3, Shadow3), - gradient2_2 = listOf(Ocean4, Shadow2), - gradient2_3 = listOf(Lavender3, Rose3), - tornado1 = listOf(Shadow4, Ocean3), - isDark = true, -) +val StyleScope.typography: Typography + get() = JetsnackTheme.LocalJetsnackTheme.currentValue.typography + +val StyleScope.shapes: Shapes + get() = JetsnackTheme.LocalJetsnackTheme.currentValue.shapes + +internal val LocalJetsnackThemeInstance = staticCompositionLocalOf { JetsnackTheme() } @Composable fun JetsnackTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { val colors = if (darkTheme) DarkColorPalette else LightColorPalette + val theme = JetsnackTheme(colors = colors, styles = Styles()) - ProvideJetsnackColors(colors) { + CompositionLocalProvider( + JetsnackTheme.LocalJetsnackTheme provides theme, + ) { MaterialTheme( colorScheme = debugColors(darkTheme), typography = Typography, @@ -90,96 +87,3 @@ fun JetsnackTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composab ) } } - -object JetsnackTheme { - val colors: JetsnackColors - @Composable - get() = LocalJetsnackColors.current -} - -/** - * Jetsnack custom Color Palette - */ -@Immutable -data class JetsnackColors( - val gradient6_1: List, - val gradient6_2: List, - val gradient3_1: List, - val gradient3_2: List, - val gradient2_1: List, - val gradient2_2: List, - val gradient2_3: List, - val brand: Color, - val brandSecondary: Color, - val uiBackground: Color, - val uiBorder: Color, - val uiFloated: Color, - val interactivePrimary: List = gradient2_1, - val interactiveSecondary: List = gradient2_2, - val interactiveMask: List = gradient6_1, - val textPrimary: Color = brand, - val textSecondary: Color, - val textHelp: Color, - val textInteractive: Color, - val textLink: Color, - val tornado1: List, - val iconPrimary: Color = brand, - val iconSecondary: Color, - val iconInteractive: Color, - val iconInteractiveInactive: Color, - val error: Color, - val notificationBadge: Color = error, - val isDark: Boolean, -) - -@Composable -fun ProvideJetsnackColors(colors: JetsnackColors, content: @Composable () -> Unit) { - CompositionLocalProvider(LocalJetsnackColors provides colors, content = content) -} - -private val LocalJetsnackColors = staticCompositionLocalOf { - error("No JetsnackColorPalette provided") -} - -/** - * A Material [Colors] implementation which sets all colors to [debugColor] to discourage usage of - * [MaterialTheme.colorScheme] in preference to [JetsnackTheme.colors]. - */ -fun debugColors(darkTheme: Boolean, debugColor: Color = Color.Magenta) = ColorScheme( - primary = debugColor, - onPrimary = debugColor, - primaryContainer = debugColor, - onPrimaryContainer = debugColor, - inversePrimary = debugColor, - secondary = debugColor, - onSecondary = debugColor, - secondaryContainer = debugColor, - onSecondaryContainer = debugColor, - tertiary = debugColor, - onTertiary = debugColor, - tertiaryContainer = debugColor, - onTertiaryContainer = debugColor, - background = debugColor, - onBackground = debugColor, - surface = debugColor, - onSurface = debugColor, - surfaceVariant = debugColor, - onSurfaceVariant = debugColor, - surfaceTint = debugColor, - inverseSurface = debugColor, - inverseOnSurface = debugColor, - error = debugColor, - onError = debugColor, - errorContainer = debugColor, - onErrorContainer = debugColor, - outline = debugColor, - outlineVariant = debugColor, - scrim = debugColor, - surfaceBright = debugColor, - surfaceDim = debugColor, - surfaceContainer = debugColor, - surfaceContainerHigh = debugColor, - surfaceContainerHighest = debugColor, - surfaceContainerLow = debugColor, - surfaceContainerLowest = debugColor, -) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Type.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Type.kt index 9f4045fa18..0fabf0b516 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Type.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Type.kt @@ -18,107 +18,108 @@ package com.example.jetsnack.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.googlefonts.Font +import androidx.compose.ui.text.googlefonts.GoogleFont import androidx.compose.ui.unit.sp import com.example.jetsnack.R -private val Montserrat = FontFamily( - Font(R.font.montserrat_light, FontWeight.Light), - Font(R.font.montserrat_regular, FontWeight.Normal), - Font(R.font.montserrat_medium, FontWeight.Medium), - Font(R.font.montserrat_semibold, FontWeight.SemiBold), +val provider = GoogleFont.Provider( + providerAuthority = "com.google.android.gms.fonts", + providerPackage = "com.google.android.gms", + certificates = R.array.com_google_android_gms_fonts_certs, ) -private val Karla = FontFamily( - Font(R.font.karla_regular, FontWeight.Normal), - Font(R.font.karla_bold, FontWeight.Bold), +val instrumentSansFontName = GoogleFont("Instrument Sans") + +val instrumentSansFontFamily = FontFamily( + Font(googleFont = instrumentSansFontName, fontProvider = provider), ) val Typography = Typography( displayLarge = TextStyle( - fontFamily = Montserrat, + fontFamily = instrumentSansFontFamily, fontSize = 96.sp, fontWeight = FontWeight.Light, lineHeight = 117.sp, letterSpacing = (-1.5).sp, ), displayMedium = TextStyle( - fontFamily = Montserrat, + fontFamily = instrumentSansFontFamily, fontSize = 60.sp, fontWeight = FontWeight.Light, lineHeight = 73.sp, letterSpacing = (-0.5).sp, ), displaySmall = TextStyle( - fontFamily = Montserrat, + fontFamily = instrumentSansFontFamily, fontSize = 48.sp, fontWeight = FontWeight.Normal, lineHeight = 59.sp, ), headlineMedium = TextStyle( - fontFamily = Montserrat, - fontSize = 30.sp, - fontWeight = FontWeight.SemiBold, - lineHeight = 37.sp, + fontFamily = instrumentSansFontFamily, + fontSize = 26.sp, + fontWeight = FontWeight(500), + lineHeight = 36.sp, ), headlineSmall = TextStyle( - fontFamily = Montserrat, + fontFamily = instrumentSansFontFamily, fontSize = 24.sp, fontWeight = FontWeight.SemiBold, lineHeight = 29.sp, ), titleLarge = TextStyle( - fontFamily = Montserrat, + fontFamily = instrumentSansFontFamily, fontSize = 20.sp, fontWeight = FontWeight.SemiBold, lineHeight = 24.sp, ), titleMedium = TextStyle( - fontFamily = Montserrat, + fontFamily = instrumentSansFontFamily, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, lineHeight = 24.sp, letterSpacing = 0.15.sp, ), titleSmall = TextStyle( - fontFamily = Karla, + fontFamily = instrumentSansFontFamily, fontSize = 14.sp, fontWeight = FontWeight.Bold, lineHeight = 24.sp, letterSpacing = 0.1.sp, ), bodyLarge = TextStyle( - fontFamily = Karla, + fontFamily = instrumentSansFontFamily, fontSize = 16.sp, fontWeight = FontWeight.Normal, lineHeight = 28.sp, letterSpacing = 0.15.sp, ), bodyMedium = TextStyle( - fontFamily = Montserrat, + fontFamily = instrumentSansFontFamily, fontSize = 14.sp, fontWeight = FontWeight.Medium, lineHeight = 20.sp, letterSpacing = 0.25.sp, ), labelLarge = TextStyle( - fontFamily = Montserrat, + lineHeight = 20.sp, + fontWeight = FontWeight(600), + letterSpacing = 0.1.sp, + fontFamily = instrumentSansFontFamily, fontSize = 14.sp, - fontWeight = FontWeight.SemiBold, - lineHeight = 16.sp, - letterSpacing = 1.25.sp, ), bodySmall = TextStyle( - fontFamily = Karla, + fontFamily = instrumentSansFontFamily, fontSize = 12.sp, fontWeight = FontWeight.Bold, lineHeight = 16.sp, letterSpacing = 0.4.sp, ), labelSmall = TextStyle( - fontFamily = Montserrat, + fontFamily = instrumentSansFontFamily, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, lineHeight = 16.sp, diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/EllipticalGradient.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/EllipticalGradient.kt new file mode 100644 index 0000000000..b136e7f7de --- /dev/null +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/EllipticalGradient.kt @@ -0,0 +1,53 @@ +package com.example.jetsnack.ui.utils + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Matrix +import androidx.compose.ui.graphics.RadialGradientShader +import androidx.compose.ui.graphics.Shader +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.graphics.TileMode + +/** + * Creates an elliptical radial gradient brush that emulates the CSS radial-gradient spec. + * + * @param colors The colors to be distributed along the gradient. + * @param stops Optional color stops (0.0 to 1.0). + * @param radiusXPercent The horizontal radius as a percentage of the width (1.0 = 100%). + * @param radiusYPercent The vertical radius as a percentage of the height (1.0 = 100%). + * @param centerXPercent The horizontal center position as a percentage of the width. + * @param centerYPercent The vertical center position as a percentage of the height. + * @param tileMode The tile mode for the gradient. + */ +fun Brush.Companion.ellipticalGradient( + colors: List, + stops: List? = null, + radiusXPercent: Float, + radiusYPercent: Float, + centerXPercent: Float = 0.5f, + centerYPercent: Float = 0.5f, + tileMode: TileMode = TileMode.Clamp, +): ShaderBrush = object : ShaderBrush() { + override fun createShader(size: Size): Shader { + val rX = size.width * radiusXPercent + val rY = size.height * radiusYPercent + val cX = size.width * centerXPercent + val cY = size.height * centerYPercent + + this.transform = Matrix().apply { + reset() + translate(cX, cY) + scale(rX, rY) + } + + return RadialGradientShader( + colors = colors, + colorStops = stops, + center = Offset.Zero, + radius = 1f, + tileMode = tileMode, + ) + } +} \ No newline at end of file diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/PreviewWrapper.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/PreviewWrapper.kt new file mode 100644 index 0000000000..b751b6f028 --- /dev/null +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/PreviewWrapper.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalMediaQueryApi::class, ExperimentalComposeUiApi::class) + +package com.example.jetsnack.ui.utils + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.ComposeUiFlags +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.ExperimentalMediaQueryApi +import androidx.compose.ui.LocalUiMediaScope +import androidx.compose.ui.UiMediaScope +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.example.jetsnack.ui.LocalNavAnimatedVisibilityScope +import com.example.jetsnack.ui.LocalSharedTransitionScope +import com.example.jetsnack.ui.theme.JetsnackTheme + +@Composable +fun JetsnackThemeWrapper(content: @Composable () -> Unit) { + JetsnackTheme { + SharedTransitionLayout { + AnimatedVisibility(true) { + CompositionLocalProvider(LocalSharedTransitionScope provides this@SharedTransitionLayout) { + CompositionLocalProvider(LocalNavAnimatedVisibilityScope provides this@AnimatedVisibility) { + content() + } + } + } + } + } +} + +@Composable +fun UiMediaScopeWrapper( + keyboardKind: UiMediaScope.KeyboardKind = UiMediaScope.KeyboardKind.Virtual, + pointerPrecision: UiMediaScope.PointerPrecision = UiMediaScope.PointerPrecision.Blunt, + content: @Composable () -> Unit, +) { + + ComposeUiFlags.isMediaQueryIntegrationEnabled = true + BoxWithConstraints { + val uiMediaScope = object : UiMediaScope { + override val keyboardKind: UiMediaScope.KeyboardKind + get() = keyboardKind + override val windowPosture: UiMediaScope.Posture + get() = UiMediaScope.Posture.Flat + override val windowWidth = maxWidth // faking the width and height for previews + override val windowHeight = maxHeight + override val pointerPrecision = pointerPrecision + override val hasMicrophone = true + override val hasCamera = true + override val viewingDistance = UiMediaScope.ViewingDistance.Near + } + + JetsnackTheme { + CompositionLocalProvider(LocalUiMediaScope provides uiMediaScope) { + content() + } + } + } +} + +// Assuming KeyboardKind is your enum/class +@OptIn(ExperimentalMediaQueryApi::class) +class KeyboardKindProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + UiMediaScope.KeyboardKind.Physical, + UiMediaScope.KeyboardKind.Virtual, + ) +} +@OptIn(ExperimentalMediaQueryApi::class) +class PointerPrecisionProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + UiMediaScope.PointerPrecision.Fine, + UiMediaScope.PointerPrecision.Blunt, + UiMediaScope.PointerPrecision.Coarse, + UiMediaScope.PointerPrecision.None, + ) +} diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/SharedBoundsMorph.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/SharedBoundsMorph.kt new file mode 100644 index 0000000000..f64f47207d --- /dev/null +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/SharedBoundsMorph.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.utils + +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.BoundsTransform +import androidx.compose.animation.EnterExitState +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.tween +import androidx.compose.foundation.shape.GenericShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Matrix +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.asComposePath +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import androidx.graphics.shapes.CornerRounding +import androidx.graphics.shapes.Morph +import androidx.graphics.shapes.RoundedPolygon +import androidx.graphics.shapes.circle +import androidx.graphics.shapes.pill +import androidx.graphics.shapes.rectangle +import androidx.graphics.shapes.toPath +import com.example.jetsnack.ui.LocalNavAnimatedVisibilityScope +import com.example.jetsnack.ui.LocalSharedTransitionScope +import kotlin.math.max + +class MorphOverlayClip(val morph: Morph, private val animatedProgress: () -> Float) : SharedTransitionScope.OverlayClip { + private val matrix = Matrix() + + override fun getClipPath( + sharedContentState: SharedTransitionScope.SharedContentState, + bounds: Rect, + layoutDirection: LayoutDirection, + density: Density, + ): Path? { + matrix.reset() + val max = max(bounds.width, bounds.height) + matrix.scale(max, max) + + val path = morph.toPath(progress = animatedProgress.invoke()).asComposePath() + path.transform(matrix) + path.translate(bounds.center + Offset(-max / 2f, -max / 2f)) + return path + } +} + +@Composable +fun Modifier.sharedBoundsRevealWithShapeMorph( + sharedContentState: SharedTransitionScope.SharedContentState, + sharedTransitionScope: SharedTransitionScope? = LocalSharedTransitionScope.current, + animatedVisibilityScope: AnimatedVisibilityScope? = LocalNavAnimatedVisibilityScope.current, + boundsTransform: BoundsTransform = { tween(600) }, + resizeMode: SharedTransitionScope.ResizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds, + restingShape: RoundedPolygon = RoundedPolygon.rectangle().normalized(), + targetShape: RoundedPolygon = RoundedPolygon.circle().normalized(), + renderInOverlayDuringTransition: Boolean = true, + targetValueByState: @Composable (state: EnterExitState) -> Float = { + when (it) { + EnterExitState.PreEnter -> 1f + EnterExitState.Visible -> 0f + EnterExitState.PostExit -> 1f + } + }, + keepChildrenSizePlacement: Boolean = false, +): Modifier { + if (sharedTransitionScope == null || animatedVisibilityScope == null) return this + with(sharedTransitionScope) { + val animatedProgress = + animatedVisibilityScope.transition.animateFloat( + targetValueByState = targetValueByState, + transitionSpec = { + tween(300, easing = LinearEasing) + }, + ) + + val morph = remember { + Morph(restingShape, targetShape) + } + val morphClip = MorphOverlayClip(morph, { animatedProgress.value }) + val modifier = if (keepChildrenSizePlacement) { + Modifier + .skipToLookaheadSize() + .skipToLookaheadPosition() + } else { + Modifier + } + return this@sharedBoundsRevealWithShapeMorph + .sharedBounds( + sharedContentState = sharedContentState, + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = boundsTransform, + resizeMode = resizeMode, + clipInOverlayDuringTransition = morphClip, + renderInOverlayDuringTransition = renderInOverlayDuringTransition, + ) + .then(modifier) + } +} + +fun RoundedPolygon.asShape(): Shape = GenericShape { size: Size, _ -> + val matrix = Matrix().apply { scale(size.width, size.height) } + this.addPath(this@asShape.toPath().asComposePath()) + this.transform(matrix) +} + +fun Morph.asShape(progress: Float): Shape = GenericShape { size: Size, _ -> + val matrix = Matrix().apply { scale(size.width, size.height) } + this.addPath(this@asShape.toPath(progress).asComposePath()) + this.transform(matrix) +} + +object SnackPolygons { + val snackDetailPolygon = RoundedPolygon.pill(height = 1.85f).normalized() + + val snackItemPolygon = RoundedPolygon.rectangle( + perVertexRounding = listOf( + CornerRounding(0.25f), CornerRounding(0.25f), + CornerRounding(0.25f), CornerRounding(0.25f), + ), + ).normalized() + + val snackItemPolygonRounded = RoundedPolygon.pill(height = 1.80f).normalized() +} diff --git a/Jetsnack/app/src/main/res/font/montserrat_light.ttf b/Jetsnack/app/src/main/res/font/montserrat_light.ttf deleted file mode 100755 index 990857de8e..0000000000 Binary files a/Jetsnack/app/src/main/res/font/montserrat_light.ttf and /dev/null differ diff --git a/Jetsnack/app/src/main/res/font/montserrat_medium.ttf b/Jetsnack/app/src/main/res/font/montserrat_medium.ttf deleted file mode 100755 index 6e079f6984..0000000000 Binary files a/Jetsnack/app/src/main/res/font/montserrat_medium.ttf and /dev/null differ diff --git a/Jetsnack/app/src/main/res/font/montserrat_regular.ttf b/Jetsnack/app/src/main/res/font/montserrat_regular.ttf deleted file mode 100755 index 8d443d5d56..0000000000 Binary files a/Jetsnack/app/src/main/res/font/montserrat_regular.ttf and /dev/null differ diff --git a/Jetsnack/app/src/main/res/font/montserrat_semibold.ttf b/Jetsnack/app/src/main/res/font/montserrat_semibold.ttf deleted file mode 100755 index f8a43f2b20..0000000000 Binary files a/Jetsnack/app/src/main/res/font/montserrat_semibold.ttf and /dev/null differ diff --git a/Jetsnack/app/src/main/res/values-v23/font_certs.xml b/Jetsnack/app/src/main/res/values-v23/font_certs.xml new file mode 100644 index 0000000000..1ff72f47a4 --- /dev/null +++ b/Jetsnack/app/src/main/res/values-v23/font_certs.xml @@ -0,0 +1,31 @@ + + + + @array/com_google_android_gms_fonts_certs_dev + @array/com_google_android_gms_fonts_certs_prod + + + + MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs= + + + + + MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK + + + diff --git a/Jetsnack/gradle/libs.versions.toml b/Jetsnack/gradle/libs.versions.toml index c51b64c942..f11a21714d 100644 --- a/Jetsnack/gradle/libs.versions.toml +++ b/Jetsnack/gradle/libs.versions.toml @@ -1,18 +1,19 @@ [versions] accompanist = "0.37.3" -android-material3 = "1.14.0-alpha09" +android-material3 = "1.14.0-alpha10" androidGradlePlugin = "9.0.1" androidx-activity-compose = "1.12.4" androidx-appcompat = "1.7.1" -androidx-compose-bom = "2026.02.00" +androidx-compose-bom = "2026.03.00" androidx-core-splashscreen = "1.2.0" -androidx-corektx = "1.17.0" +androidx-corektx = "1.18.0" androidx-glance = "1.2.0-rc01" androidx-lifecycle = "2.8.2" androidx-lifecycle-compose = "2.10.0" androidx-lifecycle-runtime-compose = "2.10.0" androidx-navigation = "2.9.7" androidx-palette = "1.0.0" +androidx-startup = "1.2.0" androidx-test = "1.7.0" androidx-test-espresso = "3.7.0" androidx-test-ext-junit = "1.3.0" @@ -51,8 +52,11 @@ room = "2.8.4" secrets = "2.0.1" spotless = "8.2.1" # @keep +startupRuntime = "1.0.0" targetSdk = "36" version-catalog-update = "1.1.0" +uiTextGoogleFonts = "1.10.5" +graphicsShapes = "1.1.0" [libraries] accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" } @@ -63,9 +67,9 @@ androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-compose-animation = { module = "androidx.compose.animation:animation" } androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } -androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } +androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version = "1.12.0-SNAPSHOT" } androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" } -androidx-compose-material3 = { module = "androidx.compose.material3:material3" } +androidx-compose-material3 = { module = "androidx.compose.material3:material3", version = "1.5.0-alpha15" } androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive" } androidx-compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout" } androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation" } @@ -80,8 +84,8 @@ androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test" } androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } androidx-compose-ui-text = { module = "androidx.compose.ui:ui-text" } -androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } -androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version = "1.11.0-beta01" } +androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version = "1.11.0-beta01" } androidx-compose-ui-util = { module = "androidx.compose.ui:ui-util" } androidx-compose-ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-corektx" } @@ -103,6 +107,7 @@ androidx-palette = { module = "androidx.palette:palette", version.ref = "android androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +androidx-startup = { module = "androidx.startup:startup-runtime", version.ref = "androidx-startup" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso" } androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext-junit" } @@ -151,6 +156,8 @@ roborazzi-compose = { module = "io.github.takahirom.roborazzi:roborazzi-compose" roborazzi-rule = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi" } rometools-modules = { module = "com.rometools:rome-modules", version.ref = "rome" } rometools-rome = { module = "com.rometools:rome", version.ref = "rome" } +androidx-compose-ui-text-google-fonts = { group = "androidx.compose.ui", name = "ui-text-google-fonts", version.ref = "uiTextGoogleFonts" } +androidx-graphics-shapes = { group = "androidx.graphics", name = "graphics-shapes", version.ref = "graphicsShapes" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } diff --git a/Jetsnack/settings.gradle.kts b/Jetsnack/settings.gradle.kts index 3bc8533030..68987c4d84 100644 --- a/Jetsnack/settings.gradle.kts +++ b/Jetsnack/settings.gradle.kts @@ -13,28 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -val snapshotVersion : String? = System.getenv("COMPOSE_SNAPSHOT_ID") pluginManagement { repositories { - gradlePluginPortal() google() + gradlePluginPortal() mavenCentral() - maven { url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev/org/jetbrains/kotlin/kotlin-compose-compiler-plugin/2.0.0-RC2-200/") } + + maven { + // You can find the maven URL for other artifacts (e.g. KMP, METALAVA) on their + // build pages. + url = uri("https://androidx.dev/snapshots/builds/15080370/artifacts/repository") + } } } + dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { - snapshotVersion?.let { - println("https://androidx.dev/snapshots/builds/$it/artifacts/repository/") - maven { url = uri("https://androidx.dev/snapshots/builds/$it/artifacts/repository/") } - maven { url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev/org/jetbrains/kotlin/kotlin-compose-compiler-plugin/2.0.0-RC2-200/") } - } - google() mavenCentral() + maven { + // You can find the maven URL for other artifacts (e.g. KMP, METALAVA) on their + // build pages. + url = uri("https://androidx.dev/snapshots/builds/15080370/artifacts/repository") + } } } + rootProject.name = "Jetsnack" include(":app")