diff --git a/compose/snippets/build.gradle.kts b/compose/snippets/build.gradle.kts index e21218d6c..c2a4f6eaf 100644 --- a/compose/snippets/build.gradle.kts +++ b/compose/snippets/build.gradle.kts @@ -31,7 +31,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..e0560e5a5 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/DosDonts.kt @@ -0,0 +1,108 @@ +/* + * 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.styleable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + + +// [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] 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..a8a63e331 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/Examples.kt @@ -0,0 +1,311 @@ +/* + * 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.LinearEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +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.WindowInsets +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding +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.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.Shader +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.graphics.TransformOrigin +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] + +// [START android_compose_styles_gradient_glow_button] +@Preview +@Composable +fun GradientGlowButtonExample() { + val infiniteTransition = rememberInfiniteTransition(label = "glowing_button_85_animation") + val animatedProgress by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 20000, easing = LinearEasing), + ), label = "progress" + ) + + val gradientColors = listOf( + Color(0xffff0000), Color(0xffff7300), Color(0xfffffb00), Color(0xff48ff00), + Color(0xff00ffd5), Color(0xff002bff), Color(0xff7a00ff), Color(0xffff00c8), + Color(0xffff0000) + ) + + val glowingBrush = remember(animatedProgress) { + object : ShaderBrush() { + override fun createShader(size: Size): Shader { + val width = size.width * 4 + val brushSize = width * animatedProgress + return LinearGradientShader( + colors = gradientColors, + from = Offset(brushSize, 0f), + to = Offset(brushSize + width, 0f), + tileMode = TileMode.Repeated + ) + } + } + } + + + Box( + modifier = Modifier + .padding(32.dp), + contentAlignment = Alignment.Center + ) { + BaseButton( + onClick = { }, + style = Style { + dropShadow( + Shadow( + brush = glowingBrush, + radius = 5.dp + ) + ) + transformOrigin(TransformOrigin.Center) + pressed { + animate { + dropShadow( + Shadow( + brush = glowingBrush, + radius = 10.dp + ) + ) + scale(0.95f) + } + + } + size(width = 200.dp, height = 50.dp) + background(Color(0xFF111111)) + shape(RoundedCornerShape(10.dp)) + contentColor(Color.White) + contentPadding(vertical = (0.6f * 14).dp, horizontal = (2f * 14).dp) + border(width = 0.dp, color = Color.Transparent) + } + ) { + BaseText(text = "Button 85") + } + } +} +// [END android_compose_styles_gradient_glow_button] 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..6b10538bf --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/StateAnimations.kt @@ -0,0 +1,259 @@ +/* + * 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) + /* There is currently a known issue where combining a shape with gradient backgrounds doesn't render properly. + b/482308908 + 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/components/Button.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/components/Button.kt new file mode 100644 index 000000000..fb5b37ba8 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/styles/components/Button.kt @@ -0,0 +1,76 @@ +/* + * 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.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? = null, + content: @Composable RowScope.() -> Unit +) { + val effectiveInteractionSource = interactionSource ?: remember { + MutableInteractionSource() + } + val styleState = remember(effectiveInteractionSource) { + MutableStyleState(effectiveInteractionSource) + } + styleState.isEnabled = enabled + Row( + modifier = modifier + .semantics(properties = { + role = Role.Button + }) + .clickable( + enabled = enabled, + onClick = onClick, + interactionSource = effectiveInteractionSource, + 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 a44682a23..daa44f302 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ androidGradlePlugin = "9.0.1" androidx-activity-compose = "1.12.4" androidx-appcompat = "1.7.0" androidx-appfunctions = "1.0.0-alpha07" -androidx-compose-bom = "2026.02.01" +androidx-compose-bom = "2026.03.00" androidx-compose-ui-test = "1.7.0-alpha08" androidx-compose-ui-test-junit4-accessibility = "1.11.0-alpha06" androidx-constraintlayout = "2.2.1" @@ -120,10 +120,10 @@ androidx-appfunctions = { module = "androidx.appfunctions:appfunctions", version androidx-appfunctions-compiler = { module = "androidx.appfunctions:appfunctions-compiler", version.ref = "androidx-appfunctions" } androidx-appfunctions-service = { module = "androidx.appfunctions:appfunctions-service", version.ref = "androidx-appfunctions" } 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" } @@ -133,7 +133,7 @@ androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.ma androidx-compose-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", 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" }