ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

Command Your User Inputs with Jetpack Compose— Text Field Features Hidden in Plain Sight

Nirbhay Pherwani
ProAndroidDev
Published in
12 min readJul 21, 2024

This image was created with the assistance of DALL·E 3

Introduction

1) The Basics

Basic Text Field Example
@Composable
fun BasicTextFieldExample() {
var text by remember { mutableStateOf("Pre Filled Text") }
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Placeholder") }
}

Explanation

2) Gradient Text Field

Gradient Text Field and Gradient Cursor
@Composable
fun GradientTextField() {
var text by remember { mutableStateOf("") }
BasicTextField(
value = text,
onValueChange = { text = it },
textStyle = TextStyle(
brush = Brush.linearGradient(
colors = listOf(Color.Red, Color.Blue, Color.Green, Color.Magenta)
),
fontSize = 32.sp
),
cursorBrush = Brush.verticalGradient(
colors = listOf(Color.Blue, Color.Cyan, Color.Red, Color.Magenta)
),
)
}

Explanation

3) Decoration Box

Decoration Box in Action
@Composable
fun DecoratedTextField() {
var text by remember { mutableStateOf("") }

BasicTextField(
value = text,
onValueChange = { text = it },
decorationBox = { innerTextField ->
Row(
Modifier
.padding(horizontal = 16.dp, vertical = 50.dp)
.border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.Email, contentDescription = "Email")
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier = Modifier.weight(1f)
) {
if (text.isEmpty()) {
Text(
text = "Enter email",
style = TextStyle(color = Color.Gray)
)
}
innerTextField()
}
if (text.isNotEmpty()) {
IconButton(onClick = { text = "" }) {
Icon(Icons.Default.Clear, contentDescription = "Clear text")
}
}
}
},
textStyle = TextStyle(
color = Color.Black,
fontSize = 16.sp
)
)
}

Explanation

4) Let’s Go Funky

Funky Text
@Composable
fun FunkyExample() {
var text by remember { mutableStateOf("") }

BasicTextField(
modifier = Modifier.padding(vertical = 50.dp),
onValueChange = { text = it },
value = text,
textStyle = TextStyle(
fontSize = 24.sp,
baselineShift = BaselineShift.Superscript,
background = Color.Yellow,
textDecoration = TextDecoration.Underline,
lineHeight = 32.sp,
textGeometricTransform = TextGeometricTransform(
scaleX = 3f,
skewX = 0.5f
),
drawStyle = Stroke(
width = 10f,
),
hyphens = Hyphens.Auto,
lineBreak = LineBreak.Paragraph,
textMotion = TextMotion.Animated
)
)
}

Quick Explanation

5) Masked Text Field for Credit Card Input

@Composable
fun CreditCardTextField() {
var text by remember { mutableStateOf("") }
val visualTransformation = CreditCardVisualTransformation()

Column(modifier = Modifier.padding(16.dp)) {
BasicTextField(
value = text,
onValueChange = { text = it.filter { it.isDigit() } },
visualTransformation = visualTransformation,
textStyle = TextStyle(color = Color.Black, fontSize = 18.sp),
modifier = Modifier
.fillMaxWidth()
.border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
.padding(16.dp)
)

Spacer(modifier = Modifier.height(8.dp))

Text(text = "Enter your credit card number", style = TextStyle(fontSize = 16.sp))
}
}

// Sample transformation for example purposes only
class CreditCardVisualTransformation : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val trimmed = if (text.text.length >= 16) text.text.substring(0..15) else text.text
val out = StringBuilder()

for (i in trimmed.indices) {
out.append(trimmed[i])
if (i % 4 == 3 && i != 15) out.append(" ")
}

val creditCardOffsetTranslator = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
if (offset <= 3) return offset
if (offset <= 7) return offset + 1
if (offset <= 11) return offset + 2
if (offset <= 16) return offset + 3
return 19
}

override fun transformedToOriginal(offset: Int): Int {
if (offset <= 4) return offset
if (offset <= 9) return offset - 1
if (offset <= 14) return offset - 2
if (offset <= 19) return offset - 3
return 16
}
}

return TransformedText(AnnotatedString(out.toString()), creditCardOffsetTranslator)
}
}

Explanation

Where else can it be applied?

6) Handling User Interactions

Logs from using Interactive Text Field
@Composable
fun InteractiveTextField() {
var text by remember { mutableStateOf("") }
val interactionSource = remember { MutableInteractionSource() }
val focusRequester = remember { FocusRequester() }

LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> println("Testing TextField Pressed")
is PressInteraction.Release -> println("Testing TextField Released")
is FocusInteraction.Focus -> println("Testing TextField Focused")
is FocusInteraction.Unfocus -> println("Testing TextField Unfocused")
}
}
}

Column(modifier = Modifier.padding(16.dp)) {
BasicTextField(
value = text,
onValueChange = { text = it },
interactionSource = interactionSource,
modifier = Modifier
.fillMaxWidth()
.border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
.padding(16.dp)
.focusRequester(focusRequester)
)

Spacer(modifier = Modifier.height(8.dp))

Button(onClick = { focusRequester.requestFocus() }) {
Text(text = "Focus TextField")
}
}
}

Explanation

Use Cases

7) Real Time User Tagging

Real Time User Tagging
@Composable
fun RealTimeUserTaggingTextField() {
var text by remember { mutableStateOf("") }
val context = LocalContext.current

val annotatedText = buildAnnotatedString {
val regex = Regex("@[\\w]+")
var lastIndex = 0
regex.findAll(text).forEach { result ->
append(text.substring(lastIndex, result.range.first))
pushStringAnnotation(tag = "USER_TAG", annotation = result.value)
withStyle(style = SpanStyle(color = Color.Blue, textDecoration = TextDecoration.Underline)) {
append(result.value)
}
pop()
lastIndex = result.range.last + 1
}
append(text.substring(lastIndex))
}

val focusRequester = remember { FocusRequester() }

Column (modifier = Modifier.padding(horizontal = 16.dp)) {
Spacer(modifier = Modifier.height(300.dp))

BasicTextField(
value = text,
onValueChange = { text = it },
textStyle = TextStyle(color = Color.Black, fontSize = 18.sp),
modifier = Modifier
.fillMaxWidth()
.clickable {
focusRequester.requestFocus()
}
.focusRequester(focusRequester)
.border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
.padding(8.dp),
decorationBox = { innerTextField ->
Box {
ClickableText(
text = annotatedText,
onClick = { offset ->
focusRequester.requestFocus()
annotatedText.getStringAnnotations(tag = "USER_TAG", start = offset, end = offset).firstOrNull()?.let {
val username = it.item
Toast.makeText(context, "User $username clicked", Toast.LENGTH_SHORT).show()
}
},
style = TextStyle(color = Color.Black, fontSize = 18.sp)
)
innerTextField()
}
}
)

Spacer(modifier = Modifier.height(8.dp))

Text(text = "Mention users by typing @username. Clicking on the @username shows a toast.", style = TextStyle(fontSize = 16.sp))
}
}

Explanation

Other Use Cases

8) Keyboard Actions

Keyboard Actions
@Composable
fun KeyboardActionsTextField() {
var text by remember { mutableStateOf("Lorem Ipsum Lorem Ipsum") }
val context = LocalContext.current

Column {
Spacer(modifier = Modifier.height(300.dp))

BasicTextField(
value = text,
onValueChange = { text = it },
textStyle = TextStyle(color = Color.Black, fontSize = 18.sp),
modifier = Modifier
.fillMaxWidth()
.border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
.padding(8.dp),
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Send
),
keyboardActions = KeyboardActions(
onDone = {
Toast.makeText(context, "Done action pressed: $text", Toast.LENGTH_SHORT).show()
},
onSearch = {
Toast.makeText(context, "Search action pressed: $text", Toast.LENGTH_SHORT).show()
},
onGo = {
Toast.makeText(context, "Go action pressed: $text", Toast.LENGTH_SHORT).show()
},
onSend = {
Toast.makeText(context, "Send action pressed: $text", Toast.LENGTH_SHORT).show()
}
)
)
}
}

Explanation

Use Cases

9) Providing Haptic Feedback

Haptic Feedback (Vibrations)
@Composable
fun AccessibleForm() {
var email by remember { mutableStateOf("") }
var submissionStatus by remember { mutableStateOf("") }
var charVibration by remember { mutableStateOf("") }
val context = LocalContext.current
val vibrator = ContextCompat.getSystemService(context, Vibrator::class.java)

val brailleMap = mapOf(
'a' to longArrayOf(0, 50), // Example Braille pattern for 'a'
'b' to longArrayOf(0, 50, 100, 50),
'c' to longArrayOf(0, 100),
'.' to longArrayOf(0, 100, 100, 100),
'@' to longArrayOf(0, 200),
'o' to longArrayOf(0, 200, 200, 200),
'm' to longArrayOf(0, 200, 200, 200, 200, 200),
// Add mappings for other characters
)

val vibrate = { pattern: LongArray ->
if (vibrator?.hasVibrator() == true) {
vibrator.vibrate(VibrationEffect.createWaveform(pattern, -1))
}
}

val validateEmail = { input: String ->
when {
input.isEmpty() -> {
vibrate(longArrayOf(0, 100, 100, 100)) // Warning vibration
"Email cannot be empty"
}
!android.util.Patterns.EMAIL_ADDRESS.matcher(input).matches() -> {
vibrate(longArrayOf(0, 100, 100, 100, 100, 100, 100, 100)) // Error vibration
"Invalid email address"
}
else -> {
vibrate(longArrayOf(0, 50)) // Success vibration
null
}
}
}

Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 100.dp)) {
Text("Login Form", style = TextStyle(fontSize = 24.sp, color = Color.Black))

Spacer(modifier = Modifier.height(16.dp))

BasicTextField(
value = email,
onValueChange = { newText ->
email = newText
newText.lastOrNull()?.let { char ->
brailleMap[char]?.let { pattern ->
charVibration = "Vibrating for $char${pattern.asList()}"
vibrate(pattern)
}
}
},
textStyle = TextStyle(color = Color.Black, fontSize = 18.sp),
modifier = Modifier
.fillMaxWidth()
.border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
.padding(8.dp),
decorationBox = { innerTextField ->
Box(
modifier = Modifier.padding(8.dp)
) {
if (email.isEmpty()) {
Text("Enter your email", style = TextStyle(color = Color.Gray, fontSize = 18.sp))
}
innerTextField()
}
}
)

Spacer(modifier = Modifier.height(8.dp))
if(charVibration.isNotEmpty()) {
Text(charVibration, style = TextStyle(fontSize = 16.sp, color = Color.DarkGray))
}


Spacer(modifier = Modifier.height(16.dp))

Button(
onClick = {
val emailError = validateEmail(email)

submissionStatus = if (emailError == null) {
"Submission successful"
} else {
"Submission failed: $emailError"
}

if (emailError == null) {
vibrate(longArrayOf(0, 50, 50, 50, 50, 50, 50, 50)) // Success vibration
}
},
modifier = Modifier.fillMaxWidth()
) {
Text("Submit", style = TextStyle(fontSize = 18.sp, color = Color.White))
}

Spacer(modifier = Modifier.height(16.dp))

if(submissionStatus.isNotEmpty()) {
val textColor = if (submissionStatus.contains("failed")) Color.Red else Color.Green
Text("Submission status ➡ $submissionStatus", style = TextStyle(fontSize = 16.sp, color = textColor))
}
}
}

Explanation

Other Use Cases

10) Supporting Rich Media Content

Supporting Rich Media Content
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SupportRichContent() {
var images by remember { mutableStateOf<List<Uri>>(emptyList()) }
val state = rememberTextFieldState("")
val scrollState = rememberScrollState()
val coroutineScope = rememberCoroutineScope()

Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Spacer(Modifier.height(125.dp))
Row(
modifier = Modifier
.padding(bottom = 8.dp)
.fillMaxWidth()
.horizontalScroll(scrollState),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
images.forEach { uri ->
AsyncImage(
model = uri,
contentDescription = null,
modifier = Modifier
.size(100.dp)
.clip(RoundedCornerShape(8.dp))
.border(1.dp, Color.Gray, RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
)
}
}
BasicTextField(
state = state,
modifier = Modifier
.fillMaxWidth()
.background(Color.LightGray, RoundedCornerShape(8.dp))
.padding(16.dp)
.contentReceiver(
receiveContentListener = object : ReceiveContentListener {
override fun onReceive(
transferableContent: TransferableContent
)
: TransferableContent? {

if (!transferableContent.hasMediaType(MediaType.Image)) {
return transferableContent
}

return transferableContent.consume { item ->
images += item.uri
coroutineScope.launch {
scrollState.animateScrollTo(scrollState.maxValue)
}
true
}
}
}
),
textStyle = TextStyle(color = Color.Black, fontSize = 18.sp),
)
}
}

Explaination

Conclusion

Changing the Minutest Detail

Delivering Visual and Functional Excellence

Ensuring Accessibility and Usability

Closing Remarks

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Responses (2)

Write a response