Skip to content

Instantly share code, notes, and snippets.

@halilozercan
Forked from MagicalMeghan/CollarCustomizationScreenGist.kt
Last active May 15, 2025 19:38
Show Gist options
  • Save halilozercan/f766d9c1fa63f39d14c5a76f09587d54 to your computer and use it in GitHub Desktop.
Save halilozercan/f766d9c1fa63f39d14c5a76f09587d54 to your computer and use it in GitHub Desktop.
Gists for "Mastering Text Input" talk for Google IO 25
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.InputTransformation
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.maxLength
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.contentType
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun TwoFAScreen() {
val otpState = rememberTextFieldState()
BasicTextField(
state = otpState,
modifier = Modifier.semantics { contentType = ContentType.SmsOtpCode },
inputTransformation = InputTransformation.maxLength(6),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
lineLimits = TextFieldLineLimits.SingleLine,
decorator = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
val otpCode = otpState.text.toString()
repeat(6) {
Digit(number = otpCode.getOrElse(it) { ' ' })
}
}
}
)
}
@Composable
fun Digit(number: Char) {
Box(
Modifier
.size(48.dp)
.padding(1.dp)
.border(1.5.dp, Color(0xFFD0BCFF), SquircleShape)
.background(Color(0xFFECD2DA), SquircleShape)
) {
Text(
text = "$number",
fontSize = 32.sp,
modifier = Modifier.align(Alignment.Center)
)
}
}
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.border
import androidx.compose.foundation.content.ReceiveContentListener
import androidx.compose.foundation.content.TransferableContent
import androidx.compose.foundation.content.consume
import androidx.compose.foundation.content.contentReceiver
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ChaScreen(
chatViewModel: ChatViewModel,
modifier: Modifier = Modifier,
) {
var background by remember { mutableStateOf(Color.Transparent) }
var borderStroke by remember { mutableStateOf(Color.Transparent) }
val receiveContentListener = remember {
object : ReceiveContentListener {
override fun onDragEnd() {
background = Color.Transparent
borderStroke = Color.Transparent
}
override fun onDragEnter() {
background = Color.Red.copy(alpha = .4f)
}
override fun onDragExit() {
background = Color.Red.copy(alpha = 0.2f)
}
override fun onDragStart() {
borderStroke = Color.Red
background = Color.Red.copy(alpha = 0.2f)
}
override fun onReceive(transferableContent: TransferableContent): TransferableContent? {
return transferableContent.consume { clipDataItem ->
if (!clipDataItem.text.isNullOrBlank()) {
chatViewModel.inputState.edit {
replace(selection.min, selection.max, clipDataItem.text?.toString() ?: "")
selection = TextRange(length)
}
}
if (clipDataItem.uri != null) {
chatViewModel.addSelectedImage(clipDataItem.uri)
}
true
}
}
}
}
Scaffold(
modifier = modifier
.border(width = 2.dp, color = borderStroke)
.drawWithContent {
drawContent()
drawRect(background)
}
.contentReceiver(receiveContentListener)
) { paddingValues ->
Column(Modifier.fillMaxSize().padding(paddingValues)) {
TextField(chatViewModel.inputState)
}
}
}
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.foundation.text.input.InputTransformation
import androidx.compose.foundation.text.input.OutputTransformation
import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.insert
import androidx.compose.foundation.text.input.maxLength
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.input.then
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.semantics.contentType
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import androidx.core.text.isDigitsOnly
@Preview
@Composable
fun CollarCustomizerGist() {
Column {
val dogNameState = rememberTextFieldState()
val phoneState = rememberTextFieldState()
BasicText(
text = "${dogNameState.text}",
autoSize = TextAutoSize.StepBased(),
style = LocalTextStyle.current.copy(
textAlign = TextAlign.Center,
fontSize = 24.sp
),
modifier = Modifier.weight(3f)
)
BasicText(
text = phoneState.outputTransformed(PhoneNumberOutputTransformation()),
autoSize = TextAutoSize.StepBased(),
style = LocalTextStyle.current.copy(
textAlign = TextAlign.Center,
fontSize = 24.sp
),
modifier = Modifier.weight(1f)
)
OutlinedTextField(
state = dogNameState,
lineLimits = TextFieldLineLimits.SingleLine,
placeholder = { Text("Pet Name") },
)
OutlinedTextField(
state = phoneState,
lineLimits = TextFieldLineLimits.SingleLine,
placeholder = { Text("Phone Number") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
modifier = Modifier
.semantics { contentType = ContentType.PhoneNumber },
inputTransformation = InputTransformation.maxLength(10).then {
if (!asCharSequence().isDigitsOnly()) {
revertAllChanges()
}
},
outputTransformation = PhoneNumberOutputTransformation()
)
}
}
class PhoneNumberOutputTransformation : OutputTransformation {
override fun TextFieldBuffer.transformOutput() {
if (length > 0) insert(0, "(")
if (length > 4) insert(4, ")")
if (length > 8) insert(8, "-")
}
}
fun TextFieldState.outputTransformed(outputTransformation: OutputTransformation): String {
var buffer: TextFieldBuffer? = null
edit { buffer = this }
with(outputTransformation) {
buffer?.transformOutput()
}
return buffer?.asCharSequence()?.toString() ?: ""
}
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextObfuscationMode.Companion.RevealLastTyped
import androidx.compose.foundation.text.input.TextObfuscationMode.Companion.Visible
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pets
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Icon
import androidx.compose.material.OutlinedSecureTextField
import androidx.compose.material.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.draw.clip
import androidx.compose.ui.semantics.contentType
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
@Preview
@Composable
fun LoginGist() {
var passwordVisible by remember { mutableStateOf(false) }
Column {
OutlinedTextField(
state = rememberTextFieldState(),
lineLimits = TextFieldLineLimits.SingleLine,
placeholder = { Text("Username") },
leadingIcon = { Icon(Icons.Filled.Pets, contentDescription = null) },
modifier = Modifier.semantics { contentType = ContentType.Username },
)
OutlinedSecureTextField(
state = rememberTextFieldState(),
placeholder = { Text("Password") },
modifier = Modifier.semantics { contentType = ContentType.Password },
trailingIcon = {
Icon(
if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff,
contentDescription = "Toggle password visibility",
modifier = Modifier.clip(CircleShape).clickable { passwordVisible = !passwordVisible }
)
},
textObfuscationMode = if (passwordVisible) Visible else RevealLastTyped
)
}
}
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.OutlinedSecureTextField
import androidx.compose.material.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.semantics.contentType
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
@Preview
@Composable
fun RegistrationGist() {
Column {
OutlinedTextField(
state = rememberTextFieldState(),
lineLimits = TextFieldLineLimits.SingleLine,
placeholder = { Text("Username") },
modifier = Modifier.semantics { contentType = ContentType.NewUsername }
)
val passwordState = rememberTextFieldState()
val isError = passwordState.text.length in 1..7
OutlinedSecureTextField(
state = passwordState,
placeholder = { Text("Password") },
isError = isError,
supportingText = {
if (isError) {
Text("Password must be at least 8 characters")
}
},
modifier = Modifier.semantics { contentType = ContentType.NewPassword }
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment