在 Android 11 及更高版本中,“快速访问设备控制器”功能 让用户能够快速查看和控制外部设备,例如灯具、 温控器和摄像头 - 用户只需一次简单的操作, 默认启动器。设备 OEM 会选择自己使用的启动器。设备 聚合信息网站(如 Google Home)和第三方供应商应用 提供可在此空间中展示的设备本页介绍了如何展示 此空间中的设备控制器,并将其关联到您的控制应用。
<ph type="x-smartling-placeholder">如需添加此支持,请创建并声明 ControlsProviderService
。创建
根据预定义的控件类型,应用支持的控件,然后创建
这些控件
界面
设备以模板化微件的形式显示在设备控制器下。五 设备控制微件可用,如下图所示:
![]() |
![]() |
<ph type="x-smartling-placeholder">![]() |
![]() |
![]() |
触摸和按住微件即可转到应用,以便您进行更深入的控制。您可以 自定义每个微件的图标和颜色,但为了提供最佳用户体验, 如果默认组与设备匹配,则使用默认图标和颜色。
![显示温度面板 widget 的图片(已打开)](https://developer.android.google.cn/static/images/device-control/device-control-panel2.png?authuser=0000&hl=bg)
创建服务
本部分介绍了如何创建
ControlsProviderService
。
此服务会告知 Android 系统界面,您的应用包含设备控制器
必须在 Android 界面的设备控制器区域显示。
ControlsProviderService
API 假定您熟悉响应式流,例如
在响应式流 GitHub 中定义
项目
在 Java 9 Flow 中实现
界面。
该 API 围绕以下概念构建而成:
- 发布商:您的应用是发布商。
- 订阅者:系统界面是订阅者,可以请求号码 一系列控件
- 订阅:发布商可以发送更新的时间范围 进入系统界面发布者或订阅者都可以关闭此对话框 窗口。
声明服务
您的应用必须在 MyCustomControlService
应用清单
服务必须包含 ControlsProviderService
的 intent 过滤器。这个
过滤器允许应用向系统界面提供控件。
您还需要一个显示在系统界面的控件中的 label
。
以下示例展示了如何声明服务:
<service
android:name="MyCustomControlService"
android:label="My Custom Controls"
android:permission="android.permission.BIND_CONTROLS"
android:exported="true"
>
<intent-filter>
<action android:name="android.service.controls.ControlsProviderService" />
</intent-filter>
</service>
接下来,创建一个名为 MyCustomControlService.kt
的新 Kotlin 文件,并将其
扩展 ControlsProviderService()
:
Kotlin
class MyCustomControlService : ControlsProviderService() { ... }
Java
public class MyCustomJavaControlService extends ControlsProviderService { ... }
选择正确的控件类型
API 提供了用于创建控件的构建器方法。要填充 构建器,确定要控制的设备以及用户的互动方式 。请执行以下步骤:
- 选择控件代表的设备类型。通过
DeviceTypes
类是一个 枚举所有受支持的设备。类型用于确定 界面中的设备图标和颜色。 - 确定面向用户的名称、设备位置,例如 厨房—以及与控件相关的其他界面文本元素。
- 选择最佳模板,为用户互动提供支持。系统会为控件分配一个来自应用的
ControlTemplate
。此模板直接向 以及可用的输入法(即ControlAction
。 下表列出了一些可用的模板以及相关操作 支持:
模板 | 操作 | 说明 |
ControlTemplate.getNoTemplateObject()
|
None
|
应用可能会使用此字段传递有关控件、 但用户无法与之互动 |
ToggleTemplate
|
BooleanAction
|
表示可在启用和停用状态之间切换的控件。BooleanAction 对象包含一个会更改
来表示用户点按该控件时请求的新状态。
|
RangeTemplate
|
FloatAction
|
表示具有指定最小值、最大值和步长值的滑块微件。时间
当用户与滑块互动时,发送新的 FloatAction
对象返回给应用。
|
ToggleRangeTemplate
|
BooleanAction, FloatAction
|
此模板是 ToggleTemplate 和 RangeTemplate 的组合。它支持触摸事件以及滑块,
例如控制可调光的灯具
|
TemperatureControlTemplate
|
ModeAction, BooleanAction, FloatAction
|
除了封装上述操作之外,此模板还允许 用户设置了一个模式,例如供暖、制冷、供暖/制冷、节能或关闭。 |
StatelessTemplate
|
CommandAction
|
用于指示提供触控功能但其状态的控件 例如红外线电视遥控器。您可以使用此模板定义常规操作或宏,其中聚合了控件和状态更改。 |
利用这些信息,您可以创建控件:
- 如果控件的状态未知,就使用
Control.StatelessBuilder
构建器类。 - 如果控件的状态已知,就使用
Control.StatefulBuilder
构建器类。
例如,如需控制智能灯泡和温控器,请添加以下代码
常量添加到 MyCustomControlService
中:
Kotlin
private const val LIGHT_ID = 1234 private const val LIGHT_TITLE = "My fancy light" private const val LIGHT_TYPE = DeviceTypes.TYPE_LIGHT private const val THERMOSTAT_ID = 5678 private const val THERMOSTAT_TITLE = "My fancy thermostat" private const val THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT class MyCustomControlService : ControlsProviderService() { ... }
Java
public class MyCustomJavaControlService extends ControlsProviderService { private final int LIGHT_ID = 1337; private final String LIGHT_TITLE = "My fancy light"; private final int LIGHT_TYPE = DeviceTypes.TYPE_LIGHT; private final int THERMOSTAT_ID = 1338; private final String THERMOSTAT_TITLE = "My fancy thermostat"; private final int THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT; ... }
为控件创建发布者
创建控件后,它需要一个发布方。发布者将告知系统界面该控件的存在。ControlsProviderService
类有两种您必须在应用代码中替换的发布者方法:
createPublisherForAllAvailable()
:用于创建Publisher
找到您的应用中可用的所有控件使用Control.StatelessBuilder()
为该发布商构建Control
对象。createPublisherFor()
:为一系列给定控件创建一个Publisher
。 由其字符串标识符标识。使用Control.StatefulBuilder
执行以下操作: 构建这些Control
对象,因为发布商必须将状态分配给 每个控件
创建发布者
当您的应用首次将控件发布到系统界面时,应用并不知道
每个控件的状态获取状态可能是一项非常耗时的操作
涉及设备提供商网络中的多个跃点使用 createPublisherForAllAvailable()
方法将可用的控件告知系统。此方法使用
Control.StatelessBuilder
构建器类,因为每个控件的状态都是
未知。
控件显示在 Android UI 中后,用户可以选择收藏 控件。
如需使用 Kotlin 协程创建 ControlsProviderService
,请添加一个新的
依赖项添加到您的 build.gradle
:
Groovy
dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4" }
Kotlin
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4") }
同步 Gradle 文件后,请将以下代码段添加到 Service
中,
实现 createPublisherForAllAvailable()
:
Kotlin
class MyCustomControlService : ControlsProviderService() { override fun createPublisherForAllAvailable(): Flow.Publisher= flowPublish { send(createStatelessControl(LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE)) send(createStatelessControl(THERMOSTAT_ID, THERMOSTAT_TITLE, THERMOSTAT_TYPE)) } private fun createStatelessControl(id: Int, title: String, type: Int): Control { val intent = Intent(this, MainActivity::class.java) .putExtra(EXTRA_MESSAGE, title) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) val action = PendingIntent.getActivity( this, id, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) return Control.StatelessBuilder(id.toString(), action) .setTitle(title) .setDeviceType(type) .build() } override fun createPublisherFor(controlIds: List ): Flow.Publisher { TODO() } override fun performControlAction( controlId: String, action: ControlAction, consumer: Consumer ) { TODO() } }
Java
public class MyCustomJavaControlService extends ControlsProviderService { private final int LIGHT_ID = 1337; private final String LIGHT_TITLE = "My fancy light"; private final int LIGHT_TYPE = DeviceTypes.TYPE_LIGHT; private final int THERMOSTAT_ID = 1338; private final String THERMOSTAT_TITLE = "My fancy thermostat"; private final int THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT; private boolean toggleState = false; private float rangeState = 18f; private final Map<String, ReplayProcessor> controlFlows = new HashMap<>(); @NonNull @Override public Flow.Publisher createPublisherForAllAvailable() { List controls = new ArrayList<>(); controls.add(createStatelessControl(LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE)); controls.add(createStatelessControl(THERMOSTAT_ID, THERMOSTAT_TITLE, THERMOSTAT_TYPE)); return FlowAdapters.toFlowPublisher(Flowable.fromIterable(controls)); } @NonNull @Override public Flow.Publisher createPublisherFor(@NonNull List controlIds) { ReplayProcessor updatePublisher = ReplayProcessor.create(); controlIds.forEach(control -> { controlFlows.put(control, updatePublisher); updatePublisher.onNext(createLight()); updatePublisher.onNext(createThermostat()); }); return FlowAdapters.toFlowPublisher(updatePublisher); } }
向下滑动系统菜单并找到设备控制器按钮,如 图 4:
![一张图片,显示设备控制器的系统界面](https://developer.android.google.cn/static/images/ui/device_controls_ui.png?authuser=0000&hl=bg)
点按设备控制器会转到第二个屏幕,供您选择 。选择应用后,您会看到上一个代码段如何创建 显示新控件的自定义系统菜单,如图 5 所示:
![一张图片,显示包含灯具和温控器控件的系统菜单](https://developer.android.google.cn/static/images/ui/device_controls_example_1.png?authuser=0000&hl=bg)
现在,实现 createPublisherFor()
方法,并将以下代码添加到
Service
:
Kotlin
private val job = SupervisorJob() private val scope = CoroutineScope(Dispatchers.IO + job) private val controlFlows = mutableMapOf<String, MutableSharedFlow>() private var toggleState = false private var rangeState = 18f override fun createPublisherFor(controlIds: List ): Flow.Publisher { val flow = MutableSharedFlow (replay = 2, extraBufferCapacity = 2) controlIds.forEach { controlFlows[it] = flow } scope.launch { delay(1000) // Retrieving the toggle state. flow.tryEmit(createLight()) delay(1000) // Retrieving the range state. flow.tryEmit(createThermostat()) } return flow.asPublisher() } private fun createLight() = createStatefulControl( LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE, toggleState, ToggleTemplate( LIGHT_ID.toString(), ControlButton( toggleState, toggleState.toString().uppercase(Locale.getDefault()) ) ) ) private fun createThermostat() = createStatefulControl( THERMOSTAT_ID, THERMOSTAT_TITLE, THERMOSTAT_TYPE, rangeState, RangeTemplate( THERMOSTAT_ID.toString(), 15f, 25f, rangeState, 0.1f, "%1.1f" ) ) private fun createStatefulControl(id: Int, title: String, type: Int, state: T, template: ControlTemplate): Control { val intent = Intent(this, MainActivity::class.java) .putExtra(EXTRA_MESSAGE, "$title $state") .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) val action = PendingIntent.getActivity( this, id, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) return Control.StatefulBuilder(id.toString(), action) .setTitle(title) .setDeviceType(type) .setStatus(Control.STATUS_OK) .setControlTemplate(template) .build() } override fun onDestroy() { super.onDestroy() job.cancel() }
Java
@NonNull @Override public Flow.PublishercreatePublisherFor(@NonNull List controlIds) { ReplayProcessor updatePublisher = ReplayProcessor.create(); controlIds.forEach(control -> { controlFlows.put(control, updatePublisher); updatePublisher.onNext(createLight()); updatePublisher.onNext(createThermostat()); }); return FlowAdapters.toFlowPublisher(updatePublisher); } private Control createStatelessControl(int id, String title, int type) { Intent intent = new Intent(this, MainActivity.class) .putExtra(EXTRA_MESSAGE, title) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent action = PendingIntent.getActivity( this, id, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); return new Control.StatelessBuilder(id + "", action) .setTitle(title) .setDeviceType(type) .build(); } private Control createLight() { return createStatefulControl( LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE, toggleState, new ToggleTemplate( LIGHT_ID + "", new ControlButton( toggleState, String.valueOf(toggleState).toUpperCase(Locale.getDefault()) ) ) ); } private Control createThermostat() { return createStatefulControl( THERMOSTAT_ID, THERMOSTAT_TITLE, THERMOSTAT_TYPE, rangeState, new RangeTemplate( THERMOSTAT_ID + "", 15f, 25f, rangeState, 0.1f, "%1.1f" ) ); } private Control createStatefulControl(int id, String title, int type, T state, ControlTemplate template) { Intent intent = new Intent(this, MainActivity.class) .putExtra(EXTRA_MESSAGE, "$title $state") .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent action = PendingIntent.getActivity( this, id, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); return new Control.StatefulBuilder(id + "", action) .setTitle(title) .setDeviceType(type) .setStatus(Control.STATUS_OK) .setControlTemplate(template) .build(); }
在此示例中,createPublisherFor()
方法包含一个虚构实例,
实现您的应用必须执行的操作:与您的设备通信
检索其状态,并将该状态发送到系统。
createPublisherFor()
方法使用 Kotlin 协程和数据流来满足
所需的 Reactive Streams API:
- 创建
Flow
。 - 等待一秒钟。
- 创建并发出智能灯的状态。
- 再等待一秒。
- 创建并发出温控器的状态。
处理操作
performControlAction()
方法会在用户与
发布的控件发送的 ControlAction
类型决定了操作。
对给定控件执行适当的操作,然后更新状态
在 Android 界面中查看设备的自定义状态。
如需完成示例,请将以下内容添加到 Service
中:
Kotlin
override fun performControlAction( controlId: String, action: ControlAction, consumer: Consumer) { controlFlows[controlId]?.let { flow -> when (controlId) { LIGHT_ID.toString() -> { consumer.accept(ControlAction.RESPONSE_OK) if (action is BooleanAction) toggleState = action.newState flow.tryEmit(createLight()) } THERMOSTAT_ID.toString() -> { consumer.accept(ControlAction.RESPONSE_OK) if (action is FloatAction) rangeState = action.newValue flow.tryEmit(createThermostat()) } else -> consumer.accept(ControlAction.RESPONSE_FAIL) } } ?: consumer.accept(ControlAction.RESPONSE_FAIL) }
Java
@Override public void performControlAction(@NonNull String controlId, @NonNull ControlAction action, @NonNull Consumerconsumer) { ReplayProcessor processor = controlFlows.get(controlId); if (processor == null) return; if (controlId.equals(LIGHT_ID + "")) { consumer.accept(ControlAction.RESPONSE_OK); if (action instanceof BooleanAction) toggleState = ((BooleanAction) action).getNewState(); processor.onNext(createLight()); } if (controlId.equals(THERMOSTAT_ID + "")) { consumer.accept(ControlAction.RESPONSE_OK); if (action instanceof FloatAction) rangeState = ((FloatAction) action).getNewValue() processor.onNext(createThermostat()); } }
运行应用,进入设备控制器菜单,然后查看指示灯和 温控器控件。
![显示灯具和温控器控件的图片](https://developer.android.google.cn/static/images/ui/device_controls_example_2.png?authuser=0000&hl=bg)