大屏设备上的输入兼容性

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

  • 测试基本键盘支持,例如按 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") }
    }
}

如需了解更多信息,请参阅使用焦点组提供一致的导航

仅使用键盘测试对您应用中每个 UI 元素的访问。常用元素应无需鼠标或触摸输入即可访问。

请记住,键盘支持对于有无障碍需求的用户来说可能至关重要。

按键

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

一些示例包括:聊天应用使用 Enter 键发送消息,媒体应用使用 Spacebar 键启动和停止播放,以及游戏使用 wasd 键控制移动。

您可以使用 onKeyEvent 修饰符处理单个按键,该修饰符接受一个 lambda,当被修改的组件收到按键事件时会调用该 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 修饰符添加到绘图表面。实现一个 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>

当应用在系统中注册后,用户可以将其选为默认笔记应用。当请求新笔记时,应用应创建一个空笔记,准备好进行手写笔输入。当用户希望批注图片(例如屏幕截图或下载的图片)时,应用会启动并带有包含一个或多个 content:// URI 项的 ClipData。应用应创建一个笔记,将第一张附加图片用作背景图片,并进入用户可以使用手写笔在屏幕上绘图的模式。

在没有手写笔的情况下测试笔记 intent

[待删除部分。]

要在没有激活手写笔的情况下测试应用是否正确响应笔记 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 UI
  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
        },
    )
}

有关构建上下文菜单的详细信息,请参阅创建上下文菜单

悬停

通过处理悬停事件,您可以让您的应用布局感觉更精致、更易于使用。对于自定义组件尤其如此:

最常见的两个例子是:

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

拖放

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

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

要添加拖放支持,请参阅拖放,并查看ChromeOS 上的 Android — 实现拖放博客文章。

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" />

其他资源