استخدِم المكوّنات الإضافية في "مكتبة واجهة مستخدم السيارة" لإنشاء عمليات تنفيذ كاملة لتخصيصات المكوّنات في "مكتبة واجهة مستخدم السيارة" بدلاً من استخدام عمليات تراكب الموارد في وقت التشغيل (RRO). تتيح لك عمليات RRO تغيير موارد XML فقط لمكوّنات مكتبة واجهة مستخدم السيارة، ما يحدّ من نطاق التخصيص.
إنشاء إضافة
المكوّن الإضافي لمكتبة واجهة مستخدم السيارة هو حزمة APK تحتوي على فئات تنفّذ مجموعة من واجهات برمجة التطبيقات للمكوّنات الإضافية. يمكن تجميع واجهات برمجة التطبيقات الخاصة بالإضافات في إضافة كمكتبة ثابتة.
اطّلِع على الأمثلة في Soong وGradle:
Soong
إليك مثال على Soong:
android_app {
name: "my-plugin",
min_sdk_version: "28",
target_sdk_version: "30",
aaptflags: ["--shared-lib"],
sdk_version: "current",
manifest: "src/main/AndroidManifest.xml",
srcs: ["src/main/java/**/*.java"],
resource_dirs: ["src/main/res"],
static_libs: [
"car-ui-lib-oem-apis",
],
// Disable optimization is mandatory to prevent R.java class from being
// stripped out
optimize: {
enabled: false,
},
certificate: ":my-plugin-certificate",
}
Gradle
اطّلِع على الملف build.gradle
التالي:
apply plugin: 'com.android.application'
android {
compileSdkVersion 30
defaultConfig {
minSdkVersion 28
targetSdkVersion 30
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
signingConfigs {
debug {
storeFile file('chassis_upload_key.jks')
storePassword 'chassis'
keyAlias 'chassis'
keyPassword 'chassis'
}
}
}
dependencies {
implementation project(':oem-apis')
// Or use the following if you'd like to use the maven artifact
// implementation 'com.android.car.ui:car-ui-lib-plugin-apis:1.0.0'
}
Settings.gradle
:
// You can remove the ':oem-apis' if you're using the maven artifact.
include ':oem-apis'
project(':oem-apis').projectDir = new File('./path/to/oem-apis')
include ':my-plugin'
project(':my-plugin').projectDir = new File('./my-plugin')
يجب أن يتضمّن المكوّن الإضافي موفّر محتوى تم تحديده في ملف البيان ويتضمّن السمات التالية:
android:authorities="com.android.car.ui.plugin"
android:enabled="true"
android:exported="true"
تجعل السمة android:authorities="com.android.car.ui.plugin"
المكوّن الإضافي قابلاً للاكتشاف
من خلال مكتبة Car UI. يجب تصدير الموفّر حتى يمكن الاستعلام عنه في وقت التشغيل. بالإضافة إلى ذلك، إذا تم ضبط السمة enabled
على false
، سيتم استخدام التنفيذ التلقائي بدلاً من تنفيذ المكوّن الإضافي. ليس من الضروري أن تكون فئة موفّر المحتوى متوفّرة. في هذه الحالة، احرص على إضافة
tools:ignore="MissingClass"
إلى تعريف مقدّم الخدمة. اطّلِع على نموذج إدخال البيان التالي:
<application>
<provider
android:name="com.android.car.ui.plugin.PluginNameProvider"
android:authorities="com.android.car.ui.plugin"
android:enabled="false"
android:exported="true"
tools:ignore="MissingClass"/>
</application>
أخيرًا، كإجراء أمني، عليك توقيع تطبيقك.
الإضافات كمكتبة مشتركة
على عكس مكتبات Android الثابتة التي يتم تجميعها مباشرةً في التطبيقات، يتم تجميع مكتبات Android المشترَكة في حزمة APK مستقلة تشير إليها التطبيقات الأخرى في وقت التشغيل.
تتم إضافة الفئات تلقائيًا إلى أداة تحميل الفئات المشتركة بين التطبيقات إذا تم تنفيذ الإضافات كمكتبة مشتركة في Android. عندما يحدّد تطبيق يستخدم "مكتبة واجهة مستخدم السيارة" تبعية وقت التشغيل لمكتبة المكوّن الإضافي المشتركة، يمكن لمحمّل الفئات الوصول إلى فئات مكتبة المكوّن الإضافي المشتركة. يمكن أن تؤثر المكوّنات الإضافية التي يتم تنفيذها كتطبيقات Android عادية (وليست مكتبة مشتركة) بشكل سلبي في أوقات بدء تشغيل التطبيق على البارد.
تنفيذ المكتبات المشتركة وإنشاؤها
يشبه التطوير باستخدام مكتبات Android المشترَكة عملية تطوير تطبيقات Android العادية، مع بعض الاختلافات الرئيسية.
- استخدِم العلامة
library
ضمن العلامةapplication
مع اسم حزمة المكوّن الإضافي في بيان تطبيق المكوّن الإضافي:
<application>
<library android:name="com.chassis.car.ui.plugin" />
...
</application>
- اضبط قاعدة إنشاء Soong
android_app
(Android.bp
) باستخدام علامة AAPTshared-lib
، والتي تُستخدَم لإنشاء مكتبة مشترَكة:
android_app {
...
aaptflags: ["--shared-lib"],
...
}
العناصر التابعة للمكتبات المشتركة
بالنسبة إلى كل تطبيق على النظام يستخدم مكتبة Car UI، أدرِج العلامة
uses-library
في ملف بيان التطبيق ضمن العلامة
application
مع اسم حزمة المكوّن الإضافي:
<manifest>
<application
android:name=".MyApp"
...>
<uses-library android:name="com.chassis.car.ui.plugin" android:required="false"/>
...
</application>
</manifest>
تثبيت مكوّن إضافي
يجب تثبيت المكوّنات الإضافية مسبقًا في قسم النظام من خلال تضمين الوحدة في PRODUCT_PACKAGES
. يمكن تحديث الحِزمة المثبَّتة مسبقًا بالطريقة نفسها التي يتم بها تحديث أي تطبيق آخر مثبَّت.
إذا كنت بصدد تحديث إضافة حالية على النظام، سيتم إغلاق أي تطبيقات تستخدم هذه الإضافة تلقائيًا. وبعد أن يعيد المستخدم فتحها، ستظهر له التغييرات المعدَّلة. إذا لم يكن التطبيق قيد التشغيل، سيتم تحديث المكوّن الإضافي في المرة التالية التي يتم فيها تشغيل التطبيق.
عند تثبيت مكوّن إضافي باستخدام "استوديو Android"، هناك بعض الاعتبارات الإضافية التي يجب أخذها في الحسبان. في وقت كتابة هذا المقال، كانت هناك خطأ في عملية تثبيت تطبيق Android Studio يتسبّب في عدم تطبيق تحديثات أحد المكوّنات الإضافية. يمكن حلّ هذه المشكلة من خلال اختيار الخيار التثبيت دائمًا باستخدام مدير الحِزم (يؤدي ذلك إلى إيقاف تحسينات النشر على Android 11 والإصدارات الأحدث) في إعدادات الإصدار الخاصة بالمكوّن الإضافي.
بالإضافة إلى ذلك، عند تثبيت المكوّن الإضافي، يعرض Android Studio رسالة خطأ تفيد بأنّه لا يمكنه العثور على نشاط رئيسي لتشغيله. وهذا أمر متوقّع، لأنّ المكوّن الإضافي لا يتضمّن أي أنشطة (باستثناء الغرض الفارغ المستخدَم لتحديد الغرض). لإزالة الخطأ، غيِّر خيار التشغيل إلى لا شيء في إعدادات الإصدار.
الشكل 1. إعداد المكوّن الإضافي في "استوديو Android"
المكوّن الإضافي للخادم الوكيل
يتطلّب تخصيص التطبيقات باستخدام Car UI Library توفير حزمة RRO تستهدف كل تطبيق محدّد سيتم تعديله، حتى إذا كانت التخصيصات متطابقة في جميع التطبيقات. وهذا يعني أنّه يجب توفير RRO لكل تطبيق. الاطّلاع على التطبيقات التي تستخدم "مكتبة واجهة مستخدم السيارة"
المكوّن الإضافي لوكيل مكتبة Car UI هو مثال على مكتبة مشتركة للمكوّنات الإضافية تنقل عمليات تنفيذ المكوّنات إلى الإصدار الثابت من مكتبة Car UI. يمكن استهداف هذا المكوّن الإضافي باستخدام حزمة RRO، والتي يمكن استخدامها كنقطة تخصيص واحدة للتطبيقات التي تستخدم مكتبة Car UI بدون الحاجة إلى تنفيذ مكوّن إضافي وظيفي. لمزيد من المعلومات حول عمليات استبدال الموارد في وقت التشغيل، راجِع مقالة تغيير قيمة موارد التطبيق في وقت التشغيل.
المكوّن الإضافي للوكيل هو مجرد مثال ونقطة بداية لتنفيذ التخصيص باستخدام مكوّن إضافي. للتخصيص بما يتجاوز RRO، يمكن تنفيذ مجموعة فرعية من مكوّنات البرنامج المساعد واستخدام البرنامج المساعد للوكيل في بقية المكوّنات، أو تنفيذ جميع مكوّنات البرنامج المساعد بالكامل من البداية.
على الرغم من أنّ مكوّن الوكيل الإضافي يوفّر نقطة واحدة لتخصيص حزمة RRO للتطبيقات، سيظل على التطبيقات التي تختار عدم استخدام المكوّن الإضافي توفير حزمة RRO تستهدف التطبيق نفسه مباشرةً.
تنفيذ واجهات برمجة التطبيقات الخاصة بالمكوّن الإضافي
نقطة الدخول الرئيسية إلى المكوّن الإضافي هي فئة
com.android.car.ui.plugin.PluginVersionProviderImpl
. يجب أن تتضمّن جميع المكوّنات الإضافية فئة بهذا الاسم واسم الحزمة المحدّدَين. يجب أن تتضمّن هذه الفئة أداة إنشاء تلقائية وأن تنفّذ الواجهة PluginVersionProviderOEMV1
.
يجب أن تتوافق مكوّنات CarUi الإضافية مع التطبيقات التي تكون أقدم أو أحدث من المكوّن الإضافي. لتسهيل ذلك، يتم تحديد إصدارات جميع واجهات برمجة التطبيقات الخاصة بالإضافات من خلال إضافة V#
إلى نهاية اسم الفئة. إذا تم إصدار نسخة جديدة من "مكتبة واجهة مستخدم السيارة" تتضمّن ميزات جديدة، ستكون هذه الميزات جزءًا من الإصدار V2
من المكوّن. تبذل "مكتبة واجهة مستخدم السيارة" قصارى جهدها لجعل الميزات الجديدة تعمل في نطاق مكوّن إضافي قديم.
على سبيل المثال، من خلال تحويل نوع جديد من الأزرار في شريط الأدوات إلى MenuItems
.
ومع ذلك، لا يمكن لتطبيق يستخدم إصدارًا قديمًا من "مكتبة واجهة مستخدم السيارة" أن يتوافق مع مكوّن إضافي جديد مكتوب باستخدام واجهات برمجة تطبيقات أحدث. لحلّ هذه المشكلة، نسمح للمكوّنات الإضافية بعرض عمليات تنفيذ مختلفة استنادًا إلى إصدار واجهة برمجة التطبيقات الخاصة بمصنّع المعدات الأصلية المتوافق مع التطبيقات.
يحتوي PluginVersionProviderOEMV1
على طريقة واحدة:
Object getPluginFactory(int maxVersion, Context context, String packageName);
تعرض هذه الطريقة عنصرًا ينفّذ أعلى إصدار من
PluginFactoryOEMV#
يتيحه المكوّن الإضافي، مع الحفاظ على أن يكون أقل من أو
يساوي maxVersion
. إذا لم يتضمّن المكوّن الإضافي تنفيذًا لـ PluginFactory
بهذا القدم، قد يعرض null
، وفي هذه الحالة، سيتم استخدام التنفيذ المرتبط بشكل ثابت لمكوّنات CarUi.
للحفاظ على التوافق مع الإصدارات القديمة من التطبيقات التي تم تجميعها باستخدام إصدارات قديمة من مكتبة Car Ui الثابتة، يُنصح بتوفير maxVersion
بقيم 2 و5 وأعلى من داخل عملية تنفيذ الفئة PluginVersionProvider
في المكوّن الإضافي. الإصدارات 1 و3 و4 غير متوافقة. لمزيد من المعلومات، يُرجى الاطّلاع على PluginVersionProviderImpl
.
PluginFactory
هي الواجهة التي تنشئ جميع مكوّنات CarUi الأخرى. ويحدّد أيضًا إصدار واجهاته الذي يجب استخدامه. إذا لم يكن المكوّن الإضافي يهدف إلى تنفيذ أي من هذه المكوّنات، يمكنه عرض null
في دالة الإنشاء (باستثناء شريط الأدوات الذي يتضمّن دالة customizesBaseLayout()
منفصلة).
يفرض pluginFactory
قيودًا على إصدارات مكوّنات CarUi التي يمكن استخدامها معًا. على سبيل المثال، لن يكون هناك أبدًا pluginFactory
يمكنه إنشاء الإصدار 100 من Toolbar
والإصدار 1 من RecyclerView
، لأنّه لن يكون هناك ضمان كبير بأنّ مجموعة متنوعة من إصدارات المكوّنات ستعمل معًا. لاستخدام الإصدار 100 من شريط الأدوات، على المطوّرين توفير تنفيذ لإصدار من pluginFactory
ينشئ الإصدار 100 من شريط الأدوات، ما يؤدي إلى الحدّ من الخيارات المتاحة في إصدارات المكوّنات الأخرى التي يمكن إنشاؤها. قد لا تتساوى إصدارات المكوّنات الأخرى، على سبيل المثال، يمكن أن ينشئ pluginFactoryOEMV100
ToolbarControllerOEMV100
وRecyclerViewOEMV70
.
شريط الأدوات
التصميم الأساسي
يرتبط شريط الأدوات و "التصميم الأساسي" ارتباطًا وثيقًا، ولهذا السبب، يُطلق على الدالة التي تنشئ شريط الأدوات اسم installBaseLayoutAround
. التصميم الأساسي هو مفهوم يتيح وضع شريط الأدوات في أي مكان حول محتوى التطبيق، ما يتيح عرض شريط أدوات في أعلى التطبيق أو أسفله، أو بشكل عمودي على الجانبين، أو حتى شريط أدوات دائري يحيط بالتطبيق بأكمله. ويتم ذلك من خلال تمرير طريقة عرض إلى installBaseLayoutAround
لكي يلتف شريط الأدوات/التصميم الأساسي حولها.
يجب أن تأخذ الإضافة طريقة العرض المقدَّمة، وتفصلها عن العنصر الرئيسي، وتوسّع تصميم الإضافة الخاص في الفهرس نفسه للعنصر الرئيسي وباستخدام LayoutParams
نفسه الذي تم فصل طريقة العرض به، ثم تعيد ربط طريقة العرض في مكان ما داخل التصميم الذي تم توسيعه. سيحتوي التصميم المضخّم على شريط الأدوات، إذا طلبه التطبيق.
يمكن للتطبيق طلب تصميم أساسي بدون شريط أدوات. إذا كان الأمر كذلك، من المفترض أن تعرض الدالة installBaseLayoutAround
قيمة فارغة. بالنسبة إلى معظم المكوّنات الإضافية، هذا كل ما يجب فعله، ولكن إذا أراد مؤلف المكوّن الإضافي تطبيق، على سبيل المثال، زخرفة حول حافة التطبيق، يمكن إجراء ذلك باستخدام تخطيط أساسي. تكون هذه
الزخارف مفيدة بشكل خاص للأجهزة التي تحتوي على شاشات غير مستطيلة، إذ
يمكنها نقل التطبيق إلى مساحة مستطيلة وإضافة انتقالات سلسة إلى
المساحة غير المستطيلة.
يتم أيضًا تمرير Consumer<InsetsOEMV1>
إلى installBaseLayoutAround
. يمكن استخدام هذا المستهلك لإبلاغ التطبيق بأنّ المكوّن الإضافي يغطّي جزئيًا محتوى التطبيق (باستخدام شريط الأدوات أو غير ذلك). سيعرف التطبيق بعد ذلك أنّه يجب مواصلة الرسم في هذه المساحة، ولكن مع إبقاء أي مكوّنات مهمة يمكن للمستخدم التفاعل معها خارجها. يتم استخدام هذا التأثير في التصميم المرجعي لجعل شريط الأدوات شبه شفاف، وللسماح للقوائم بالتمرير أسفله. في حال عدم تنفيذ هذه الميزة، سيبقى العنصر الأول في القائمة عالقًا أسفل شريط الأدوات ولن يكون قابلاً للنقر. إذا لم تكن هناك حاجة إلى هذا التأثير، يمكن للمكوّن الإضافي تجاهل
Consumer.
الشكل 2. تمرير المحتوى أسفل شريط الأدوات
من منظور التطبيق، عندما يرسل المكوّن الإضافي هوامش إضافية جديدة، سيتلقّى هذه الهوامش من أي أنشطة أو أجزاء تنفّذ InsetsChangedListener
. إذا لم تنفّذ إحدى الأنشطة أو إحدى التجزئات InsetsChangedListener
، ستتعامل مكتبة Car Ui مع الحواف الداخلية تلقائيًا من خلال تطبيق الحواف الداخلية كحاشية على Activity
أو FragmentActivity
التي تحتوي على التجزئة. لا تطبّق المكتبة عمليات الإزاحة تلقائيًا على الأجزاء. في ما يلي نموذج مقتطف من عملية تنفيذ تطبّق الحواف الداخلية كمسافة بادئة على RecyclerView
في التطبيق:
public class MainActivity extends Activity implements InsetsChangedListener {
@Override
public void onCarUiInsetsChanged(Insets insets) {
CarUiRecyclerView rv = requireViewById(R.id.recyclerview);
rv.setPadding(insets.getLeft(), insets.getTop(),
insets.getRight(), insets.getBottom());
}
}
أخيرًا، يتم تزويد المكوّن الإضافي بتلميح fullscreen
، ويُستخدم هذا التلميح للإشارة إلى ما إذا كان العرض الذي يجب تضمينه يشغل التطبيق بأكمله أو جزءًا صغيرًا منه فقط.
يمكن استخدام ذلك لتجنُّب تطبيق بعض الزخارف على طول الحافة التي لا يكون لها معنى إلا إذا ظهرت على طول حافة الشاشة بأكملها. من الأمثلة على التطبيقات التي تستخدم تخطيطات أساسية غير ملء الشاشة تطبيق "الإعدادات"، حيث يحتوي كل جزء من أجزاء التخطيط ذي اللوحتين على شريط أدوات خاص به.
بما أنّه من المتوقّع أن تعرض الدالة installBaseLayoutAround
القيمة null عندما تكون قيمة toolbarEnabled
هي false
، يجب أن تعرض الدالة false
من customizesBaseLayout
لكي يشير المكوّن الإضافي إلى أنّه لا يريد تخصيص التصميم الأساسي.
يجب أن يحتوي التصميم الأساسي على FocusParkingView
وFocusArea
لتوفير الدعم الكامل لعناصر التحكّم الدوّارة. يمكن حذف هذه طرق العرض على الأجهزة التي لا تتوافق مع عناصر التحكّم الدوّارة. يتم تنفيذ FocusParkingView/FocusAreas
في مكتبة CarUi الثابتة، لذا يتم استخدام setRotaryFactories
لتوفير المصانع لإنشاء طرق العرض من السياقات.
يجب أن تكون السياقات المستخدَمة لإنشاء طرق عرض "التركيز" هي سياق المصدر، وليس سياق المكوّن الإضافي. يجب أن يكون FocusParkingView
أقرب ما يمكن إلى العرض الأول في الشجرة، لأنّه العنصر الذي يتم التركيز عليه عندما لا يكون هناك تركيز مرئي للمستخدم. يجب أن يغلّف FocusArea
شريط الأدوات في التنسيق الأساسي للإشارة إلى أنّه منطقة تنبيه دوّارة. إذا لم يتم توفير FocusArea
، لن يتمكّن المستخدم من الانتقال إلى أي أزرار في شريط الأدوات باستخدام أداة التحكم الدوّارة.
وحدة التحكّم في شريط الأدوات
يجب أن يكون تنفيذ ToolbarController
الفعلي أسهل بكثير من تنفيذ التصميم الأساسي. مهمته هي تلقّي المعلومات التي يتم تمريرها إلى أدوات الضبط وعرضها في التصميم الأساسي. يمكنك الاطّلاع على Javadoc للحصول على معلومات حول معظم الطرق. في ما يلي بعض الطرق الأكثر تعقيدًا.
يُستخدَم getImeSearchInterface
لعرض نتائج البحث في نافذة IME (لوحة المفاتيح). ويمكن أن يكون ذلك مفيدًا لعرض نتائج البحث أو تحريكها بجانب لوحة المفاتيح، مثلاً إذا كانت لوحة المفاتيح تشغل نصف الشاشة فقط. يتم تنفيذ معظم الوظائف في مكتبة CarUi الثابتة، ولا توفّر واجهة البحث في المكوّن الإضافي سوى طرق للمكتبة الثابتة للحصول على عمليات معاودة الاتصال TextView
وonPrivateIMECommand
. لإتاحة ذلك، يجب أن تستخدم الإضافة فئة فرعية من TextView
تتجاوز onPrivateIMECommand
وتمرّر المكالمة إلى أداة الاستماع المتوفّرة باعتبارها TextView
الخاصة بشريط البحث.
تعرض setMenuItems
ببساطة عناصر القائمة على الشاشة، ولكن سيتم استدعاؤها بشكل متكرر. بما أنّ واجهة برمجة التطبيقات الخاصة بالمكوّن الإضافي لعناصر القائمة غير قابلة للتغيير، سيتم إجراء عملية استدعاء setMenuItems
جديدة بالكامل عند تغيير أي عنصر من عناصر القائمة. قد يحدث ذلك لأسباب بسيطة، مثل نقر المستخدم على عنصر قائمة تبديل، وأدى هذا النقر إلى تبديل حالة عنصر القائمة. لذا، ننصحك بحساب الفرق بين قائمتَي MenuItem القديمة والجديدة، وتعديل طرق العرض التي تغيّرت فقط، وذلك لأسباب تتعلّق بالأداء والرسوم المتحركة. توفّر عناصر القائمة
حقل key
يمكن أن يساعد في ذلك، لأنّ المفتاح يجب أن يكون هو نفسه
في مختلف طلبات setMenuItems
لعنصر القائمة نفسه.
AppStyledView
AppStyledView
هي حاوية لعرض غير مخصّص على الإطلاق. ويمكن استخدامها لتوفير حد حول طريقة العرض هذه، ما يجعلها تبرز عن بقية التطبيق، ويشير إلى المستخدم بأنّ هذا نوع مختلف من الواجهات. يتم توفير طريقة العرض التي يغلّفها AppStyledView في
setContent
. يمكن أن يحتوي AppStyledView
أيضًا على زر رجوع أو إغلاق حسب طلب التطبيق.
لا يدرج AppStyledView
طرق العرض الخاصة به على الفور في بنية طرق العرض
مثلما يفعل installBaseLayoutAround
، بل يعرض طريقة العرض الخاصة به فقط في
المكتبة الثابتة من خلال getView
، التي تتولى عملية الإدراج. يمكن أيضًا التحكّم في موضع AppStyledView
وحجمه من خلال تنفيذ getDialogWindowLayoutParam
.
السياقات
يجب أن يكون المكوّن الإضافي حذرًا عند استخدام السياقات، لأنّ هناك سياقات للمكوّن الإضافي وسياقات "المصدر". يتم تقديم سياق المكوّن الإضافي كوسيطة إلى getPluginFactory
، وهو السياق الوحيد الذي يتضمّن موارد المكوّن الإضافي. وهذا يعني أنّه السياق الوحيد الذي يمكن استخدامه لتوسيع التنسيقات في المكوّن الإضافي.
ومع ذلك، قد لا يتضمّن سياق المكوّن الإضافي الإعدادات الصحيحة. للحصول على الإعدادات الصحيحة، نقدّم سياقات المصدر في الطرق التي تنشئ المكوّنات. يكون سياق المصدر عادةً نشاطًا، ولكن في بعض الحالات، قد يكون أيضًا خدمة أو أحد مكونات Android الأخرى. لاستخدام الإعداد من سياق المصدر مع الموارد من سياق المكوّن الإضافي، يجب إنشاء سياق جديد باستخدام createConfigurationContext
. في حال عدم استخدام الإعدادات الصحيحة، سيحدث انتهاك لوضع Android الصارم، وقد لا تكون للأبعاد المضخّمة القيم الصحيحة.
Context layoutInflationContext = pluginContext.createConfigurationContext(
sourceContext.getResources().getConfiguration());
تغييرات الوضع
يمكن لبعض المكوّنات الإضافية أن تتوافق مع أوضاع متعددة لمكوّناتها، مثل وضع الرياضة والوضع الاقتصادي اللذين يختلفان بصريًا. لا تتوفّر ميزة مدمجة لتنفيذ هذه الوظيفة في CarUi، ولكن لا يوجد ما يمنع المكوّن الإضافي من تنفيذها داخليًا بالكامل. يمكن للمكوّن الإضافي مراقبة أي شروط يريدها لمعرفة وقت التبديل بين الأوضاع، مثل الاستماع إلى عمليات البث. لا يمكن للمكوّن الإضافي بدء تغيير في الإعدادات لتغيير الأوضاع، ولكن لا يُنصح بالاعتماد على تغييرات الإعدادات على أي حال، لأنّ تعديل مظهر كل مكوّن يدويًا يكون أكثر سلاسة بالنسبة إلى المستخدم ويتيح أيضًا إجراء انتقالات لا يمكن إجراؤها باستخدام تغييرات الإعدادات.
Jetpack Compose
يمكن تنفيذ الإضافات باستخدام Jetpack Compose، ولكن هذه الميزة في مستوى الإصدار الأوّلي ويجب عدم اعتبارها إصدارًا ثابتًا.
يمكن أن تستخدم المكوّنات الإضافية
ComposeView
لإنشاء مساحة عرض متوافقة مع Compose. سيكون ComposeView
هو القيمة التي يتم عرضها في التطبيق من خلال طريقة getView
في المكوّنات.
تتمثّل إحدى المشاكل الرئيسية في استخدام ComposeView
في أنّه يضبط العلامات على العرض الجذر في التصميم من أجل تخزين المتغيّرات العامة التي تتم مشاركتها بين عناصر ComposeView المختلفة في التسلسل الهرمي. بما أنّ معرّفات موارد المكوّن الإضافي لا يتم تحديد نطاقها بشكل منفصل عن التطبيق، قد يؤدي ذلك إلى حدوث تعارضات عندما يضبط كل من التطبيق والمكوّن الإضافي العلامات على طريقة العرض نفسها. يتم توفير
ComposeViewWithLifecycle
مخصّص ينقل هذه المتغيرات العامة إلى
ComposeView
أدناه. مرة أخرى، لا ينبغي اعتبار هذا الإصدار ثابتًا.
ComposeViewWithLifecycle
:
class ComposeViewWithLifecycle @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr),
LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {
private val lifeCycle = LifecycleRegistry(this)
private val modelStore = ViewModelStore()
private val savedStateRegistryController = SavedStateRegistryController.create(this)
private var composeView: ComposeView? = null
private var content = @Composable {}
init {
ViewTreeLifecycleOwner.set(this, this)
ViewTreeViewModelStoreOwner.set(this, this)
ViewTreeSavedStateRegistryOwner.set(this, this)
compositionContext = createCompositionContext()
}
fun setContent(content: @Composable () -> Unit) {
this.content = content
composeView?.setContent(content)
}
override fun getLifecycle(): Lifecycle {
return lifeCycle
}
override fun getViewModelStore(): ViewModelStore {
return modelStore
}
override fun getSavedStateRegistry(): SavedStateRegistry {
return savedStateRegistryController.savedStateRegistry
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
savedStateRegistryController.performRestore(Bundle())
lifeCycle.currentState = Lifecycle.State.RESUMED
composeView = ComposeView(context)
composeView?.setContent(content)
addView(composeView, LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
lifeCycle.currentState = Lifecycle.State.DESTROYED
modelStore.clear()
removeAllViews()
composeView = null
}
// Exact copy of View.createCompositionContext() in androidx's WindowRecomposer.android.kt
private fun createCompositionContext(): CompositionContext {
val currentThreadContext = AndroidUiDispatcher.CurrentThread
val pausableClock = currentThreadContext[MonotonicFrameClock]?.let {
PausableMonotonicFrameClock(it).apply { pause() }
}
val contextWithClock = currentThreadContext + (pausableClock ?: EmptyCoroutineContext)
val recomposer = Recomposer(contextWithClock)
val runRecomposeScope = CoroutineScope(contextWithClock)
val viewTreeLifecycleOwner = checkNotNull(ViewTreeLifecycleOwner.get(this)) {
"ViewTreeLifecycleOwner not found from $this"
}
viewTreeLifecycleOwner.lifecycle.addObserver(
LifecycleEventObserver { _, event ->
@Suppress("NON_EXHAUSTIVE_WHEN")
when (event) {
Lifecycle.Event.ON_CREATE ->
// Undispatched launch since we've configured this scope
// to be on the UI thread
runRecomposeScope.launch(start = CoroutineStart.UNDISPATCHED) {
recomposer.runRecomposeAndApplyChanges()
}
Lifecycle.Event.ON_START -> pausableClock?.resume()
Lifecycle.Event.ON_STOP -> pausableClock?.pause()
Lifecycle.Event.ON_DESTROY -> {
recomposer.cancel()
}
}
}
)
return recomposer
}
// TODO: ComposeViewWithLifecycle should handle saving state and other lifecycle things
// override fun onSaveInstanceState(): Parcelable? {
// val superState = super.onSaveInstanceState()
// val bundle = Bundle()
// savedStateRegistryController.performSave(bundle)
// }
}