在 Android 11 以上版本中,「快速存取裝置控制項」功能 可讓使用者快速查看及控制外部裝置,例如燈具、 溫度控制器和攝影機 預設啟動器裝置原始設備製造商 (OEM) 會選擇要使用的啟動器。裝置 集結網站 (例如 Google Home) 和第三方供應商的應用程式, 提供要在這個空間中顯示的裝置。本頁說明如何 並將裝置控制項連結至你的控制應用程式
如要新增這項支援功能,請建立並宣告 ControlsProviderService
。建立
以預先定義的控制項類型為依據
以便使用這些控制項
使用者介面
裝置會以樣板化的小工具顯示在「Device controls」中。五 可用的裝置控制小工具,如下圖所示:
![]() |
![]() |
![]() |
![]() |
![]() |
觸控與按下小工具可前往應用程式進行更多控制你可以 自訂每個小工具的圖示和顏色,但為了提供最佳使用者體驗, 如果預設圖示和顏色符合裝置,則使用預設圖示和顏色。
![顯示溫度面板小工具 (開啟) 的圖片](https://developer.android.google.cn/static/images/device-control/device-control-panel2.png?authuser=0000&hl=zh-tw)
建立服務
本節將說明如何建立
ControlsProviderService
。
這項服務會告知 Android 系統 UI,您的應用程式含有裝置控制項
都必須顯示在 Android UI 的「Device controls」區域。
ControlsProviderService
API 會假設您熟悉回應式串流,
Reactive Streams GitHub 中所定義
專案
並在 Java 9 流程
介面。
這個 API 是以下列概念為基礎:
- 發布商:應用程式是由發布商負責。
- 訂閱者:系統 UI 是訂閱者,可要求號碼 這些都是重要的控管選項
- 訂閱:發布商可傳送更新的時間範圍 。發布商或訂閱者都可以關閉這個應用程式 視窗。
宣告服務
您的應用程式必須在以下位置宣告服務 (例如 MyCustomControlService
)
含有應用程式資訊清單。
服務必須為 ControlsProviderService
加上一個意圖篩選器。這個
篩選器可讓應用程式為系統 UI 提供控制項。
您還需要在系統 UI 控制項中顯示的 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
類別是 所有支援裝置的列舉。類型可用來決定 使用者介面中裝置的圖示和顏色。 - 確定向使用者顯示的名稱和裝置位置,例如 廚房與其他 UI 文字元素
- 選擇最適合用來支援使用者互動的範本。應用程式會指派一個
ControlTemplate
給控制項。這個範本會直接向 以及可用的輸入方式,也就是ControlAction
。 下表大致列出部分可用的範本和操作 他們支援的瀏覽器:
範本 | 操作 | 說明 |
ControlTemplate.getNoTemplateObject()
|
None
|
應用程式可能會利用這個物件來傳送控制項的相關資訊、 但無法與其互動 |
ToggleTemplate
|
BooleanAction
|
代表該控制項可在啟用及停用狀態之間切換。BooleanAction 物件包含會變更的欄位
在使用者輕觸控制項時,代表要求的新狀態。
|
RangeTemplate
|
FloatAction
|
代表含有指定最小值、最大值和步數值的滑桿小工具。時間
使用者與滑桿互動,傳送新的 FloatAction
即可將物件傳回至含有更新值的應用程式。
|
ToggleRangeTemplate
|
BooleanAction, FloatAction
|
本範本是 ToggleTemplate 和
RangeTemplate 。支援觸控事件和滑桿
例如控制可調暗的燈光
|
TemperatureControlTemplate
|
ModeAction, BooleanAction, FloatAction
|
除了封裝上述動作外,這個範本還可讓您 使用者設定模式 (例如暖氣、冷氣、暖氣/冷氣、節能或關閉)。 |
StatelessTemplate
|
CommandAction
|
用於表示提供觸控功能的控制項,但其狀態 例如 IR 電視遙控器您可以使用 定義處理常式或巨集; 以及狀態變更 |
有了這些資訊,您就能建立控制項:
- 如果控制項的狀態不明,請使用
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; ... }
為控制項建立發布商
建立控制項之後,需要一個發布商。發布商會在設定的
控制項的存在的系統 UIControlsProviderService
類別有兩個發布商方法,必須在應用程式的程式碼中將其覆寫:
createPublisherForAllAvailable()
:建立Publisher
在應用程式中使用所有可用控制項使用Control.StatelessBuilder()
為這個發布商建立Control
物件。createPublisherFor()
:針對指定控制項清單建立Publisher
。 以字串識別碼識別。使用Control.StatefulBuilder
即可: 建立這些Control
物件,因為發布商必須指派狀態給 每個控制項中
建立發布商
應用程式首次將控制項發布至系統 UI 時,應用程式無法得知
每個控制項的狀態取得狀態是一項非常耗時的作業
包含許多躍點。使用 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); } }
將系統選單向下滑動,並找出「Device controls」按鈕,如下圖所示 圖 4:
![顯示裝置控制功能系統 UI 的圖片](https://developer.android.google.cn/static/images/ui/device_controls_ui.png?authuser=0000&hl=zh-tw)
輕觸「裝置控制項」,即可前往第二個畫面選取 選取應用程式後,您會看到先前的程式碼片段 自訂系統選單顯示新的控制項,如圖 5 所示:
![這張圖片顯示系統選單,內含燈具和溫度控制器控制選項](https://developer.android.google.cn/static/images/ui/device_controls_example_1.png?authuser=0000&hl=zh-tw)
現在,實作 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
。 - 等候 1 秒鐘。
- 建立並發出智慧型燈具的狀態。
- 再等一下。
- 建立並發出溫度控制器的狀態。
處理動作
performControlAction()
方法會在使用者與
動作的 ControlAction
類型取決於已傳送。
對給定的控制項採取適當操作,然後更新狀態
。
如要完成範例,請在 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()); } }
執行應用程式,存取「Device controls」選單,並查看燈具和 溫度控制器控制選項
![顯示燈具和溫度控制器控制的圖片](https://developer.android.google.cn/static/images/ui/device_controls_example_2.png?authuser=0000&hl=zh-tw)