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")