androidx.compose.foundation.text.handwriting

Extension functions summary

Modifier

Configures an element to act as a handwriting detector which detects stylus handwriting and delegates handling of the recognised text to another element.

Modifier

Configures an element to act as a stylus handwriting handler which can handle text input from a handwriting session which was triggered by stylus handwriting on a handwriting detector.

Extension functions

handwritingDetector

fun Modifier.handwritingDetector(callback: () -> Unit): Modifier

Configures an element to act as a handwriting detector which detects stylus handwriting and delegates handling of the recognised text to another element.

Stylus movement on the element will start a handwriting session, and trigger the callback. The callback implementation is expected to show and focus a text input field with a handwritingHandler modifier which can handle the recognized text from the handwriting session.

A common use case is a component which looks like a text input field but does not actually support text input itself, and clicking on this fake text input field causes a real text input field to be shown. To support handwriting initiation in this case, this modifier can be applied to the fake text input field to configure it as a detector, and a handwritingHandler modifier can be applied to the real text input field. The callback implementation is typically the same as the onClick implementation for the fake text field's clickable modifier, which shows and focuses the real text input field.

This function returns a no-op modifier on API levels below Android U (34) as stylus handwriting is not supported.

import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.handwriting.handwritingDetector
import androidx.compose.foundation.text.handwriting.handwritingHandler
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.ContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog

var openDialog by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }

Column(
    Modifier.imePadding().requiredWidth(300.dp).verticalScroll(rememberScrollState()),
    verticalArrangement = Arrangement.Center,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Text(
        "This is not an actual text field, but it is a handwriting detector so you can use " +
            "a stylus to write here."
    )
    Spacer(Modifier.size(16.dp))
    Text(
        "Fake text field",
        Modifier.fillMaxWidth()
            .handwritingDetector { openDialog = !openDialog }
            .padding(4.dp)
            .border(
                1.dp,
                MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
                RoundedCornerShape(4.dp)
            )
            .padding(16.dp),
        color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium)
    )
}

if (openDialog) {
    Dialog(onDismissRequest = { openDialog = false }) {
        Card(modifier = Modifier.width(300.dp), shape = RoundedCornerShape(16.dp)) {
            Column(
                modifier = Modifier.padding(24.dp),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text("This text field is a handwriting handler.")
                Spacer(Modifier.size(16.dp))
                val state = remember { TextFieldState() }
                BasicTextField(
                    state = state,
                    modifier =
                        Modifier.fillMaxWidth()
                            .focusRequester(focusRequester)
                            .handwritingHandler(),
                    decorator = { innerTextField ->
                        Box(
                            Modifier.padding(4.dp)
                                .border(
                                    1.dp,
                                    MaterialTheme.colors.onSurface,
                                    RoundedCornerShape(4.dp)
                                )
                                .padding(16.dp)
                        ) {
                            innerTextField()
                        }
                    }
                )
            }
        }

        val windowInfo = LocalWindowInfo.current
        LaunchedEffect(windowInfo) {
            snapshotFlow { windowInfo.isWindowFocused }
                .collect { isWindowFocused ->
                    if (isWindowFocused) {
                        focusRequester.requestFocus()
                    }
                }
        }
    }
}
Parameters
callback: () -> Unit

a callback which will be triggered when stylus handwriting is detected

fun Modifier.handwritingHandler(): Modifier

Configures an element to act as a stylus handwriting handler which can handle text input from a handwriting session which was triggered by stylus handwriting on a handwriting detector.

When this element gains focus, if there is an ongoing stylus handwriting delegation which was triggered by stylus handwriting on a handwriting detector, this element will receive text input from the handwriting session via its input connection.

A common use case is a component which looks like a text input field but does not actually support text input itself, and clicking on this fake text input field causes a real text input field to be shown. To support handwriting initiation in this case, a handwritingDetector modifier can be applied to the fake text input field to configure it as a detector, and this modifier can be applied to the real text input field. The callback implementation for the fake text field's handwritingDetector modifier is typically the same as the onClick implementation its clickable modifier, which shows and focuses the real text input field.

This function returns a no-op modifier on API levels below Android U (34) as stylus handwriting is not supported.

import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.handwriting.handwritingDetector
import androidx.compose.foundation.text.handwriting.handwritingHandler
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.ContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog

var openDialog by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }

Column(
    Modifier.imePadding().requiredWidth(300.dp).verticalScroll(rememberScrollState()),
    verticalArrangement = Arrangement.Center,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Text(
        "This is not an actual text field, but it is a handwriting detector so you can use " +
            "a stylus to write here."
    )
    Spacer(Modifier.size(16.dp))
    Text(
        "Fake text field",
        Modifier.fillMaxWidth()
            .handwritingDetector { openDialog = !openDialog }
            .padding(4.dp)
            .border(
                1.dp,
                MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
                RoundedCornerShape(4.dp)
            )
            .padding(16.dp),
        color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium)
    )
}

if (openDialog) {
    Dialog(onDismissRequest = { openDialog = false }) {
        Card(modifier = Modifier.width(300.dp), shape = RoundedCornerShape(16.dp)) {
            Column(
                modifier = Modifier.padding(24.dp),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text("This text field is a handwriting handler.")
                Spacer(Modifier.size(16.dp))
                val state = remember { TextFieldState() }
                BasicTextField(
                    state = state,
                    modifier =
                        Modifier.fillMaxWidth()
                            .focusRequester(focusRequester)
                            .handwritingHandler(),
                    decorator = { innerTextField ->
                        Box(
                            Modifier.padding(4.dp)
                                .border(
                                    1.dp,
                                    MaterialTheme.colors.onSurface,
                                    RoundedCornerShape(4.dp)
                                )
                                .padding(16.dp)
                        ) {
                            innerTextField()
                        }
                    }
                )
            }
        }

        val windowInfo = LocalWindowInfo.current
        LaunchedEffect(windowInfo) {
            snapshotFlow { windowInfo.isWindowFocused }
                .collect { isWindowFocused ->
                    if (isWindowFocused) {
                        focusRequester.requestFocus()
                    }
                }
        }
    }
}