diff --git a/compose/snippets/build.gradle.kts b/compose/snippets/build.gradle.kts index d09615d7d..aca58186b 100644 --- a/compose/snippets/build.gradle.kts +++ b/compose/snippets/build.gradle.kts @@ -29,7 +29,7 @@ android { defaultConfig { applicationId = "com.example.compose.snippets" - minSdk = libs.versions.minSdk.get().toInt() + minSdk = 23 targetSdk = libs.versions.targetSdk.get().toInt() versionCode = 1 versionName = "1.0" diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt index 4d59ea4ef..f5dca89af 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt @@ -61,6 +61,7 @@ import com.example.compose.snippets.ui.theme.SnippetsTheme import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +import androidx.compose.ui.platform.LocalLocale @Preview @Composable @@ -105,7 +106,7 @@ fun DatePickerExamples() { // [END_EXCLUDE] if (selectedDate != null) { val date = Date(selectedDate!!) - val formattedDate = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()).format(date) + val formattedDate = SimpleDateFormat("MMM dd, yyyy", LocalLocale.current.platformLocale).format(date) Text("Selected date: $formattedDate") } else { Text("No date selected") @@ -121,8 +122,8 @@ fun DatePickerExamples() { if (selectedDateRange.first != null && selectedDateRange.second != null) { val startDate = Date(selectedDateRange.first!!) val endDate = Date(selectedDateRange.second!!) - val formattedStartDate = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()).format(startDate) - val formattedEndDate = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()).format(endDate) + val formattedStartDate = SimpleDateFormat("MMM dd, yyyy", LocalLocale.current.platformLocale).format(startDate) + val formattedEndDate = SimpleDateFormat("MMM dd, yyyy", LocalLocale.current.platformLocale).format(endDate) Text("Selected date range: $formattedStartDate - $formattedEndDate") } else { Text("No date range selected") diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/CustomStates.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/CustomStates.kt new file mode 100644 index 000000000..c033041ea --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/CustomStates.kt @@ -0,0 +1,173 @@ +/* + * 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) + +package com.example.compose.snippets.designsystems.styles + +import androidx.compose.foundation.layout.Box +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.styleable +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.unit.dp + + +// [START android_compose_styles_custom_key_1] +enum class PlayerState { + Stopped, + Playing, + Paused +} + +val playerStateKey = StyleStateKey(PlayerState.Stopped) +// [END android_compose_styles_custom_key_1] + +// [START android_compose_styles_custom_key_2] +// Extension Function on MutableStyleState to query and set the current playState +var MutableStyleState.playerState + get() = this[playerStateKey] + set(value) { this[playerStateKey] = value } + +fun StyleScope.playerPlaying(value: Style) { + state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing }) +} +fun StyleScope.playerPaused(value: Style) { + state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused }) +} +// [END android_compose_styles_custom_key_2] + +private object Step2StyleState { + // [START android_compose_styles_link_to_custom_state_pass] + @Composable + fun MediaPlayer( + url: String, + modifier: Modifier = Modifier, + style: Style = Style, + state: PlayerState = remember { PlayerState.Paused } + ) { + // Hoist style state, set playstate as a parameter, + val styleState = remember { MutableStyleState(null) } + // Set equal to incoming state to link the two together + styleState.playerState = state + Box( + modifier = modifier.styleable(styleState, style)) { + ///.. + } + } + // [END android_compose_styles_link_to_custom_state_pass] + + // [START android_compose_styles_link_to_custom_state_key] + @Composable + fun StyleStateKeySample() { + // Using the extension function to change the border color to green while playing + val style = Style { + borderColor(Color.Gray) + playerPlaying { + animate { + borderColor(Color.Green) + } + } + playerPaused { + animate { + borderColor(Color.Blue) + } + } + } + val styleState = remember { MutableStyleState(null) } + styleState[playerStateKey] = PlayerState.Playing + + // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too. + MediaPlayer(url = "https://example.com/media/video", + style = style, + state = PlayerState.Stopped) + } + // [END android_compose_styles_link_to_custom_state_key] +} + +private object Step4FullSnippetState { + // [START android_compose_styles_state_full_snippet] + enum class PlayerState { + Stopped, + Playing, + Paused + } + val playerStateKey = StyleStateKey(PlayerState.Stopped) + var MutableStyleState.playerState + get() = this[playerStateKey] + set(value) { this[playerStateKey] = value } + + fun StyleScope.playerPlaying(value: Style) { + state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing }) + } + fun StyleScope.playerPaused(value: Style) { + state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused }) + + } + + @Composable + fun MediaPlayer( + url: String, + modifier: Modifier = Modifier, + style: Style = Style, + state: PlayerState = remember { PlayerState.Paused } + ) { + // Hoist style state, set playstate as a parameter, + val styleState = remember { MutableStyleState(null) } + // Set equal to incoming state to link the two together + styleState.playerState = state + Box( + modifier = modifier.styleable(styleState, Style { + size(100.dp) + border(2.dp, Color.Red) + + }, style, )) { + + ///.. + } + } + @Composable + fun StyleStateKeySample() { + // Using the extension function to change the border color to green while playing + val style = Style { + borderColor(Color.Gray) + playerPlaying { + animate { + borderColor(Color.Green) + } + } + playerPaused { + animate { + borderColor(Color.Blue) + } + } + } + val styleState = remember { MutableStyleState(null) } + styleState[playerStateKey] = PlayerState.Playing + + // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too. + MediaPlayer(url = "https://example.com/media/video", + style = style, + state = PlayerState.Stopped) + } + // [END android_compose_styles_state_full_snippet] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/DosDonts.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/DosDonts.kt new file mode 100644 index 000000000..6789924a7 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/DosDonts.kt @@ -0,0 +1,211 @@ +/* + * 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) + +package com.example.compose.snippets.designsystems.styles + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +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.styleable +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.example.compose.snippets.designsystems.styles.CustomThemingWithStyles.JetsnackColors + + +// [START android_compose_styles_dos_expose_style] +@Composable +fun GradientButton( + modifier: Modifier = Modifier, + // ✅ DO: for design system components, expose a style modifier to consumers to be able to customize the components + style: Style = Style +) { + // Consume the style +} +// [END android_compose_styles_dos_expose_style] + +// [START android_compose_styles_dos_replace_params] +// Before +@Composable +fun OldButton(background: Color, fontColor: Color) { +} + +// After +// ✅ DO: Replace visual-based parameters with a style that includes same properties +@Composable +fun NewButton(style: Style = Style) { +} +// [END android_compose_styles_dos_replace_params] + +// [START android_compose_styles_dos_wrapper] +@Composable +fun BaseButton( + modifier: Modifier = Modifier, + style: Style = Style +) { + // Uses LocalTheme.appStyles.button + incoming style +} + +// ✅ Do create wrapper composables that expose common implementations of the same component +@Composable +fun SpecialGradientButton( + modifier: Modifier = Modifier, + style: Style = Style +) { + // Uses LocalTheme.appStyles.button + LocalTheme.appStyles.gradientButton + incoming style - merge these styles +} +// [END android_compose_styles_dos_wrapper] + +// [START android_compose_styles_donts_default_style] +@Composable +fun BadButton( + modifier: Modifier = Modifier, + // ❌ DON'T set a default style here as a parameter + style: Style = Style { background(Color.Red) } +) { +} +// [END android_compose_styles_donts_default_style] + +// [START android_compose_styles_do_default_style] +@Composable +fun GoodButton( + modifier: Modifier = Modifier, + // ✅ Do: always pass it as a Style, do not pass other defaults + style: Style = Style +) { + // [START_EXCLUDE] + // this is a snippet of the BaseButton - see the full snippet in /components/Button.kt + val effectiveInteractionSource = remember { + MutableInteractionSource() + } + val styleState = remember(effectiveInteractionSource) { + MutableStyleState(effectiveInteractionSource) + } + // [END_EXCLUDE] + val defaultStyle = Style { background(Color.Red) } + // ✅ Do Combine defaults inside with incoming parameter + Box(modifier = modifier.styleable(styleState, defaultStyle, style)) { + // your logic + } +} +// [END android_compose_styles_do_default_style] + +private object ThemingDoDonts { + object JetsnackStyles { + + } + val LightJetsnackColors = JetsnackColors( + isDark = false, + brand = Color.Magenta, + brandLight = Color.LightGray, + uiBackground = Color.LightGray, + textPrimary = Color.Black, + brandSecondary = Color.Blue + ) + + val LocalJetsnackTheme: ProvidableCompositionLocal + get() = LocalJetsnackThemeInstance + + internal val LocalJetsnackThemeInstance = staticCompositionLocalOf { JetsnackTheme() } + + @Immutable + class JetsnackTheme( + val colors: JetsnackColors = LightJetsnackColors, + val typography: androidx.compose.material3.Typography = androidx.compose.material3.Typography(), + val shapes: Shapes = Shapes(), + ) { + companion object { + val colors: JetsnackColors + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.colors + + val typography: androidx.compose.material3.Typography + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.typography + + val shapes: Shapes + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.shapes + + val styles: JetsnackStyles = JetsnackStyles + val LocalJetsnackTheme: ProvidableCompositionLocal + get() = LocalJetsnackThemeInstance + } + } + + // [START android_compose_styles_theming_dos_composition] + // DON'T - Create styles in Composition that access composition locals in this way - this will likely lead to issues when style is used / accessed, as it would not get updated when the value changes. + @Composable + fun containerStyle(): Style { + val background = MaterialTheme.colorScheme.background + val onBackground = MaterialTheme.colorScheme.onBackground + return Style { + background(background) + contentColor(onBackground) + } + } + + // Do: Instead, Create StyleScope extension functions for your subsystems to access themed composition Locals + val StyleScope.colors: JetsnackColors + get() = JetsnackTheme.LocalJetsnackTheme.currentValue.colors + + val StyleScope.typography: androidx.compose.material3.Typography + get() = JetsnackTheme.LocalJetsnackTheme.currentValue.typography + val StyleScope.shapes: Shapes + get() = JetsnackTheme.LocalJetsnackTheme.currentValue.shapes + // Access CompositionLocals + val button = Style { + background(colors.brandSecondary) + shape(shapes.small) + } + // [END android_compose_styles_theming_dos_composition] + + // [START android_compose_styles_themed_values] + // Do: Use CompositionLocals or themed values to create a single style + val buttonStyle = Style { + background(colors.brandSecondary) + shape(shapes.small) + } + // [END android_compose_styles_themed_values] + + // [START android_compose_styles_switch_product] + // DO Switch out whole styles when many properties differ - if Product A and Product B are two white labelled apps that provide different Themes. + val productBThemedButton = Style { + shape(shapes.small) + background(colors.brandSecondary) + // other properties are fundamentally different + } + + val productAThemedButton = Style { + shape(shapes.large) + background(colors.brand) + // other properties are fundamentally different + } + // [END android_compose_styles_switch_product] +} + diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/Examples.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/Examples.kt new file mode 100644 index 000000000..0a020bd7d --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/Examples.kt @@ -0,0 +1,222 @@ +/* + * 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) + +package com.example.compose.snippets.designsystems.styles + +import androidx.compose.animation.core.tween +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +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.fillSize +import androidx.compose.foundation.style.hovered +import androidx.compose.foundation.style.pressed +import androidx.compose.foundation.style.styleable +import androidx.compose.runtime.Composable +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.shadow.Shadow +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.toUpperCase +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.designsystems.styles.components.BaseButton +import com.example.compose.snippets.designsystems.styles.components.BaseText + + +// [START android_compose_styles_hover_button] +@Preview +@Composable +fun HoverButtonExample() { + Box( + modifier = Modifier.padding(32.dp), + contentAlignment = Alignment.Center + ) { + BaseButton( + onClick = {}, + style = Style { + background(Color.Transparent) + shape(RoundedCornerShape(0.dp)) + border(1.dp, Color.Black) + contentColor(Color.Black) + fontSize(16.sp) + fontWeight(FontWeight.Light) + letterSpacing(1.sp) + contentPadding(vertical = 13.dp, horizontal = 20.dp) + dropShadow( + Shadow( + spread = 0.dp, color = Color(0xFFFFE54C), + radius = 0.dp, + offset = DpOffset(7.dp, 7.dp) + ) + ) + hovered { + animate(tween(200)) { + dropShadow( + Shadow( + spread = 0.dp, color = Color(0xFFFFE54C), + radius = 0.dp, + offset = DpOffset(0.dp, 0.dp) + ) + ) + } + } + pressed { + animate(tween(200)) { + dropShadow( + Shadow( + spread = 0.dp, color = Color(0xFFFFE54C), + radius = 0.dp, + offset = DpOffset(0.dp, 0.dp) + ) + ) + } + } + } + ) { + BaseText("Button 52") + } + } +} +// [END android_compose_styles_hover_button] + +// [START android_compose_styles_rounded_depth_button] +@Preview +@Composable +fun ShadowAnimationButton() { + Box(modifier = Modifier.padding(32.dp)) { + val density = LocalDensity.current + val buttonStyle = Style { + background(Color(0xFFFBEED0)) + border(2.dp, Color(0xFF422800)) + shape(RoundedCornerShape(30.dp)) + dropShadow( + Shadow( + color = Color(0xFF422800), offset = DpOffset(4.dp, 4.dp), + radius = 0.dp, spread = 0.dp + ) + ) + contentColor(Color(0xFF422800)) + fontWeight(FontWeight.SemiBold) + fontSize(18.sp) + contentPaddingHorizontal(25.dp) + externalPadding(8.dp) + height(50.dp) + textAlign(TextAlign.Center) + hovered { + animate { + background(Color.White) + } + } + pressed { + animate { + dropShadow( + Shadow( + color = Color(0xFF422800), + offset = DpOffset(2.dp, 2.dp), + radius = 0.dp, + spread = 0.dp + ) + ) + translation(with(density) { 2.dp.toPx() }, with(density) { 2.dp.toPx() }) + } + } + } + BaseButton( + onClick = {}, + style = buttonStyle + ) { + BaseText("Button 74") + } + } +} +// [END android_compose_styles_rounded_depth_button] + +// [START android_compose_styles_depth_pressed_button] +@Preview +@Composable +fun MultipleStylesButton() { + val interactionSource = remember { MutableInteractionSource() } + val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } + val density = LocalDensity.current + + Box( + modifier = Modifier + .styleable(styleState) { + size(200.dp, 48.dp) + externalPadding(32.dp) + } + .clickable(interactionSource, indication = null) {}, + contentAlignment = Alignment.Center + ) { + val edgeStyle = Style { + fillSize() + shape(RoundedCornerShape(16.dp)) + background(Color(0xFF1CB0F6)) + } + + val frontStyle = Style { + fillSize() + background(Color(0xFF1899D6)) + shape(RoundedCornerShape(16.dp)) + contentPadding(vertical = 12.dp, horizontal = 16.dp) + translationY(with(density) { (-4).dp.toPx() }) + pressed { + animate { + translationY(with(density) { (0).dp.toPx() }) + } + } + } + Box(modifier = Modifier + .semantics(properties = { + role = Role.Button + }) + .styleable(styleState, edgeStyle)) { + Box( + modifier = Modifier + .styleable(styleState, frontStyle), + contentAlignment = Alignment.Center + ) { + BaseText( + "Button 19".toUpperCase(Locale.current), + style = Style { + contentColor(Color.White) + fontSize(15.sp) + fontWeight(FontWeight.Bold) + letterSpacing(0.8.sp) + } + ) + } + } + } +} +// [END android_compose_styles_depth_pressed_button] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/Skill.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/Skill.kt new file mode 100644 index 000000000..d56f6852e --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/Skill.kt @@ -0,0 +1,161 @@ +/* + * 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) + +package com.example.compose.snippets.designsystems.styles + +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.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.style.ExperimentalFoundationStyleApi +import androidx.compose.foundation.style.Style +import androidx.compose.foundation.style.StyleScope +import androidx.compose.foundation.style.disabled +import androidx.compose.foundation.style.rememberUpdatedStyleState +import androidx.compose.foundation.style.styleable +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.Shapes +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ReadOnlyComposable +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.text.TextStyle +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.designsystems.styles.CustomThemingWithStyles.JetsnackColors +import com.example.compose.snippets.designsystems.styles.CustomThemingWithStyles.JetsnackTheme.Companion.LocalJetsnackTheme + + + +// [START android_compose_styles_skill_component_styles] +object ExampleComponentStyles { + val customButtonStyle: Style = { + + } + val customTextFieldStyle: Style = { + + } +} +// [END android_compose_styles_skill_component_styles] + +// [START android_compose_styles_skill_component_styles_theme] +@Immutable +class JetsnackTheme( + // other Design system properties +) { + companion object { + val colors: CustomThemingWithStyles.JetsnackColors + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.colors + // [START_EXCLUDE] + val shapes: Shapes + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.shapes + val typography: Typography + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.typography + // [END_EXCLUDE] + + // add helper static reference + val styles: ComponentStyles = ComponentStyles + } +} +// [END android_compose_styles_skill_component_styles_theme] + + +// [START android_compose_styles_skill_scope_ext] +val StyleScope.colors: JetsnackColors + get() = LocalJetsnackTheme.currentValue.colors + +val StyleScope.typography: androidx.compose.material3.Typography + get() = LocalJetsnackTheme.currentValue.typography + +val StyleScope.shapes: Shapes + get() = LocalJetsnackTheme.currentValue.shapes + +// [END android_compose_styles_skill_scope_ext] + +// [START android_compose_styles_skill_before_migration] +@Composable +fun CustomButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + backgroundColor: Color = JetsnackTheme.colors.brandLight, + disabledBackgroundColor: Color = JetsnackTheme.colors.brandSecondary, + shape: Shape = JetsnackTheme.shapes.extraLarge, + textStyle: TextStyle = JetsnackTheme.typography.labelLarge, + enabled: Boolean = true, + content: @Composable RowScope.() -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + Row( + modifier + .clickable(onClick = onClick, indication = null, interactionSource = interactionSource) + .background(if (enabled) backgroundColor else disabledBackgroundColor, shape) + .defaultMinSize(58.dp, 40.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + content = content, + ) +} +// [END android_compose_styles_skill_before_migration] + +// [START android_compose_styles_skill_after_migration] +// Exposed via ComponentStyles.kt +object ComponentStyles { + val buttonStyle = Style { + background(colors.brandLight) + shape(shapes.extraLarge) + minWidth(58.dp) + minHeight(40.dp) + textStyle(typography.labelLarge) + disabled { + background(colors.brandSecondary) + } + } +} + +@Composable +fun CustomButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + style: Style = Style, + enabled: Boolean = true, + content: @Composable RowScope.() -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + val styleState = rememberUpdatedStyleState(interactionSource) { + it.isEnabled = enabled + } + Row( + modifier + .clickable(onClick = onClick, indication = null, interactionSource = interactionSource) + .styleable(styleState, JetsnackTheme.styles.buttonStyle, style), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + content = content, + ) +} +// [END android_compose_styles_skill_after_migration] \ No newline at end of file diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/StateAnimations.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/StateAnimations.kt new file mode 100644 index 000000000..4770f7fef --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/StateAnimations.kt @@ -0,0 +1,257 @@ +/* + * 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) + +package com.example.compose.snippets.designsystems.styles + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +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.focused +import androidx.compose.foundation.style.hovered +import androidx.compose.foundation.style.pressed +import androidx.compose.foundation.style.rememberUpdatedStyleState +import androidx.compose.foundation.style.styleable +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.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.designsystems.styles.components.BaseButton +import com.example.compose.snippets.designsystems.styles.components.BaseText + +val outlinedButtonStyle = Style { + externalPadding(48.dp) + contentPadding(12.dp) + shape(RoundedCornerShape(8.dp)) + clip(true) + border(2.dp, lightBlue) +} + +val lightBlue = Color(0xFF03A9F4) +val lightPurple = Color(0xFF9575CD) +val lightOrange = Color(0xFFFFE0B2) +val lightRed = Color(0xFFE57373) + +// [START android_compose_styles_state_basic] +@Preview +@Composable +private fun OpenButton() { + BaseButton( + style = outlinedButtonStyle then { + background(Color.White) + hovered { + background(lightPurple) + border(2.dp, lightPurple) + } + focused { + background(lightBlue) + } + }, + onClick = { }, + content = { + BaseText("Open in Studio", style = { + contentColor(Color.Black) + fontSize(26.sp) + textAlign(TextAlign.Center) + }) + } + ) +} + +// [END android_compose_styles_state_basic] + +@Preview +// [START android_compose_styles_state_basic_combined_states] +@Composable +private fun OpenButton_CombinedStates() { + BaseButton( + style = outlinedButtonStyle then { + background(Color.White) + hovered { + // light purple + background(lightPurple) + pressed { + // When running on a device that can hover, whilst hovering and then pressing the button this would be invoked + background(lightOrange) + } + } + pressed { + // when running on a device without a mouse attached, this would be invoked as you wouldn't be in a hovered state only + background(lightRed) + } + focused { + background(lightBlue) + } + }, + onClick = { }, + content = { + BaseText("Open in Studio", style = { + contentColor(Color.Black) + fontSize(26.sp) + textAlign(TextAlign.Center) + }) + } + ) +} +// [END android_compose_styles_state_basic_combined_states] + +val baseGradientButtonStyle = Style { + contentPadding(16.dp) + externalPadding(64.dp) + fontSize(16.sp) + shape(RoundedCornerShape(16.dp)) + clip(true) + background(Brush.linearGradient(listOf(lightPurple, lightBlue))) +} + +// [START android_compose_styles_state_custom_state_gradient] +@Composable +private fun GradientButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + style: Style = Style, + enabled: Boolean = true, + interactionSource: MutableInteractionSource? = null, + content: @Composable RowScope.() -> Unit, +) { + val interactionSource = interactionSource ?: remember { MutableInteractionSource() } + val styleState = rememberUpdatedStyleState(interactionSource) { + it.isEnabled = enabled + } + Row( + modifier = + modifier + .clickable( + onClick = onClick, + enabled = enabled, + interactionSource = interactionSource, + indication = null, + ) + .styleable(styleState, baseGradientButtonStyle then style), + content = content, + ) +} +// [END android_compose_styles_state_custom_state_gradient] + +// [START android_compose_styles_state_override] +@Preview +@Composable +fun LoginButton() { + val loginButtonStyle = Style { + pressed { + background( + Brush.linearGradient( + listOf(Color.Magenta, Color.Red) + ) + ) + } + } + GradientButton(onClick = { + // Login logic + }, style = loginButtonStyle) { + BaseText("Login") + } +} +// [END android_compose_styles_state_override] + +// [START android_compose_styles_animate_style_basic] +val animatingStyle = Style { + externalPadding(48.dp) + border(3.dp, Color.Black) + background(Color.White) + size(100.dp) + + pressed { + animate { + borderColor(Color.Magenta) + background(Color(0xFFB39DDB)) + } + } +} + +@Preview +@Composable +private fun AnimatingStyleChanges() { + val interactionSource = remember { MutableInteractionSource() } + val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } + Box(modifier = Modifier + .clickable( + interactionSource, + enabled = true, + indication = null, + onClick = { + + } + ) + .styleable(styleState, animatingStyle)) { + + } +} +// [END android_compose_styles_animate_style_basic] + +// [START android_compose_styles_animate_style_set_spec] +val animatingStyleSpec = Style { + externalPadding(48.dp) + border(3.dp, Color.Black) + background(Color.White) + size(100.dp) + transformOrigin(TransformOrigin.Center) + pressed { + animate { + borderColor(Color.Magenta) + background(Color(0xFFB39DDB)) + } + animate(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) { + scale(1.2f) + } + } +} + +@Preview(showBackground = true) +@Composable +fun AnimatingStyleChangesSpec() { + val interactionSource = remember { MutableInteractionSource() } + val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } + Box(modifier = Modifier + .clickable( + interactionSource, + enabled = true, + indication = null, + onClick = { + + } + ) + .styleable(styleState, animatingStyleSpec)) +} +// [END android_compose_styles_animate_style_set_spec] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/StylesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/StylesSnippets.kt new file mode 100644 index 000000000..c45dba8ab --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/StylesSnippets.kt @@ -0,0 +1,351 @@ +/* + * 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) +@file:Suppress("Unused", "UnusedVariable") + +package com.example.compose.snippets.designsystems.styles + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.styleable +import androidx.compose.foundation.style.then +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.designsystems.FullyCustomDesignSystem.LocalCustomColors +import com.example.compose.snippets.designsystems.styles.components.BaseButton +import com.example.compose.snippets.designsystems.styles.components.BaseText + +@Composable +fun BasicButtonStyle() { + // [START android_compose_styles_basic_button] + BaseButton( + onClick = { }, + style = { } + ) { + BaseText("Click me") + } + // [END android_compose_styles_basic_button] +} + +@Composable +fun ButtonBackgroundStyle() { + // [START android_compose_styles_button_background] + BaseButton( + onClick = { }, + style = { background(Color.Blue) } + ) { + BaseText("Click me") + } + // [END android_compose_styles_button_background] +} + + +@Composable +fun RowStyleable() { + // [START android_compose_styles_row_styleable] + Row( + modifier = Modifier.styleable { } + ) { + BaseText("Content") + } + // [END android_compose_styles_row_styleable] +} + + +@Composable +fun RowStyleableBackground() { + // [START android_compose_styles_row_styleable_background] + Row( + modifier = Modifier.styleable { + background(Color.Blue) + } + ) { + BaseText("Content") + } + // [END android_compose_styles_row_styleable_background] +} + + +// [START android_compose_styles_standalone_style] +val style = Style { background(Color.Blue) } +// [END android_compose_styles_standalone_style] + +@Composable +fun StandaloneStyleUsage() { + // [START android_compose_styles_standalone_usage] + val style = Style { background(Color.Blue) } + + // built in parameter + BaseButton(onClick = { }, style = style) { + BaseText("Button") + } + + // modifier styleable + val styleState = remember { MutableStyleState(null) } + Column( + Modifier.styleable(styleState, style) + ) { + BaseText("Column content") + } + // [END android_compose_styles_standalone_usage] +} + +@Composable +fun MultipleComponentsStyle() { + +// [START android_compose_styles_multiple_components] + val style = Style { background(Color.Blue) } + + // built in parameter + BaseButton(onClick = { }, style = style) { + BaseText("Button") + } + BaseText("Different text that uses the same style parameter", style = style) + + // modifier styleable + val columnStyleState = remember { MutableStyleState(null) } + Column( + Modifier.styleable(columnStyleState, style) + ) { + BaseText("Column") + } + val rowStyleState = remember { MutableStyleState(null) } + Row( + Modifier.styleable(rowStyleState, style) + ) { + BaseText("Row") + } + // [END android_compose_styles_multiple_components] +} + + +@Composable +fun MultiplePropertiesStyle() { + +// [START android_compose_styles_multiple_properties] + BaseButton( + onClick = { }, + style = { + background(Color.Blue) + contentPaddingStart(16.dp) + } + ) { + BaseText("Button") + } + // [END android_compose_styles_multiple_properties] +} + + +val TealColor = Color(0xFF008080) + + +@Composable +fun OverwritePropertiesStyle() { + // [START android_compose_styles_overwrite_properties] + BaseButton( + style = { + background(Color.Red) + // Background of Red is now overridden with TealColor instead + background(TealColor) + // All directions of padding are set to 64.dp (top, start, end, bottom) + contentPadding(64.dp) + // Top padding is now set to 16.dp, all other paddings remain at 64.dp + contentPaddingTop(16.dp) + }, + onClick = { + // + } + ) { + BaseText("Click me!") + } + // [END android_compose_styles_overwrite_properties] +} + + + +@Composable +fun MergeStyles() { + // [START android_compose_styles_merge_styles] + val style1 = Style { background(TealColor) } + val style2 = Style { contentPaddingTop(16.dp) } + + BaseButton( + style = style1 then style2, + onClick = { + + }, + ) { + BaseText("Click me!") + } + // [END android_compose_styles_merge_styles] +} + +@Composable +fun MergeOverwriteStyles() { + // [START android_compose_styles_merge_overwrite] + val style1 = Style { + background(Color.Red) + contentPadding(32.dp) + } + + val style2 = Style { + contentPaddingHorizontal(8.dp) + background(Color.LightGray) + } + + BaseButton( + style = style1 then style2, + onClick = { + + }, + ) { + BaseText("Click me!") + } + // [END android_compose_styles_merge_overwrite] +} + +@Composable +fun ParentStyling() { + // [START android_compose_styles_parent_styling] + val styleState = remember { MutableStyleState(null) } + Column( + modifier = Modifier.styleable(styleState) { + background(Color.LightGray) + val blue = Color(0xFF4285F4) + val purple = Color(0xFFA250EA) + val colors = listOf(blue, purple) + contentBrush(Brush.linearGradient(colors)) + }, + ) { + BaseText("Children inherit", style = { width(60.dp) }) + BaseText("certain properties") + BaseText("from their parents") + } + // [END android_compose_styles_parent_styling] +} + + +@Composable +fun ChildOverrideStyling() { + // [START android_compose_styles_child_override] + val styleState = remember { MutableStyleState(null) } + Column( + modifier = Modifier.styleable(styleState) { + background(Color.LightGray) + val blue = Color(0xFF4285F4) + val purple = Color(0xFFA250EA) + val colors = listOf(blue, purple) + contentBrush(Brush.linearGradient(colors)) + }, + ) { + BaseText("Children can ", style = { + contentBrush(Brush.linearGradient(listOf(Color.Red, Color.Blue))) + }) + BaseText("override properties") + BaseText("set by their parents") + } + // [END android_compose_styles_child_override] +} + +// [START android_compose_styles_custom_extension] +fun StyleScope.outlinedBackground(color: Color) { + border(1.dp, color) + background(color) +} +// [END android_compose_styles_custom_extension] + +// [START android_compose_styles_custom_extension_usage] +val customExtensionStyle = Style { + outlinedBackground(Color.Blue) +} +// [END android_compose_styles_custom_extension_usage] + +@Composable +fun CompositionLocalStyle() { + // [START android_compose_styles_composition_local] + val buttonStyle = Style { + contentPadding(12.dp) + shape(RoundedCornerShape(50)) + background(Brush.verticalGradient(LocalCustomColors.currentValue.background)) + } + // [END android_compose_styles_composition_local] +} + +// [START android_compose_styles_design_system_component] +@Composable +fun LoginButtonSnippet(modifier: Modifier = Modifier, style: Style = Style) { + // Your custom component applying the style via the styleable modifier + // e.g., Box(modifier = modifier.styleable(styleState, style)) +} +// [END android_compose_styles_design_system_component] + +// [START android_compose_styles_theme_integration] +@Immutable +data class AppStyles( + val baseButtonStyle: Style = Style, + val baseTextStyle: Style = Style, + val baseCardStyle: Style = Style +) + +val LocalAppStyles = compositionLocalOf { AppStyles() } + +@Composable +fun CustomTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val styles = AppStyles() + CompositionLocalProvider( + LocalAppStyles provides styles, + content = content + ) +} + +@Composable +fun CustomButton( + modifier: Modifier = Modifier, + style: Style = Style, + text: String +) { + val interactionSource = remember { MutableInteractionSource() } + val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } + + Box( + modifier = modifier + .clickable(interactionSource = interactionSource, onClick = { }) + .styleable(styleState, LocalAppStyles.current.baseButtonStyle, style) + ) { + BaseText(text) + } +} +// [END android_compose_styles_theme_integration] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/Theming.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/Theming.kt new file mode 100644 index 000000000..eaad2222c --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/Theming.kt @@ -0,0 +1,235 @@ +/* + * 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) + +package com.example.compose.snippets.designsystems.styles + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +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.disabled +import androidx.compose.foundation.style.hovered +import androidx.compose.foundation.style.styleable +import androidx.compose.foundation.style.then +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.material3.Text +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.remember +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.shadow.Shadow +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.designsystems.styles.CustomThemingWithStyles.JetsnackTheme.Companion.LocalJetsnackTheme + + +object ThemingStyles { + // [START android_compose_styles_theming_atomic_styles] + + // Define single-purpose "atomic" styles + val paddingAtomic = Style { + contentPadding(16.dp) + } + val roundedCornerShapeAtomic = Style { + shape(RoundedCornerShape(8.dp)) + } + val primaryBackgroundAtomic = Style { + background(Color.Blue) + } + val largeSizeAtomic = Style { + size(100.dp, 40.dp) + } + val interactiveShadowAtomic = Style { + hovered { + animate { + dropShadow( + Shadow( + offset = DpOffset( + 0.dp, + 0.dp + ), + radius = 2.dp, + spread = 0.dp, + color = Color.Blue, + ) + ) + } + } + } + + // [END android_compose_styles_theming_atomic_styles] + + // [START android_compose_styles_theming_traditional_non_atomic] + // One large monolithic style + val buttonStyle = Style { + contentPadding(16.dp) + shape(RoundedCornerShape(8.dp)) + background(Color.Blue) + } + // [END android_compose_styles_theming_traditional_non_atomic] + + object AtomicThenExample { + // [START android_compose_styles_theming_traditional_atomic_then] + // Combine atoms to create the final appearance + val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then interactiveShadowAtomic + // [END android_compose_styles_theming_traditional_atomic_then] + } +} + +object CustomThemingWithStyles { + // [START android_compose_styles_default_object] + object JetsnackStyles{ + val buttonStyle: Style = Style { + shape(shapes.medium) + background(colors.brand) + contentColor(colors.textPrimary) + contentPaddingVertical(8.dp) + contentPaddingHorizontal(24.dp) + textStyle(typography.labelLarge) + disabled { + animate { + background(colors.brandSecondary) + } + } + } + val cardStyle: Style = Style { + shape(shapes.medium) + background(colors.uiBackground) + contentColor(colors.textPrimary) + } + } + // [END android_compose_styles_default_object] + + @Immutable + data class JetsnackColors( + val brand: Color, + val brandLight: Color, + val brandSecondary: Color, + val uiBackground: Color, + val textPrimary: Color, + val isDark: Boolean, + ) + val LightJetsnackColors = JetsnackColors( + isDark = false, + brand = Color.Magenta, + brandLight = Color.LightGray, + uiBackground = Color.LightGray, + textPrimary = Color.Black, + brandSecondary = Color.Blue + ) + val DarkJetsnackColors = JetsnackColors( + isDark = true, + brand = Color.Magenta, + brandLight = Color.LightGray, + uiBackground = Color.DarkGray, + textPrimary = Color.White, + brandSecondary = Color.Blue + ) + // Theme.kt + // [START android_compose_styles_custom_theme] + @Immutable + class JetsnackTheme( + val colors: JetsnackColors = LightJetsnackColors, + val typography: androidx.compose.material3.Typography = androidx.compose.material3.Typography(), + val shapes: Shapes = Shapes() + ) { + companion object { + val colors: JetsnackColors + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.colors + + val typography: androidx.compose.material3.Typography + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.typography + + val shapes: Shapes + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.shapes + + val styles: JetsnackStyles = JetsnackStyles + + val LocalJetsnackTheme: ProvidableCompositionLocal + get() = LocalJetsnackThemeInstance + } + } + + val StyleScope.colors: JetsnackColors + get() = LocalJetsnackTheme.currentValue.colors + + val StyleScope.typography: androidx.compose.material3.Typography + get() = LocalJetsnackTheme.currentValue.typography + + val StyleScope.shapes: Shapes + get() = LocalJetsnackTheme.currentValue.shapes + + internal val LocalJetsnackThemeInstance = staticCompositionLocalOf { JetsnackTheme() } + + @Composable + fun JetsnackTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { + val colors = if (darkTheme) DarkJetsnackColors else LightJetsnackColors + val theme = JetsnackTheme(colors = colors) + + CompositionLocalProvider( + LocalJetsnackTheme provides theme, + ) { + MaterialTheme( + typography = LocalJetsnackTheme.current.typography, + shapes = LocalJetsnackTheme.current.shapes, + content = content, + ) + } + } + // [END android_compose_styles_custom_theme] + + // [START android_compose_styles_custom_button_theme] + @Composable + fun CustomButton(modifier: Modifier, + style: Style = Style, + text: String) { + val interactionSource = remember { MutableInteractionSource() } + val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } + + // Apply style to top level container in combination with incoming style from parameter. + Box(modifier = modifier + .clickable( + interactionSource = interactionSource, + indication = null, + enabled = true, + role = Role.Button, + onClick = { + + }, + ) + .styleable(styleState, JetsnackTheme.styles.buttonStyle, style)) { + Text(text) + } + } + // [END android_compose_styles_custom_button_theme] +} \ No newline at end of file diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/components/Button.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/components/Button.kt new file mode 100644 index 000000000..46c54f215 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/components/Button.kt @@ -0,0 +1,75 @@ +/* + * 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) + +package com.example.compose.snippets.designsystems.styles.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.style.ExperimentalFoundationStyleApi +import androidx.compose.foundation.style.MutableStyleState +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.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics + +val baseButtonStyle = Style { + +} + +@ExperimentalFoundationStyleApi +// [START android_compose_styles_base_button] +@Composable +fun BaseButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + style: Style = Style, + enabled: Boolean = true, + interactionSource: MutableInteractionSource? = remember { + MutableInteractionSource() + }, + content: @Composable RowScope.() -> Unit +) { + val styleState = rememberUpdatedStyleState(interactionSource) { + it.isEnabled = enabled + } + Row( + modifier = modifier + .semantics(properties = { + role = Role.Button + }) + .clickable( + enabled = enabled, + onClick = onClick, + interactionSource = interactionSource, + indication = null, + ) + .styleable(styleState, baseButtonStyle, style), + content = content, + verticalAlignment = Alignment.CenterVertically + ) +} +// [END android_compose_styles_base_button] \ No newline at end of file diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/components/Text.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/components/Text.kt new file mode 100644 index 000000000..cb0db2a2c --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/components/Text.kt @@ -0,0 +1,53 @@ +/* + * 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.compose.snippets.designsystems.styles.components + +import androidx.compose.foundation.style.ExperimentalFoundationStyleApi +import androidx.compose.foundation.style.Style +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.style.TextOverflow + +@ExperimentalFoundationStyleApi +@Composable +fun BaseText( + 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, style), + onTextLayout = onTextLayout, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + autoSize = autoSize, + ) +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3bd830def..2e81409db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -134,10 +134,10 @@ androidx-camera-viewfinder-compose = { module = "androidx.camera.viewfinder:view androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidx-cameraX" } androidx-car = { module = "androidx.car.app:app", version.ref = "androidx-car"} androidx-compose-animation-graphics = { module = "androidx.compose.animation:animation-graphics", version.ref = "compose-latest" } -androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } -androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-latest" } -androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout", version.ref = "compose-latest" } -androidx-compose-material = { module = "androidx.compose.material:material", version.ref = "compose-latest" } +androidx-compose-bom = { module = "androidx.compose:compose-bom-alpha", version.ref = "androidx-compose-bom" } +androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } +androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" } +androidx-compose-material = { module = "androidx.compose.material:material" } androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" } androidx-compose-material-ripple = { module = "androidx.compose.material:material-ripple", version.ref = "compose-latest" } androidx-compose-material3 = { module = "androidx.compose.material3:material3" } @@ -149,7 +149,7 @@ androidx-compose-material3-adaptive-navigation3 = { module = "androidx.compose.m androidx-compose-material3-windowsizeclass = { group = "androidx.compose.material3", name = "material3-window-size-class-android", version.ref = "material3-adaptive-navigation-suite" } androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" } androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" } -androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-latest" } +androidx-compose-ui = { module = "androidx.compose.ui:ui" } androidx-compose-ui-googlefonts = { module = "androidx.compose.ui:ui-text-google-fonts" } androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test", version.ref = "compose-latest" }