From 3add10d23104af02e7c90fc7153f04bfb09cf4f1 Mon Sep 17 00:00:00 2001 From: Chiko Shimizu Date: Fri, 4 Oct 2024 14:56:28 +0900 Subject: [PATCH 1/3] Add a sample code for Keyboard Shortcuts Helper --- .../keyboardinput/KeyboardShortcutsHelper.kt | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt new file mode 100644 index 000000000..ccf5d33d7 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.touchinput.keyboardinput + +import android.app.Activity +import android.os.Build +import android.os.Bundle +import android.view.KeyEvent +import android.view.KeyboardShortcutGroup +import android.view.KeyboardShortcutInfo +import android.view.Menu +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.annotation.RequiresApi +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.ui.platform.LocalContext + +class MainActivity : ComponentActivity() { + // Activity codes such as overridden onStart method. + + @RequiresApi(Build.VERSION_CODES.N) + // [START android_compose_keyboard_shortcuts_helper] + override fun onProvideKeyboardShortcuts( + data: MutableList?, + menu: Menu?, + deviceId: Int + ) { + val shortcutGroup = + KeyboardShortcutGroup( + "Cursor movement", + listOf( + KeyboardShortcutInfo("Up", KeyEvent.KEYCODE_P, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Down", KeyEvent.KEYCODE_N, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Forward", KeyEvent.KEYCODE_F, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Backward", KeyEvent.KEYCODE_B, KeyEvent.META_CTRL_ON), + ) + ) + data?.add(shortcutGroup) + } + // [END android_compose_keyboard_shortcuts_helper] +} + +class AnotherActivity : ComponentActivity() { + + @RequiresApi(Build.VERSION_CODES.N) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + MaterialTheme { + // [START android_compose_keyboard_shortcuts_helper_request] + val activity = LocalContext.current as Activity + + Button(onClick = { activity.requestShowKeyboardShortcuts() }) { + Text(text = "Show keyboard shortcuts") + } + // [END android_compose_keyboard_shortcuts_helper_request] + } + } + } + + @RequiresApi(Build.VERSION_CODES.N) + // [START android_compose_keyboard_shortcuts_helper_with_groups] + override fun onProvideKeyboardShortcuts( + data: MutableList?, + menu: Menu?, + deviceId: Int + ) { + val cursorMovement = KeyboardShortcutGroup( + "Cursor movement", + listOf( + KeyboardShortcutInfo("Up", KeyEvent.KEYCODE_P, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Down", KeyEvent.KEYCODE_N, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Forward", KeyEvent.KEYCODE_F, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Backward", KeyEvent.KEYCODE_B, KeyEvent.META_CTRL_ON), + ) + ) + + val messageEdit = KeyboardShortcutGroup( + "Message editing", + listOf( + KeyboardShortcutInfo("Select All", KeyEvent.KEYCODE_A, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Send a message", KeyEvent.KEYCODE_ENTER, KeyEvent.META_SHIFT_ON) + ) + ) + + data?.add(cursorMovement) + data?.add(messageEdit) + } + // [END android_compose_keyboard_shortcuts_helper_with_groups] +} From 5283f3e6a93d10ec7b4225ad12737a7c5e93e883 Mon Sep 17 00:00:00 2001 From: Chiko Shimizu Date: Fri, 4 Oct 2024 17:40:20 +0900 Subject: [PATCH 2/3] Add a sample code for keyboard actions --- .../touchinput/keyboardinput/commands.kt | 337 ++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/commands.kt diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/commands.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/commands.kt new file mode 100644 index 000000000..d0c08c544 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/commands.kt @@ -0,0 +1,337 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.touchinput.keyboardinput + +import android.content.Context +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.onFocusEvent +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.isAltPressed +import androidx.compose.ui.input.key.isCtrlPressed +import androidx.compose.ui.input.key.isMetaPressed +import androidx.compose.ui.input.key.isShiftPressed +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp + +@Suppress("unused") +@Composable +fun CommandsScreen() { + val context = LocalContext.current + var playerState by rememberSaveable { mutableStateOf(false) } + + val doSomething = { + showToast(context, "Doing something") + } + + val doAnotherThing = { + showToast(context, "Doing another thing") + } + + val togglePlayPause = { + playerState = !playerState + val message = if (playerState) { + "Playing" + } else { + "Paused" + } + showToast(context, message) + } + + val actionC = { + showToast(context, "Action C") + } + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(8.dp) + ) { + KeyEvents(doSomething) + ModifierKeys(doSomething) + SpacebarAndEnterKeyTriggersClickEvents(togglePlayPause) + UnconsumedKeyEvents(doSomething, doAnotherThing, actionC) + PreviewKeyEvents() + InterceptKeyEvents( + doSomething, + { keyEvent -> + showToast(context, "onPreviewKeyEvent: ${keyEvent.key.keyCode}") + }, + { keyEvent -> + showToast(context, "onKeyEvent: ${keyEvent.key.keyCode}") + } + ) + } +} + +fun showToast(context: Context, message: String) { + val toast = Toast.makeText(context, message, Toast.LENGTH_SHORT) + toast.show() +} + +@Composable +private fun BoxWithFocusIndication( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit +) { + var isFocused by remember { mutableStateOf(false) } + val backgroundColor = if (isFocused) { + MaterialTheme.colorScheme.surfaceVariant + } else { + MaterialTheme.colorScheme.surface + } + Box( + modifier = modifier + .onFocusEvent { + isFocused = it.isFocused + } + .background(backgroundColor), + content = content + ) +} + +@Composable +private fun KeyEvents( + doSomething: () -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithFocusIndication(modifier) { + // [START android_compose_touchinput_keyboardinput_keyevents] + Box( + modifier = Modifier + .onKeyEvent { + if ( + it.type == KeyEventType.KeyUp && + it.key == Key.S + ) { + doSomething() + true + } else { + false + } + } + .focusable() + ) { + Text("Press S key") + } + // [END android_compose_touchinput_keyboardinput_keyevents] + } +} + +@Composable +private fun ModifierKeys( + doSomething: () -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithFocusIndication(modifier = modifier) { + // [START android_compose_touchinput_keyboardinput_modifierkeys] + Box( + modifier = Modifier + .focusable() + .onKeyEvent { + if ( + it.type == KeyEventType.KeyUp && + it.key == Key.S && + !it.isAltPressed && + !it.isCtrlPressed && + !it.isMetaPressed && + !it.isShiftPressed + ) { + doSomething() + true + } else { + false + } + } + ) { + Text("Press S key with a modifier key") + } + // [END android_compose_touchinput_keyboardinput_modifierkeys] + } +} + +@Composable +private fun SpacebarAndEnterKeyTriggersClickEvents( + togglePausePlay: () -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithFocusIndication(modifier = modifier) { + // [START android_compose_touchinput_keyboardinput_spacebar] + MediaPlayer(modifier = Modifier.clickable { togglePausePlay() }) + // [END android_compose_touchinput_keyboardinput_spacebar] + } +} + +@Composable +private fun MediaPlayer( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .size(200.dp) + .background(MaterialTheme.colorScheme.primaryContainer) + ) +} + +@Composable +private fun UnconsumedKeyEvents( + actionA: () -> Unit, + actionB: () -> Unit, + actionC: () -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithFocusIndication(modifier = modifier) { + // [START android_compose_touchinput_keyboardinput_unconsumedkeyevents] + OuterComponent( + modifier = Modifier.onKeyEvent { + when { + it.type == KeyEventType.KeyUp && it.key == Key.S -> { + actionB() // This function is never called. + true + } + + it.type == KeyEventType.KeyUp && it.key == Key.D -> { + actionC() + true + } + + else -> false + } + } + ) { + InnerComponent( + modifier = Modifier.onKeyEvent { + if (it.type == KeyEventType.KeyUp && it.key == Key.S) { + actionA() + true + } else { + false + } + } + ) + } + // [END android_compose_touchinput_keyboardinput_unconsumedkeyevents] + } +} + +@Composable +private fun OuterComponent( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) = + Box(content = content, modifier = modifier.focusable()) + +@Composable +private fun InnerComponent( + modifier: Modifier = Modifier +) { + Card(modifier = modifier.focusable()) { + Text("Press S key or D key", modifier = Modifier.padding(16.dp)) + } +} + +@Composable +private fun PreviewKeyEvents() { + // [START android_compose_touchinput_keyboardinput_previewkeyevents] + val focusManager = LocalFocusManager.current + var textFieldValue by remember { mutableStateOf(TextFieldValue()) } + + TextField( + textFieldValue, + onValueChange = { + textFieldValue = it + }, + modifier = Modifier.onPreviewKeyEvent { + if (it.type == KeyEventType.KeyUp && it.key == Key.Tab) { + focusManager.moveFocus(FocusDirection.Next) + true + } else { + false + } + } + ) + // [END android_compose_touchinput_keyboardinput_previewkeyevents] +} + +@Composable +private fun InterceptKeyEvents( + previewSKey: () -> Unit, + actionForPreview: (KeyEvent) -> Unit, + actionForKeyEvent: (KeyEvent) -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithFocusIndication(modifier = modifier) { + // [START android_compose_touchinput_keyboardinput_interceptevents] + Column( + modifier = Modifier.onPreviewKeyEvent { + if (it.key == Key.S) { + previewSKey() + true + } else { + false + } + } + ) { + Box( + modifier = Modifier + .focusable() + .onPreviewKeyEvent { + actionForPreview(it) + false + } + .onKeyEvent { + actionForKeyEvent(it) + true + } + ) { + Text("Press any key") + } + } + // [END android_compose_touchinput_keyboardinput_interceptevents] + } +} From 2be946cf3b8b3e4940623c08496bafab889cf5dc Mon Sep 17 00:00:00 2001 From: Chiko Shimizu Date: Tue, 15 Oct 2024 13:45:39 +0900 Subject: [PATCH 3/3] Fix the issue on casting a Context object into Activity --- .../keyboardinput/KeyboardShortcutsHelper.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt index ccf5d33d7..a7f21362f 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt @@ -66,9 +66,13 @@ class AnotherActivity : ComponentActivity() { setContent { MaterialTheme { // [START android_compose_keyboard_shortcuts_helper_request] - val activity = LocalContext.current as Activity + val activity = LocalContext.current as? Activity - Button(onClick = { activity.requestShowKeyboardShortcuts() }) { + Button( + onClick = { + activity?.requestShowKeyboardShortcuts() + } + ) { Text(text = "Show keyboard shortcuts") } // [END android_compose_keyboard_shortcuts_helper_request] @@ -97,7 +101,11 @@ class AnotherActivity : ComponentActivity() { "Message editing", listOf( KeyboardShortcutInfo("Select All", KeyEvent.KEYCODE_A, KeyEvent.META_CTRL_ON), - KeyboardShortcutInfo("Send a message", KeyEvent.KEYCODE_ENTER, KeyEvent.META_SHIFT_ON) + KeyboardShortcutInfo( + "Send a message", + KeyEvent.KEYCODE_ENTER, + KeyEvent.META_SHIFT_ON + ) ) )