大屏幕上的输入兼容性

在大屏设备上,用户通常使用键盘、鼠标、触控板、触控笔或游戏手柄与应用互动。如需让应用能够接受来自外部设备的输入,请执行以下操作:

  • 测试基本键盘支持,例如使用 Ctrl+Z 执行撤消操作、使用 Ctrl+C 执行复制操作,以及使用 Ctrl+S 执行保存操作。如需查看默认键盘快捷键的列表,请参阅处理键盘操作
  • 测试高级键盘支持,例如通过 Tab 键和箭头键进行键盘导航、按 Enter 键确认文字输入,以及在媒体应用中使用 Spacebar 键执行播放和暂停操作。
  • 测试基本的鼠标交互,包括通过右键点击打开上下文菜单、悬停鼠标时图标的变化,以及自定义组件上的鼠标滚轮或触控板滚动事件。
  • 测试应用专用的输入设备,如触控笔、游戏控制器和音乐应用 MIDI 控制器。
  • 考虑支持高级输入方式,让您的应用在桌面环境中脱颖而出;例如,支持将触控板用作 DJ 应用的唱片平滑转换器、让游戏支持鼠标捕捉,以及支持为以键盘为主要输入方式的用户设置键盘快捷键。

键盘

您的应用响应键盘输入的方式有助于打造良好的大屏幕用户体验。有三种类型的键盘输入:导航按键快捷键

键盘导航很少在以触摸为主要输入方式的应用中实现,但如果用户在使用应用时将手放在键盘上,他们就希望使用键盘导航。对于需要在手机、平板电脑、可折叠设备和桌面设备上使用无障碍功能的用户来说,键盘导航可能是必不可少的。

对于许多应用,箭头键和 Tab 键导航由 Android 框架自动处理。例如,某些可组合项默认可聚焦,例如 Button 或带有 clickable 修饰符的可组合项;键盘导航功能通常应无需添加任何代码即可实现。如需针对默认情况下不可聚焦的自定义可组合项启用键盘导航,请添加 focusable 修饰符:

var color by remember { mutableStateOf(Green) }
Box(
    Modifier
        .background(color)
        .onFocusChanged { color = if (it.isFocused) Blue else Green }
        .focusable()
) {
    Text("Focusable 1")
}

如需了解详情,请参阅使可组合项可聚焦

启用焦点后,Android 框架将根据组件的相应位置为所有可聚焦的组件创建导航映射。这通常可以按预期运行,无需进一步的开发。

不过,Compose 并不总能为标签页和列表等复杂可组合项确定正确的标签页导航下一项,例如,当其中一个可组合项是未完全显示的水平滚动条时。

如需控制焦点行为,请将 focusGroup 修饰符添加到可组合函数集合的父级可组合函数。焦点会先移至组,然后在组内移动,最后再移至下一个可聚焦的组件,例如:

Row {
    Column(Modifier.focusGroup()) {
        Button({}) { Text("Row1 Col1") }
        Button({}) { Text("Row2 Col1") }
        Button({}) { Text("Row3 Col1") }
    }
    Column(Modifier.focusGroup()) {
        Button({}) { Text("Row1 Col2") }
        Button({}) { Text("Row2 Col2") }
        Button({}) { Text("Row3 Col2") }
    }
}

如需了解详情,请参阅通过焦点小组提供一致的导航

仅使用键盘测试对应用的每个界面元素的访问权限。在没有鼠标输入或触控输入的情况下,应能访问常用的元素。

请注意,对于需要使用无障碍功能的用户来说,键盘支持可能是必不可少的。

按键

对于将由屏幕虚拟键盘 (IME) 处理的文字输入,例如、TextField、,应用应在大屏设备上按预期运行,而无需开发者执行额外的开发工作。对于框架无法预料的按键,应用需要自行处理相应的行为。对于具有自定义视图的应用来说尤其如此。

例如,聊天应用使用 Enter 键发送消息,媒体应用使用 Spacebar 键开始和停止播放,游戏使用 wasd 键控制移动,等等。

您可以使用 onKeyEvent 修饰符处理单个按键操作,该修饰符接受一个 lambda,用于在修改后的组件收到按键事件时调用。借助 KeyEvent#type 属性,您可以确定事件是按键按下 (KeyDown) 还是按键松开 (KeyUp):

Box(
    modifier = Modifier.focusable().onKeyEvent {
        if(
            it.type == KeyEventType.KeyUp &&
            it.key == Key.S
        ) {
            doSomething()
            true
        } else {
            false
        }
    }
)  {
    Text("Press S key")
}

或者,您也可以替换 onKeyUp() 回调,并针对收到的每个键码添加预期的行为:

override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
    return when (keyCode) {
        KeyEvent.KEYCODE_ENTER -> {
            sendChatMessage()
            true
        }
        KeyEvent.KEYCODE_SPACE -> {
            playOrPauseMedia()
            true
        }
        else -> super.onKeyUp(keyCode, event)
    }
}

当用户松开键时,会发生 onKeyUp 事件。使用此回调可防止在用户缓慢地按住或松开某个键时应用需要处理多个 onKeyDown 事件。如果游戏和应用需要检测用户何时按下按键或用户是否按住按键,可以监听 onKeyDown 事件并自行处理重复的 onKeyDown 事件。

如需了解详情,请参阅处理键盘操作

快捷键

使用硬件键盘时,用户希望实现包含 CtrlAltShiftMeta 键的常见键盘快捷键。如果应用不实现快捷键,用户可能会觉得应用使用起来不顺手。高级用户也喜欢使用快捷键来完成常用的应用专属任务。实现快捷键不仅能让应用更容易使用,而且与没有快捷键的应用相比也能彰显出自己的优势。

一些常用的快捷键包括 Ctrl+S(保存)、Ctrl+Z(撤消)和 Ctrl+Shift+Z(重做),等等。如需查看默认快捷键的列表,请参阅处理键盘操作

KeyEvent 对象具有以下属性,用于指示是否按下了修饰键:

例如:

Box(
    Modifier.onKeyEvent {
        if (it.isAltPressed && it.key == Key.A) {
            println("Alt + A is pressed")
            true
        } else {
            false
        }
    }
    .focusable()
)

如需了解详情,请参阅以下内容:

触控笔

许多大屏幕设备都配有触控笔。Android 应用将触控笔输入作为触摸屏输入进行处理。某些设备可能还配备 USB 或蓝牙绘图板,如 Wacom Intuos。Android 应用可以接收蓝牙输入,但不支持 USB 输入。

如需访问触控笔 MotionEvent 对象,请将 pointerInteropFilter 修饰符添加到绘制 Surface。使用用于处理动作事件的方法实现 ViewModel 类;将该方法作为 pointerInteropFilter 修饰符的 onTouchEvent lambda 传递:

@Composable
@OptIn(ExperimentalComposeUiApi::class)
fun DrawArea(modifier: Modifier = Modifier) {
   Canvas(modifier = modifier
       .clipToBounds()
       .pointerInteropFilter {
           viewModel.processMotionEvent(it)
       }

   ) {
       // Drawing code here.
   }
}

MotionEvent 对象包含与事件相关的信息:

历史点

Android 会对输入事件进行批处理,并且每帧传送一次。触控笔可以按比显示屏高得多的频率来报告事件。创建绘图应用时,请使用 getHistorical API 检查最近可能发生的事件:

防止手掌误触

使用触控笔绘图、手写或与您的应用互动时,用户有时会用手掌触摸屏幕。系统可能会在将其识别为手掌误触并予以忽略之前便将相应触摸事件(设置为 ACTION_DOWNACTION_POINTER_DOWN)报告给您的应用。

Android 通过分派 MotionEvent 来取消手掌触摸事件。如果您的应用收到 ACTION_CANCEL,则应取消相应手势。如果您的应用收到 ACTION_POINTER_UP,则应检查 FLAG_CANCELED 是否已设置。如果已设置,则应取消相应手势。

请勿仅检查 FLAG_CANCELED。在 Android 13(API 级别 33)及更高版本中,系统会为 ACTION_CANCEL 事件设置 FLAG_CANCELED,但在较低 Android 版本中,系统不会设置此标志。

Android 12

在 Android 12(API 级别 32)及更低版本上,只能针对单指针触摸事件检测手掌误触。如果手掌触摸是唯一的指针,系统会通过在动作事件对象上设置 ACTION_CANCEL 来取消相应事件。如果还有其他指针,系统会设置 ACTION_POINTER_UP,这不足以检测手掌误触。

Android 13

在 Android 13(API 级别 33)及更高版本中,如果手掌触摸是唯一的指针,系统会对动作事件对象设置 ACTION_CANCELFLAG_CANCELED 以取消相应事件。如果还有其他指针,系统会设置 ACTION_POINTER_UPFLAG_CANCELED

每当您的应用收到带有 ACTION_POINTER_UP 的动作事件时,请检查 FLAG_CANCELED 以确定相应事件是否指示防止手掌误触(或其他事件取消)。

记事应用

ChromeOS 具有一个特殊的 intent,用于将已注册的记事应用呈现给用户。如需将应用注册为记事应用,请将以下内容添加到应用清单中:

<intent-filter>
    <action android:name="org.chromium.arc.intent.action.CREATE_NOTE" />
    <category android:name="android.intent.category.DEFAULT" />
</intent-filter>

向系统注册某个应用后,用户可以选择它作为默认的记事应用。当收到新记事请求时,该应用应创建一条空记事,以备处理触控笔输入。当用户希望标注图片(如屏幕截图或下载的图片)时,该应用会使用 ClipData(包含一个或多个带有 content:// URI 的项目)启动。该应用应创建一条记事,这条记事将附加的第一张图片用作背景图片,该应用还应进入一种模式,用户可以在这种模式下使用触控笔在屏幕上进行绘制。

在没有触控笔的情况下测试记事 intent

[TBD remove section.]

如需测试在没有主动式触控笔的情况下应用是否可以正确响应记事 intent,请使用以下方法显示 ChromeOS 上的记事选项:

  1. 切换到开发模式并使设备可写入
  2. Ctrl+Alt+F2 打开终端
  3. 运行命令 sudo vi /etc/chrome_dev.conf
  4. i 进行修改,并将 --ash-enable-palette 添加到文件末尾且要另起一行
  5. Esc 键进行保存,然后输入 :wq 并按 Enter
  6. Ctrl+Alt+F1 键以返回 ChromeOS 常规界面
  7. 退出账号,然后重新登录

任务栏中现在应该有一个触控笔菜单:

  • 点按任务栏中的触控笔按钮,然后选择新建记事。这样应该会打开一条空白绘图记事。
  • 截取屏幕截图。在任务栏中依次选择触控笔按钮 > 截取屏幕,或者下载图片。通知中应该会有标注图片的选项。这样应该会启动应用,并且图片已做好标注的准备。

鼠标和触控板支持

大多数应用通常只需要处理三种以大屏幕为中心的事件:右键点击悬停拖放

右键点击

会使应用显示上下文菜单的所有操作(如轻触并按住列表项)也应该对右键点击事件作出反应。

为了处理右键点击事件,应用应注册 View.OnContextClickListener

Box(modifier = Modifier.fillMaxSize()) {
    AndroidView(
        modifier = Modifier.fillMaxSize(),
        factory = { context ->
            val rootView = FrameLayout(context)
            val onContextClickListener =
                View.OnContextClickListener { view ->
                    showContextMenu()
                    true
                }
            rootView.setOnContextClickListener(onContextClickListener)
            rootView
        },
    )
}

如需详细了解如何构造上下文菜单,请参阅创建上下文菜单

悬停

您可以通过处理悬停事件,使其应用布局更美观且更易于使用。对于自定义组件,这一点尤为重要:

这方面最常见的两个示例如下:

  • 通过改变鼠标指针图标,向用户表明某个元素是否具有交互行为,如可点击或可修改
  • 当指针悬停在大型列表或网格中的项目上时,向这些项目添加视觉反馈

拖放

在多窗口环境中,用户希望能够在应用之间拖放项目。桌面设备以及分屏模式下的平板电脑、手机和可折叠设备就是如此。

考虑用户是否有可能将项目拖入您的应用。例如,照片编辑器应该能接收照片、音频播放器应该能接收音频文件、绘图程序应该能接收照片。

如需添加拖放支持,请参阅 拖放 ,并参阅 Android on ChromeOS - 实现拖放博文。

有关 ChromeOS 的特殊注意事项

高级指针支持

对鼠标和触控板输入进行高级处理的应用应实现 pointerInput 修饰符,以获取 PointerEvent

@Composable
private fun LogPointerEvents(filter: PointerEventType? = null) {
    var log by remember { mutableStateOf("") }
    Column {
        Text(log)
        Box(
            Modifier
                .size(100.dp)
                .background(Color.Red)
                .pointerInput(filter) {
                    awaitPointerEventScope {
                        while (true) {
                            val event = awaitPointerEvent()
                            // handle pointer event
                            if (filter == null || event.type == filter) {
                                log = "${event.type}, ${event.changes.first().position}"
                            }
                        }
                    }
                }
        )
    }
}

检查 PointerEvent 对象,以确定以下各项:

游戏控制器

某些大屏幕 Android 设备最多支持四个游戏控制器。使用标准的 Android 游戏控制器 API 来处理游戏控制器(请参阅支持游戏控制器)。

游戏控制器按钮按照通用映射映射到通用值。但并非所有游戏控制器制造商都遵循相同的映射惯例。如果您允许用户选择不同的常见控制器映射,就能提供更好的体验。如需了解详情,请参阅处理游戏手柄按钮按下操作

输入转换模式

默认情况下,ChromeOS 会启用输入转换模式。对于大多数 Android 应用来说,此模式有助于应用在桌面环境中按预期运行。例如,在触控板上自动启用双指滚动、鼠标滚轮滚动,以及将原始显示坐标映射到窗口坐标,等等。通常,应用开发者不需要自行实现上述任何行为。

如果某个应用实现了自定义输入行为(例如,定义一种自定义触控板双指张合操作),或者这些输入转换未提供该应用预期的输入事件,您可以停用输入转换模式,方法是将以下代码添加到 Android 清单:

<uses-feature
    android:name="android.hardware.type.pc"
    android:required="false" />

其他资源