المكونات الإضافية لواجهة مستخدم السيارة

استخدام المكوّنات الإضافية لمكتبة واجهة مستخدم السيارة لإنشاء عمليات تنفيذ كاملة للمكوّنات عمليات التخصيص في مكتبة واجهة مستخدم السيارة بدلاً من استخدام طبقات موارد وقت التشغيل (RROs). تتيح لك سجلات الموارد المنتظمة (RRO) تغيير موارد XML فقط لمكتبة واجهة مستخدم السيارة. من المكونات، والتي تحدد مدى ما يمكنك تخصيصه.

إنشاء مكوّن إضافي

المكوّن الإضافي لمكتبة واجهة مستخدم السيارة هو حزمة APK تحتوي على فئات تنفذ مجموعة من واجهات برمجة تطبيقات المكوّنات الإضافية. يمكن تجميع واجهات برمجة تطبيقات المكوّنات الإضافية في المكون الإضافي كمكتبة ثابتة.

الاطّلاع على أمثلة في "سونغ" و"غرادل":

سونغ

ضع في الاعتبار هذا المثال لـ Sung:

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",
}

قاعدة مخروطية

انظر هذا الملف بتنسيق 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" يجعل المكوّن الإضافي قابلاً للاكتشاف إلى مكتبة واجهة مستخدم السيارة. يجب تصدير الموفر حتى يمكن الاستعلام عنه على وقت التشغيل. بالإضافة إلى ذلك، إذا تم ضبط السمة 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>
  • ضبط قاعدة إصدار android_app (Android.bp) في تطبيق Sayg باستخدام AAPT علامة shared-lib، التي تُستخدم لإنشاء مكتبة مشتركة:
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

التبعيات في المكتبات المشتركة

لكل تطبيق على النظام الذي يستخدم مكتبة واجهة مستخدم السيارة، قم بتضمين ملف 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" التي تؤدي إلى إجراء تحديثات للمكوّن الإضافي لا تدخل حيز التنفيذ. يمكن حل هذه المشكلة من خلال تحديد الخيار التثبيت دائمًا باستخدام مدير الحِزم (إيقاف نشر التحسينات على الإصدار 11 من نظام Android والإصدارات الأحدث) في تكوين تصميم المكون الإضافي.

بالإضافة إلى ذلك، عند تثبيت المكوّن الإضافي، يُبلغ "استوديو Android" عن خطأ مفاده لا يمكن العثور على نشاط رئيسي لإطلاقه. وهذا أمر متوقع، حيث لا يكون للمكون الإضافي يكون لديك أي أنشطة (باستثناء الغرض الفارغ المستخدَم لتحقيق هدف). إلى إزالة الخطأ، غيِّر خيار التشغيل إلى لا شيء في الإصدار. التكوين.

إعداد Android Studio للمكوّن الإضافي الشكل 1. إعداد Android Studio للمكوّن الإضافي

المكوّن الإضافي للخادم الوكيل

تخصيص التطبيقات التي تستخدم مكتبة "واجهة مستخدم السيارة" RRO تستهدف كل تطبيق محدد يتم تعديله، بما في ذلك عندما تكون عمليات التخصيص متطابقة بين التطبيقات. وهذا يعني أن سجل الموارد المنتظم (RRO) التطبيق. تعرَّف على التطبيقات التي تستخدم مكتبة واجهة مستخدم السيارة.

يعد المكوّن الإضافي لخادم وكيل مكتبة واجهة مستخدم السيارة مثالاً للمكوّن الإضافي المكتبة المشتركة التي تفوض عمليات تنفيذ مكوناتها إلى الخادم من مكتبة واجهة مستخدم السيارة. يمكن استهداف هذا المكون الإضافي باستخدام RRO، والذي يمكن تُستخدم كنقطة تخصيص واحدة للتطبيقات التي تستخدم مكتبة واجهة مستخدم السيارة دون الحاجة إلى تنفيذ مكون إضافي وظيفي. لمزيد من المعلومات عن RROs، راجع تغيير قيمة موارد التطبيق في وقت التشغيل.

المكوّن الإضافي للخادم الوكيل هو مثال فقط ونقطة بداية لإجراء التخصيص باستخدام أحد المكونات الإضافية. للتخصيص خارج قوائم الموارد المنتظمة (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، وفي هذه الحالة تكون السمة static- واستخدام التنفيذ المرتبط لمكونات 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 قيمة خالية. بالنسبة لمعظم المكونات الإضافية، هذا كل ما ولكن إذا رغب مؤلف المكون الإضافي في تطبيقه، مثلاً زينة حول حافة التطبيق، لا يزال من الممكن القيام بذلك باستخدام التخطيط الأساسي. هذه والزخارف مفيدة بشكل خاص للأجهزة ذات الشاشات غير المستطيلة، يمكنها دفع التطبيق إلى مساحة مستطيلة وإضافة انتقالات نظيفة إلى المساحة غير المستطيلة.

تم أيضًا تجاوز installBaseLayoutAround لـ Consumer<InsetsOEMV1>. هذا النمط يمكن استخدام المستهلك لإبلاغ التطبيق بأن المكون الإضافي جزء تغطية محتوى التطبيق (باستخدام شريط الأدوات أو غير ذلك) التطبيق سوف الاستمرار في الرسم في هذه المساحة، مع الحفاظ على أي تفاعل مهم مع المستخدم مكونًا منها. يُستخدم هذا التأثير في التصميم المرجعي لدينا، شريط الأدوات شبه شفاف، ولها قوائم يتم تمريرها تحتها. إذا كانت هذه الميزة لم يتم تنفيذه، فسيتم تعليق العنصر الأول في القائمة أسفل شريط الأدوات ولا يمكن النقر عليه إذا لم يكن هذا التأثير مطلوبًا، يمكن للمكون الإضافي تجاهل المستهلك:

تمرير المحتوى أسفل شريط الأدوات الشكل 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 قيمة خالية عند toolbarEnabled هي false، حيث يشير المكون الإضافي إلى أنها لا تريد تخصيص التخطيط الأساسي، يجب أن يعرض false من customizesBaseLayout

يجب أن يحتوي التنسيق الأساسي على FocusParkingView وFocusArea دعم عناصر التحكم الدوارة. ويمكن حذف طرق العرض هذه على الأجهزة التي لا تتوافق مع وحدة القياس الدوّارة. يتم تنفيذ FocusParkingView/FocusAreas في مكتبة CarUi ثابتة، لذلك يتم استخدام setRotaryFactories لتوفير المصانع لإنشاء طرق العرض من السياقات.

يجب أن تكون السياقات المستخدمة لإنشاء طرق عرض التركيز هي سياق المصدر، وليس لسياق المكون الإضافي. يجب أن يكون FocusParkingView هو الأقرب إلى العرض الأول. في الشجرة قدر الإمكان، لأنّ ذلك هو ما يتم التركيز عليه عندما يكون أن يكون التركيز عليها مرئيًا للمستخدم. على "FocusArea" التفاف شريط الأدوات في التصميم الأساسي للإشارة إلى أنها منطقة دفع دوار. إذا لم يكن FocusArea المقدمة، لا يتمكن المستخدم من الانتقال إلى أي أزرار في شريط الأدوات وحدة التحكّم الدوارة.

وحدة التحكم في شريط الأدوات

يجب أن تكون قيمة ToolbarController الفعلية التي تم إرجاعها أكثر وضوحًا تنفيذها أكثر من التخطيط الأساسي. وتتمثل وظيفتها في نقل المعلومات التي يتم تمريرها إلى البيانات وعرضها في التخطيط الأساسي. راجع Javadoc للحصول على معلومات عن لمعظم الطرق. وتتم مناقشة بعض الطرق الأكثر تعقيدًا أدناه.

تُستخدم getImeSearchInterface لعرض نتائج البحث في لوحة المفاتيح (IME) نافذة. يمكن الاستفادة من ذلك في عرض/تحريك نتائج البحث بجانب الزر لوحة المفاتيح، مثلاً إذا كانت لوحة المفاتيح تشغل نصف الشاشة فقط. معظم تنفيذ الوظيفة في مكتبة CarUi الثابتة، يفحص نظام في المكون الإضافي طرق للمكتبة الثابتة TextView وonPrivateIMECommand طلبات معاودة الاتصال. ولدعم ذلك، يمكن للمكون الإضافي يجب استخدام فئة فرعية TextView تلغي السمة onPrivateIMECommand وتمرر المكالمة الموجّهة إلى المستمع المقدَّم على أنّها TextView في شريط البحث.

يعرض "setMenuItems" عناصر MenuItems على الشاشة، ولكن سيتم تسميتها في كثير من الأحيان بشكل مفاجئ. ونظرًا لأن واجهة برمجة التطبيقات للمكوّن الإضافي لـ MenuItems غير قابلة للتغيير، عندما تم تغيير MenuItem، وبالتالي سيتم إجراء مكالمة setMenuItems جديدة بالكامل. يمكن أن يحدث لشيء تافه، مثل نقر المستخدم على زر "التبديل" في ListItem، وأن النقر فوق ما تسبب في تبديل المفتاح. لأسباب تتعلق بالأداء والرسوم المتحركة، لذا ننصح بحساب الفرق بين البيانات القديمة والجديدة قائمة عناصر القائمة، وتحديث طرق العرض التي تغيرت بالفعل فقط. عناصر قائمة الطعام توفير حقل key يمكنه المساعدة في ذلك، حيث يجب أن يكون المفتاح مماثلاً في مكالمات مختلفة إلى "setMenuItems" لعنصر MenuItem نفسه.

عرض نمط التطبيق

AppStyledView هي حاوية لملف شخصي غير مخصّص على الإطلاق. أُنشأها جون هنتر، الذي كان متخصصًا يمكن استخدامها لتوفير حدود حول هذا العرض تُبرز عن باقي التطبيق، وتوضح للمستخدم أن هذا نوع مختلف من من واجهة pyplot. يتم توفير العرض الذي يلتفه 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 لإنشاء مساحة عرض تتوفّر فيها ميزة "الكتابة" سيكون 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)
//  }
}