Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support singleLine and KeyboardAction on iOS #699

Merged
merged 6 commits into from Jul 28, 2023

Conversation

paxbun
Copy link

@paxbun paxbun commented Jul 21, 2023

Proposed Changes

For iOS singleLine and KeyboardAction support

  • Added UIKitTextInputService.runImeActionRequired that checks whether a keyboard action needs to be called and calls it if needed
  • Added UIKitTextInputService.onPreviewKeyEvent for hardware keyboard event debouncing
  • Made UIKitTextInputService.skikoInput.insertText call UIKitTextInputService.runImeActionRequired when the given character is a new line character
  • Prevented UIKitTextInputService.skikoInput.insertText from adding a new line character when singleLine is true or a keyboard action needs to be called
  • Passed UIKitTextInputService::onPreviewKeyEvent to ComposeLayer.setContent from ComposeWindow
  • Added return key press event debouncing logic when calling an IME action

On some Android devices, the behavior of text fields is different when with hardware keyboards and with software keyboards. As you can see in the video below, on Android, the behavior of ImeAction.Search with singleLine = false is different, where the Search action is called with software keyboards while not called with hardware keyboards. Also, on some virtual Android devices (for me, it was Resizable (Experimental) API 33), when singleLine is true and the current IME action is ImeAction.Default, the Done action is called with software keyboards while not called with hardware keyboards.

The current behavior of text fields in this PR is as follows. When an IME action is called, the new line character is always prevented from being added.

Actions singleLine = true singleLine = false
ImeAction.Default Calls the Done action. Does nothing
ImeAction.None Prevents a new line character from being added. Does nothing
ImeAction.Search Calls the Search action. Calls the Search action only when the software keyboard is used, does nothing otherwise
Others Calls the corresponding action regardless of the value of singleLine.

For iOS hardware keyboard bug fix

  • Made UIKitTextInputService.onPreviewKeyEvent consume KEY_ENTER and KEY_BACKSPACE down events to prevent double new line characters or double character removing when using hardware keyboards

Testing

Test: Tested using the Android & iOS template with the following configuration:

  • (Ground Truth) Galaxy Tab S8 with Samsung Keyboard
  • (Ground Truth) Galaxy Tab S8 with a hardware keyboard (via Bluetooth)
  • iPad Pro, 11-inch (3rd generation) (iOS 16.5.1) with the system keyboard
  • iPad Pro, 11-inch (3rd generation) (iOS 16.5.1) with Magic Keyboard (via Universal Control, but Bluetooth works the same)

App.kt is modified as the code below.

Issues Fixed

Fixes: JetBrains/compose-multiplatform#2794

Test Code

private val imeActions = arrayOf(
    ImeAction.Default,
    ImeAction.Done,
    ImeAction.Go,
    ImeAction.Next,
    ImeAction.None,
    ImeAction.Previous,
    ImeAction.Search,
    ImeAction.Send,
)

@Composable
fun App() {
    MaterialTheme {
        var currentAction by remember { mutableStateOf(ImeAction.Default) }
        val (singleLine, setSingleLine) = remember { mutableStateOf(false) }
        val (value, setValue) = remember { mutableStateOf("") }
        val keyboardActionsCalled = remember { mutableStateListOf<String>() }
        LaunchedEffect(keyboardActionsCalled.toTypedArray()) {
            delay(0.5.seconds)
            if (keyboardActionsCalled.isNotEmpty()) {
                keyboardActionsCalled.removeLast()
            }
        }
        Column {
            Row(
                modifier = Modifier.horizontalScroll(rememberScrollState()),
                horizontalArrangement = Arrangement.spacedBy(20.dp),
            ) {
                imeActions.forEach { action ->
                    Row {
                        RadioButton(
                            selected = currentAction == action,
                            onClick = { currentAction = action },
                        )
                        Text(action.toString())
                    }
                }
            }
            Row {
                Checkbox(singleLine, setSingleLine)
                Text("Single Line")
            }
            keyboardActionsCalled.forEach {
                Text("$it is called")
            }
            Row {
                TextField(
                    value = value,
                    onValueChange = setValue,
                    keyboardOptions = KeyboardOptions(
                        imeAction = currentAction
                    ),
                    keyboardActions = KeyboardActions(
                        onDone = { keyboardActionsCalled += "Done" },
                        onGo = { keyboardActionsCalled += "Go" },
                        onNext = { keyboardActionsCalled += "Next" },
                        onPrevious = { keyboardActionsCalled += "Previous" },
                        onSearch = { keyboardActionsCalled += "Search" },
                        onSend = { keyboardActionsCalled += "Send" },
                    ),
                    singleLine = singleLine,
                )
                Button({ setValue("") }) {
                    Text("Clear")
                }
            }
        }
    }
}

Test Results (iOS singleLine and KeyboardAction support)

Galaxy.Tab.S8.Software.mp4
Galaxy.Tab.S8.Hardware.mp4
iPad.Pro.Software.mov
iPad.Pro.Hardware.MP4

Test Results (iOS hardware keyboard bug fix)

Tested with hello <return> <return> world <backspace> <backspace>.

Before the fix:

iPad.Pro.Before.MP4

After the fix:

iPad.Pro.After.MP4

- Added UIKitTextInputService.runImeActionIfRequired that calls the
  current keyboard action if needed
- Added UIKitTextInputService.onKeyEvent that calls a keyboard
  action when required
- Prevented UIKitTextInputService.skikoInput from adding a new line
  character when singleLine = true or a keyboard action needs to be
  called
- Passed UIKitTextInputService::onKeyEvent to ComposeLayer.setContent
  from ComposeWindow
@paxbun paxbun reopened this Jul 22, 2023
@dima-avdeev-jb
Copy link

Hello! Big thanks, it looks very usefull

But, first can you please sign the Google Contributor’s License Agreement at https://cla.developers.google.com/.
This is needed since we synchronise most of the code with Google’s AOSP repository. Signing this agreement allows us to synchronise code from your Pull Requests as well.

As a referece, you can look at screenshots in this PR: #521

@dima-avdeev-jb
Copy link

And can you please provide a screenshot of the signed agreement?

@paxbun
Copy link
Author

paxbun commented Jul 27, 2023

Here it is (I re-signed it, just in case):
image

@dima-avdeev-jb
Copy link

Thanks for a screenshot!
I will check this PR

@dima-avdeev-jb
Copy link

You got 1 approve! But please wait for my approve too.

@devjorgecastro
Copy link

Here it is (I re-signed it, just in case): image

Excuse my ignorance, but what does the image mean?

@paxbun
Copy link
Author

paxbun commented Aug 12, 2023

@devjorgecastro That image shows that I'm registered as a contributor to Google's open-source projects. Contributions here will eventually be merged into the original Google repository, so you have to sign in at https://cla.developers.google.com/ if you want to make a PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants