diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/TextSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/TextSnippets.kt index 5973733a5..b39fb0106 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/text/TextSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/TextSnippets.kt @@ -35,8 +35,10 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicSecureTextField import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextObfuscationMode +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons @@ -328,6 +330,27 @@ private object TextBrushSnippet2 { } } +private object TextBrushSnippet2StateBased { + @Composable + fun TextStyledBrushSnippet() { + val rainbowColors: List = listOf() + // [START android_compose_text_textfield_state_brush] + var text by remember { mutableStateOf("") } + val brush = remember { + Brush.linearGradient( + colors = rainbowColors + ) + } + + TextField( + state = rememberTextFieldState(), + placeholder = { Text("username") }, + textStyle = TextStyle(brush = brush) + ) + // [END android_compose_text_textfield_state_brush] + } +} + private object TextBrushSnippet3 { @Composable fun TextStyledBrushSnippet() { @@ -449,6 +472,19 @@ private object TextTextFieldSnippet { // [END android_compose_text_textfield_filled] } +private object TextTextFieldStateSnippet { + // [START android_compose_text_textfield_state_filled] + @Composable + fun SimpleFilledTextFieldSample() { + + TextField( + state = TextFieldState(), + label = { Text("Label") } + ) + } + // [END android_compose_text_textfield_state_filled] +} + private object TextOutlinedTextFieldSnippet { // [START android_compose_text_textfield_outlined] @Composable @@ -464,6 +500,18 @@ private object TextOutlinedTextFieldSnippet { // [END android_compose_text_textfield_outlined] } +private object TextOutlinedTextFieldStateSnippet { + // [START android_compose_text_textfield_state_outlined] + @Composable + fun SimpleOutlinedTextFieldSample() { + OutlinedTextField( + state = rememberTextFieldState(), + label = { Text("Label") } + ) + } + // [END android_compose_text_textfield_state_outlined] +} + private object TextStylingTextFieldSnippet { // [START android_compose_text_textfield_styled] @Composable @@ -482,6 +530,23 @@ private object TextStylingTextFieldSnippet { // [END android_compose_text_textfield_styled] } +private object TextStylingTextFieldStateSnippet { + // [START android_compose_text_textfield_state_styled] + @Composable + fun StyledTextField() { + var value by remember { mutableStateOf("Hello\nWorld\nInvisible") } + + TextField( + state = rememberTextFieldState(), + label = { Text("Enter text") }, + lineLimits = TextFieldLineLimits.MultiLine(2), + textStyle = TextStyle(color = Color.Blue, fontWeight = FontWeight.Bold), + modifier = Modifier.padding(20.dp) + ) + } + // [END android_compose_text_textfield_state_styled] +} + private object TextFormattingTextFieldSnippet { // [START android_compose_text_textfield_visualtransformation] @Composable @@ -499,6 +564,25 @@ private object TextFormattingTextFieldSnippet { // [END android_compose_text_textfield_visualtransformation] } +private object TextFormattingTextFieldStateSnippet { + // [START android_compose_text_textfield_visualtransformation] + + // TODO: update use a different transform + @Composable + fun PasswordTextField() { + var password by rememberSaveable { mutableStateOf("") } + + TextField( + value = password, + onValueChange = { password = it }, + label = { Text("Enter password") }, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + ) + } + // [END android_compose_text_textfield_visualtransformation] +} + private object TextCleanInputSnippet { // [START android_compose_text_textfield_clean_input] @Composable @@ -514,6 +598,22 @@ private object TextCleanInputSnippet { // [END android_compose_text_textfield_clean_input] } +private object TextStateCleanInputSnippet { + // [START android_compose_text_textfield_clean_input] + @Composable + fun NoLeadingZeroes() { + // TODO: update this - use inputTransformation instead? + var input by rememberSaveable { mutableStateOf("") } + TextField( + value = input, + onValueChange = { newText -> + input = newText.trimStart { it == '0' } + } + ) + } + // [END android_compose_text_textfield_clean_input] +} + /** Effective State management **/ private object TextEffectiveStateManagement1 { @@ -560,6 +660,39 @@ private object TextEffectiveStateManagement2 { // [END android_compose_text_state_management] } +private object TextStateEffectiveStateManagement2 { + // TODO: update + class UserRepository + + private val viewModel = SignUpViewModel(UserRepository()) + + // [START android_compose_text_state_management] + // SignUpViewModel.kt + + class SignUpViewModel(private val userRepository: UserRepository) : ViewModel() { + + var username by mutableStateOf("") + private set + + fun updateUsername(input: String) { + username = input + } + } + + // SignUpScreen.kt + + @Composable + fun SignUpScreen(/*...*/) { + + OutlinedTextField( + value = viewModel.username, + onValueChange = { username -> viewModel.updateUsername(username) } + /*...*/ + ) + } + // [END android_compose_text_state_management] +} + // [START android_compose_text_link_1] @Composable fun AnnotatedStringWithLinkSample() { @@ -816,6 +949,30 @@ fun PhoneNumber() { } // [END android_compose_text_auto_format_phone_number_textfieldconfig] +// [START android_compose_text_auto_format_phone_number_textfieldconfig] +@Composable +fun PhoneNumberStateBased() { + // TODO: update to use output transformation + var phoneNumber by rememberSaveable { mutableStateOf("") } + val numericRegex = Regex("[^0-9]") + TextField( + value = phoneNumber, + onValueChange = { + // Remove non-numeric characters. + val stripped = numericRegex.replace(it, "") + phoneNumber = if (stripped.length >= 10) { + stripped.substring(0..9) + } else { + stripped + } + }, + label = { Text("Enter Phone Number") }, + visualTransformation = NanpVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) + ) +} +// [END android_compose_text_auto_format_phone_number_textfieldconfig] + // [START android_compose_text_auto_format_phone_number_transformtext] class NanpVisualTransformation : VisualTransformation { @@ -909,6 +1066,52 @@ fun PasswordTextField() { } // [END android_compose_text_showhidepassword] +// [START android_compose_text_showhidepassword] +@Composable +fun SecureTextField() { + // TODO: update to SecureTextField + val state = remember { TextFieldState() } + var showPassword by remember { mutableStateOf(false) } + BasicSecureTextField( + state = state, + textObfuscationMode = + if (showPassword) { + TextObfuscationMode.Visible + } else { + TextObfuscationMode.RevealLastTyped + }, + modifier = Modifier + .fillMaxWidth() + .padding(6.dp) + .border(1.dp, Color.LightGray, RoundedCornerShape(6.dp)) + .padding(6.dp), + decorator = { innerTextField -> + Box(modifier = Modifier.fillMaxWidth()) { + Box( + modifier = Modifier + .align(Alignment.CenterStart) + .padding(start = 16.dp, end = 48.dp) + ) { + innerTextField() + } + Icon( + if (showPassword) { + Icons.Filled.Visibility + } else { + Icons.Filled.VisibilityOff + }, + contentDescription = "Toggle password visibility", + modifier = Modifier + .align(Alignment.CenterEnd) + .requiredSize(48.dp).padding(16.dp) + .clickable { showPassword = !showPassword } + ) + } + } + ) +} +// [END android_compose_text_showhidepassword] + // [START android_compose_text_auto_format_phone_number_validatetext] class EmailViewModel : ViewModel() { var email by mutableStateOf("") @@ -954,10 +1157,64 @@ fun ValidatingInputTextField( @Composable fun ValidateInput() { val emailViewModel: EmailViewModel = viewModel() - ValidatingInputTextField( + ValidatingInputTextField1( email = emailViewModel.email, updateState = { input -> emailViewModel.updateEmail(input) }, validatorHasErrors = emailViewModel.emailHasErrors ) } // [END android_compose_text_auto_format_phone_number_validatetext] + +// [START android_compose_text_state_auto_format_phone_number_validatetext] +class EmailViewModel1 : ViewModel() { + var email by mutableStateOf("") + private set + + val emailHasErrors by derivedStateOf { + if (email.isNotEmpty()) { + // Email is considered erroneous until it completely matches EMAIL_ADDRESS. + !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() + } else { + false + } + } + + fun updateEmail(input: String) { + email = input + } +} + +@Composable +fun ValidatingInputTextField1( + email: String, + updateState: (String) -> Unit, + validatorHasErrors: Boolean +) { + // TODO: update + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + value = email, + onValueChange = updateState, + label = { Text("Email") }, + isError = validatorHasErrors, + supportingText = { + if (validatorHasErrors) { + Text("Incorrect email format.") + } + } + ) +} + +@Preview +@Composable +fun ValidateInput1() { + val emailViewModel: EmailViewModel = viewModel() + ValidatingInputTextField( + email = emailViewModel.email, + updateState = { input -> emailViewModel.updateEmail(input) }, + validatorHasErrors = emailViewModel.emailHasErrors + ) +} +// [END android_compose_text_state_auto_format_phone_number_validatetext] diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7edd2bbdc..6fbc1a329 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] accompanist = "0.36.0" androidGradlePlugin = "8.8.1" -androidx-activity-compose = "1.10.0" +androidx-activity-compose = "1.10.1" androidx-appcompat = "1.7.0" -androidx-compose-bom = "2025.02.00" +androidx-compose-bom = "2025.03.01" androidx-compose-ui-test = "1.7.0-alpha08" androidx-constraintlayout = "2.2.0" androidx-constraintlayout-compose = "1.1.0" @@ -40,7 +40,8 @@ kotlin = "2.1.10" kotlinxSerializationJson = "1.8.0" ksp = "2.1.10-1.0.30" maps-compose = "6.4.4" -material = "1.13.0-alpha10" +material = "1.13.0-alpha12" +material3-alpha = "1.4.0-alpha11" material3-adaptive = "1.1.0" material3-adaptive-navigation-suite = "1.3.1" media3 = "1.5.1" @@ -73,7 +74,7 @@ androidx-compose-foundation-layout = { module = "androidx.compose.foundation:fou androidx-compose-material = { module = "androidx.compose.material:material", version.ref = "compose-latest" } 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" } +androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3-alpha" } androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3-adaptive" } androidx-compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3-adaptive" } androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3-adaptive" }