في نظام التشغيل Android 11 والإصدارات الأحدث، تتيح ميزة "عناصر التحكّم في الجهاز" ضمن "الوصول السريع" للمستخدم عرض الأجهزة الخارجية، مثل المصابيح وأجهزة الترموستات والكاميرات، والتحكّم فيها بسرعة من خلال حمل المستخدم من خلال ثلاثة تفاعلات من مشغِّل التطبيقات التلقائي. يختار المصنّع الأصلي للجهاز مشغّل التطبيقات الذي يستخدمه. يمكن لمجمّعي الأجهزة، مثل Google Home، وتطبيقات المورّدين التابعة لجهات خارجية، توفير أجهزة لعرضها في هذه المساحة. توضّح لك هذه الصفحة كيفية عرض عناصر التحكّم بالجهاز في هذه المساحة وربطها بتطبيق التحكّم.
لإضافة هذا الدعم، يجب إنشاء ControlsProviderService
وتعريفه. يمكنك إنشاء عناصر التحكّم التي يتيحها تطبيقك استنادًا إلى أنواع عناصر التحكّم المحدَّدة مسبقًا، ثم إنشاء ناشري عناصر التحكّم هذه.
واجهة المستخدم
يتم عرض الأجهزة ضمن عناصر التحكم في الأجهزة على شكل تطبيقات مصغّرة نموذجية. وتتوفّر خمس أدوات للتحكّم في الجهاز، كما هو موضَّح في الشكل التالي:
|
|
|
|
|
ويؤدي النقر مع الاستمرار على تطبيق مصغّر إلى الانتقال إلى التطبيق للتحكّم بشكل أفضل. يمكنك تخصيص الرمز واللون في كل أداة، ولكن للحصول على أفضل تجربة للمستخدم، استخدِم الرمز واللون الافتراضيين إذا كانت المجموعة الافتراضية تتطابق مع الجهاز.
إنشاء الخدمة
يوضّح هذا القسم كيفية إنشاء ControlsProviderService
.
تخبر هذه الخدمة واجهة مستخدم نظام Android بأنّ تطبيقك يحتوي على عناصر تحكُّم في الجهاز يجب أن تظهر في منطقة عناصر التحكُّم في الجهاز من واجهة مستخدم Android.
تفترض واجهة برمجة التطبيقات ControlsProviderService
الإلمام بأحداث البث التفاعلية، على النحو المحدّد في مشروع GitHub للتدفقات التفاعلية والمنفّذ في واجهات مسار Java 9.
تم إنشاء واجهة برمجة التطبيقات استنادًا إلى المفاهيم التالية:
- الناشر: تطبيقك هو الناشر.
- المشترك: واجهة مستخدم النظام هي المشترك ويمكنها طلب عدد من عناصر التحكم من الناشر.
- الاشتراك: الإطار الزمني الذي يمكن خلاله للناشر إرسال التحديثات إلى واجهة مستخدم النظام. ويمكن إما للناشر أو المشترك إغلاق هذه النافذة.
الإعلان عن الخدمة
يجب أن يذكر تطبيقك خدمة، مثل MyCustomControlService
، في
بيان التطبيق.
يجب أن تتضمّن الخدمة فلتر أهداف للعنصر ControlsProviderService
. يتيح هذا الفلتر للتطبيقات المساهمة في عناصر التحكم في واجهة مستخدم النظام.
ستحتاج أيضًا إلى رمز 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>
بعد ذلك، أنشئ ملف Kotlin الجديد باسم MyCustomControlService.kt
ووسِّعه
ControlsProviderService()
:
Kotlin
class MyCustomControlService : ControlsProviderService() { ... }
Java
public class MyCustomJavaControlService extends ControlsProviderService { ... }
اختيار نوع عنصر التحكّم الصحيح
توفر واجهة برمجة التطبيقات طرق إنشاء لإنشاء عناصر التحكم. لملء منصة الإنشاء، حدد الجهاز الذي تريد التحكم فيه وكيفية تفاعل المستخدم معه. نفِّذ الخطوات التالية:
- اختَر نوع الجهاز الذي يمثله عنصر التحكّم. أما الفئة
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، يمكن للمستخدم اختيار عناصر التحكُّم المفضَّلة.
لاستخدام الكوروتينات في Kotlin لإنشاء ControlsProviderService
، أضِف تبعية جديدة إلى build.gradle
:
رائع
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> 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:
يؤدي النقر على عناصر التحكم في الجهاز إلى الانتقال إلى شاشة ثانية حيث يمكنك اختيار تطبيقك. وبعد اختيار التطبيق، سترى كيف ينشئ المقتطف السابق قائمة نظام مخصصة تعرض عناصر التحكم الجديدة، كما هو موضح في الشكل 5:
والآن، نفِّذ طريقة createPublisherFor()
مع إضافة ما يلي إلى Service
:
Kotlin
private val job = SupervisorJob() private val scope = CoroutineScope(Dispatchers.IO + job) private val controlFlows = mutableMapOf>() 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()); } }
شغِّل التطبيق ثم انتقِل إلى قائمة عناصر التحكّم بالأجهزة واطّلِع على عناصر التحكّم في الإضاءة وجهاز الترموستات.