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

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

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

إعداد المكوّن الإضافي في &quot;استوديو Android&quot; الشكل 1. إعدادات المكوّن الإضافي في "استوديو Android"

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

يتطلب تخصيص التطبيقات التي تستخدم مكتبة واجهة مستخدم السيارة عملية تراكب موارد في وقت التشغيل تستهدف كل تطبيق معيّن سيتم تعديله، حتى عندما تكون التخصيصات متطابقة بين التطبيقات. وهذا يعني أنّه يجب توفير عملية تراكب موارد في وقت التشغيل لكل تطبيق. اطّلِع على التطبيقات التي تستخدم مكتبة واجهة مستخدم السيارة.

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

المكوّن الإضافي للخادم الوكيل هو مجرد مثال ونقطة بداية لإجراء التخصيص باستخدام مكوّن إضافي. لإجراء تخصيصات تتجاوز عمليات تراكب الموارد في وقت التشغيل، يمكن للمستخدم تنفيذ مجموعة فرعية من مكوّنات المكوّن الإضافي واستخدام المكوّن الإضافي للخادم الوكيل لبقية المكوّنات، أو تنفيذ جميع مكوّنات المكوّن الإضافي بالكامل من البداية.

على الرغم من أنّ المكوّن الإضافي للخادم الوكيل يوفّر نقطة تخصيص واحدة لعمليات تراكب الموارد في وقت التشغيل للتطبيقات، ستظل التطبيقات التي تختار عدم استخدام المكوّن الإضافي تتطلب عملية تراكب موارد في وقت التشغيل تستهدف التطبيق نفسه مباشرةً.

تنفيذ واجهات برمجة التطبيقات الخاصة بالمكوّن الإضافي

نقطة الدخول الرئيسية إلى المكوّن الإضافي هي الفئة com.android.car.ui.plugin.PluginVersionProviderImpl. يجب أن تتضمّن جميع المكوّنات الإضافية فئة تحمل هذا الاسم واسم الحزمة بالضبط. يجب أن تحتوي هذه الفئة على دالة إنشاء تلقائية وأن تنفّذ واجهة PluginVersionProviderOEMV1.

يجب أن تعمل المكوّنات الإضافية لواجهة مستخدم السيارة مع التطبيقات الأقدم أو الأحدث من المكوّن الإضافي. لتسهيل ذلك، يتم تحديد إصدارات جميع واجهات برمجة التطبيقات الخاصة بالمكوّنات الإضافية باستخدام V# في نهاية اسم الفئة. إذا تم إصدار إصدار جديد من مكتبة واجهة مستخدم السيارة يتضمّن ميزات جديدة، ستكون هذه الميزات جزءًا من الإصدار V2 من المكوّن. تبذل مكتبة واجهة مستخدم السيارة قصارى جهدها لجعل الميزات الجديدة تعمل ضِمن نطاق مكوّن إضافي أقدم. على سبيل المثال، من خلال تحويل نوع جديد من الأزرار في شريط الأدوات إلى MenuItems.

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

تحتوي واجهة PluginVersionProviderOEMV1 على طريقة واحدة:

Object getPluginFactory(int maxVersion, Context context, String packageName);

تعرض هذه الطريقة كائنًا ينفّذ أعلى إصدار من PluginFactoryOEMV# متوافق مع المكوّن الإضافي، مع الحفاظ على أن يكون أقل من maxVersion أو مساويًا له. إذا لم يكن لدى المكوّن الإضافي عملية تنفيذ لـ PluginFactory بهذا القِدم، قد يعرض null، وفي هذه الحالة، يتم استخدام عملية التنفيذ المرتبطة بشكل ثابت لمكوّنات CarUi.

للحفاظ على التوافق مع الإصدارات السابقة مع التطبيقات التي يتم تجميعها باستخدام إصدارات أقدم من مكتبة واجهة مستخدم السيارة الثابتة، يُنصح بتوفير maxVersion‏ 2 و5 والإصدارات الأحدث من ضِمن عملية تنفيذ المكوّن الإضافي لفئة PluginVersionProvider. لا تتوافق الإصدارات 1 و3 و4. لمزيد من المعلومات، اطّلِع على PluginVersionProviderImpl.

PluginFactory هي الواجهة التي تنشئ جميع مكوّنات CarUi الأخرى. تحدّد أيضًا إصدار الواجهات التي يجب استخدامها. إذا لم يكن المكوّن الإضافي يسعى إلى تنفيذ أي من هذه المكوّنات، يمكنه عرض null في دالة الإنشاء (باستثناء شريط الأدوات الذي يحتوي على دالة customizesBaseLayout() منفصلة).

تحدّد pluginFactory إصدارات مكوّنات CarUi التي يمكن استخدامها معًا. على سبيل المثال، لن يكون هناك أبدًا pluginFactory يمكنه إنشاء الإصدار 100 من Toolbar والإصدار 1 من RecyclerView، لأنّه لن يكون هناك ضمان كبير بأنّ مجموعة كبيرة من إصدارات المكوّنات ستعمل معًا. لاستخدام الإصدار 100 من شريط الأدوات، يُتوقَّع من المطوّرين توفير عملية تنفيذ لإصدار من pluginFactory ينشئ الإصدار 100 من شريط الأدوات، ما يحدّ بعد ذلك من الخيارات المتاحة لإصدارات المكوّنات الأخرى التي يمكن إنشاؤها. قد لا تكون إصدارات المكوّنات الأخرى متساوية، على سبيل المثال، يمكن أن ينشئ pluginFactoryOEMV100ToolbarControllerOEMV100 وRecyclerViewOEMV70.

شريط الأدوات

التنسيق الأساسي

يرتبط شريط الأدوات و"التنسيق الأساسي" ارتباطًا وثيقًا، لذا يُطلق على الدالة التي تنشئ شريط الأدوات اسم installBaseLayoutAround. التنسيق الأساسي هو مفهوم يسمح بوضع شريط الأدوات في أي مكان حول محتوى التطبيق، للسماح بوضع شريط أدوات في أعلى التطبيق أو أسفله أو عموديًا على طول الجانبَين أو حتى شريط أدوات دائري يحيط بالتطبيق بأكمله. ويتم ذلك من خلال تمرير طريقة عرض إلى installBaseLayoutAround لكي يلتف شريط الأدوات أو التنسيق الأساسي حولها.

يجب أن يأخذ المكوّن الإضافي طريقة العرض المقدَّمة، ويفصلها عن العنصر الرئيسي، ويوسّع التنسيق الخاص بالمكوّن الإضافي في الفهرس نفسه للعنصر الرئيسي وباستخدام LayoutParams نفسها لطريقة العرض التي تم فصلها للتو، ثم يعيد ربط طريقة العرض في مكان ما ضِمن التنسيق الذي تم توسيعه للتو. سيحتوي التنسيق الذي تم توسيعه على شريط الأدوات، إذا طلب التطبيق ذلك.

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

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

التمرير السريع للمحتوى أسفل شريط الأدوات الشكل 2. تمرير المحتوى أسفل شريط الأدوات

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

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

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

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

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

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

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

السياقات

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