ปลั๊กอิน UI ของรถ

ใช้ปลั๊กอินไลบรารี UI ของรถเพื่อสร้างการใช้งานการปรับแต่งคอมโพเนนต์ที่สมบูรณ์ในไลบรารี UI ของรถแทนการใช้การวางซ้อนทรัพยากรรันไทม์ (RRO) RRO ช่วยให้คุณเปลี่ยนได้เฉพาะทรัพยากร XML ของคอมโพเนนต์ไลบรารี UI ของรถ ซึ่งจะจำกัดขอบเขตที่คุณปรับแต่งได้

สร้างปลั๊กอิน

ปลั๊กอินไลบรารี UI ของรถคือ APK ที่มีคลาสที่ใช้ชุด Plugin API Plugin API สามารถคอมไพล์เป็นปลั๊กอินในรูปแบบไลบรารีแบบคงที่

ดูตัวอย่างใน 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')

ปลั๊กอินต้องมีผู้ให้บริการเนื้อหาที่ประกาศไว้ในไฟล์ Manifest ซึ่งมีแอตทริบิวต์ต่อไปนี้

  android:authorities="com.android.car.ui.plugin"
  android:enabled="true"
  android:exported="true"

android:authorities="com.android.car.ui.plugin" ทำให้ค้นพบปลั๊กอินได้สำหรับไลบรารี UI ของรถ คุณต้องส่งออกผู้ให้บริการเพื่อให้ระบบค้นหาได้เมื่อรันไทม์ นอกจากนี้ หากตั้งค่าแอตทริบิวต์ enabled เป็น false ระบบจะใช้การติดตั้งใช้งานเริ่มต้นแทนการติดตั้งใช้งานปลั๊กอิน คลาสผู้ให้บริการเนื้อหาไม่จำเป็นต้องมี ในกรณีนี้ อย่าลืมเพิ่ม tools:ignore="MissingClass" ลงในคําจํากัดความของผู้ให้บริการ ดูตัวอย่างรายการไฟล์ Manifest ด้านล่าง

    <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 จะคอมไพล์เป็น APK แบบสแตนด์อโลนที่แอปอื่นๆ อ้างอิงเมื่อรันไทม์ ซึ่งต่างจากไลบรารีแบบคงที่ของ Android ที่คอมไพล์ลงในแอปโดยตรง

ระบบจะเพิ่มคลาสของปลั๊กอินที่ใช้เป็นไลบรารีที่แชร์ของ Android ลงในคลาสโหลดที่แชร์ระหว่างแอปโดยอัตโนมัติ เมื่อแอปที่ใช้ไลบรารี UI ของรถระบุข้อกําหนดในรันไทม์ในไลบรารีที่แชร์ของปลั๊กอิน คลาสโหลดเดอร์ของแอปจะเข้าถึงคลาสของไลบรารีที่แชร์ของปลั๊กอินได้ ปลั๊กอินที่ใช้เป็นแอป Android ปกติ (ไม่ใช่ไลบรารีที่ใช้ร่วมกัน) อาจส่งผลเสียต่อเวลาเริ่มต้นแอปแบบเย็น

ติดตั้งใช้งานและสร้างคลังที่ใช้ร่วมกัน

การพัฒนาด้วยไลบรารีที่แชร์ของ Android นั้นคล้ายกับการพัฒนาแอป Android ทั่วไป แต่มีข้อแตกต่างที่สำคัญบางอย่าง

  • ใช้แท็ก library ใต้แท็ก application ที่มีชื่อแพ็กเกจปลั๊กอินในไฟล์ Manifest ของแอปปลั๊กอิน
    <application>
        <library android:name="com.chassis.car.ui.plugin" />
        ...
    </application>
  • กำหนดค่าandroid_appกฎการสร้าง (Android.bp) ของ Soong ด้วย Flag shared-lib ของ AAPT ซึ่งใช้สร้างไลบรารีที่ใช้ร่วมกัน โดยทำดังนี้
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

Dependency ของไลบรารีที่ใช้ร่วมกัน

สําหรับแอปแต่ละแอปในระบบที่ใช้ไลบรารี UI ของรถ ให้ใส่แท็ก uses-library ในไฟล์ Manifest ของแอปใต้แท็ก application พร้อมชื่อแพ็กเกจปลั๊กอิน ดังนี้

<manifest>
  <application
      android:name=".MyApp"
      ...>
    <uses-library android:name="com.chassis.car.ui.plugin" android:required="false"/>
    ...
  </application>
</manifest>

ติดตั้งปลั๊กอิน

ต้องติดตั้งปลั๊กอินล่วงหน้าในพาร์ติชันระบบโดยใส่ข้อบังคับใน PRODUCT_PACKAGES แพ็กเกจที่ติดตั้งไว้ล่วงหน้าจะอัปเดตได้เช่นเดียวกับแอปอื่นๆ ที่ติดตั้งไว้

หากคุณอัปเดตปลั๊กอินที่มีอยู่ในระบบ แอปที่ใช้ปลั๊กอินดังกล่าวจะปิดโดยอัตโนมัติ เมื่อผู้ใช้เปิดไฟล์อีกครั้ง ไฟล์จะมีการเปลี่ยนแปลงที่อัปเดตแล้ว หากแอปไม่ได้ทำงานอยู่ เมื่อเริ่มแอปครั้งถัดไป จะมีปลั๊กอินที่อัปเดตแล้ว

เมื่อติดตั้งปลั๊กอินด้วย Android Studio คุณควรพิจารณาเพิ่มเติมบางอย่าง ขณะเขียนบทความนี้ กระบวนการติดตั้งแอป Android Studio มีข้อบกพร่องที่ทำให้การอัปเดตปลั๊กอินไม่มีผล ปัญหานี้แก้ไขได้โดยเลือกตัวเลือกติดตั้งด้วยเครื่องมือจัดการแพ็กเกจเสมอ (ปิดใช้การเพิ่มประสิทธิภาพการติดตั้งใน Android 11 ขึ้นไป) ในการกำหนดค่าบิลด์ของปลั๊กอิน

นอกจากนี้ เมื่อติดตั้งปลั๊กอิน Android Studio จะรายงานข้อผิดพลาดว่าไม่พบกิจกรรมหลักที่จะเปิด กรณีนี้เกิดขึ้นได้เนื่องจากปลั๊กอินไม่มีกิจกรรมใดๆ (ยกเว้น Intent ว่างที่ใช้เพื่อแก้ไข Intent) หากต้องการกำจัดข้อผิดพลาด ให้เปลี่ยนตัวเลือกเปิดเป็นไม่มีในการกำหนดค่าบิลด์

การกําหนดค่า Android Studio ของปลั๊กอิน รูปที่ 1 การกําหนดค่า Android Studio ของปลั๊กอิน

ปลั๊กอินพร็อกซี

การปรับแต่งแอปที่ใช้ไลบรารี UI ของรถยนต์ต้องใช้ RRO ที่กําหนดเป้าหมายแอปที่เฉพาะเจาะจงแต่ละแอปที่จะแก้ไข รวมถึงกรณีที่การปรับแต่งเหมือนกันในทุกแอป ซึ่งหมายความว่าต้องมี RRO ต่อแอป ดูว่าแอปใดใช้ไลบรารี UI ของรถ

ปลั๊กอินพร็อกซีของไลบรารี UI ของรถเป็นตัวอย่างไลบรารีที่ใช้ร่วมกันของปลั๊กอินที่มอบหมายการติดตั้งใช้งานคอมโพเนนต์ให้กับไลบรารี UI ของรถเวอร์ชันแบบคงที่ ปลั๊กอินนี้สามารถกําหนดเป้าหมายด้วย RRO ซึ่งสามารถใช้เป็นจุดเดียวในการปรับแต่งสําหรับแอปที่ใช้ไลบรารี UI ของรถได้โดยไม่ต้องใช้ปลั๊กอินที่ทํางาน ดูข้อมูลเพิ่มเติมเกี่ยวกับ RRO ได้ที่เปลี่ยนค่าของทรัพยากรของแอปที่รันไทม์

ปลั๊กอินพร็อกซีเป็นเพียงตัวอย่างและจุดเริ่มต้นในการปรับแต่งโดยใช้ปลั๊กอิน สําหรับการปรับแต่งนอกเหนือจาก RRO ผู้ใช้สามารถใช้คอมโพเนนต์ของปลั๊กอินชุดย่อยและใช้ปลั๊กอินพร็อกซีสําหรับส่วนที่เหลือ หรือติดตั้งคอมโพเนนต์ของปลั๊กอินทั้งหมดตั้งแต่ต้น

แม้ว่าปลั๊กอินพร็อกซีจะให้บริการปรับแต่ง RRO สำหรับแอปแบบจุดเดียว แต่แอปที่เลือกไม่ใช้ปลั๊กอินจะยังคงต้องใช้ RRO ที่กําหนดเป้าหมายแอปโดยตรง

ใช้ API ของปลั๊กอิน

จุดแรกเข้าหลักของปลั๊กอินคือคลาส com.android.car.ui.plugin.PluginVersionProviderImpl ปลั๊กอินทั้งหมดต้องมีคลาสที่มีชื่อและชื่อแพ็กเกจนี้ คลาสนี้ต้องมีเครื่องมือสร้างเริ่มต้นและติดตั้งใช้งานอินเทอร์เฟซ PluginVersionProviderOEMV1

ปลั๊กอิน CarUi ต้องทำงานร่วมกับแอปที่เก่ากว่าหรือใหม่กว่าปลั๊กอิน เพื่อช่วยอำนวยความสะดวกในเรื่องนี้ API ของปลั๊กอินทั้งหมดจะมีเวอร์ชันที่มี V# ต่อท้ายชื่อคลาส หากมีการเผยแพร่ไลบรารี UI ของรถเวอร์ชันใหม่ที่มีฟีเจอร์ใหม่ ฟีเจอร์เหล่านั้นจะเป็นส่วนหนึ่งของคอมโพเนนต์เวอร์ชัน V2 ไลบรารี UI ของรถจะพยายามอย่างเต็มที่เพื่อให้ฟีเจอร์ใหม่ทำงานได้ภายในขอบเขตของคอมโพเนนต์ปลั๊กอินเวอร์ชันเก่า เช่น แปลงปุ่มประเภทใหม่ในแถบเครื่องมือเป็น MenuItems

อย่างไรก็ตาม แอปที่มีไลบรารี UI ของรถเวอร์ชันเก่าจะปรับใช้กับปลั๊กอินใหม่ที่เขียนขึ้นสำหรับ API เวอร์ชันใหม่ไม่ได้ เราจึงอนุญาตให้ปลั๊กอินแสดงผลการใช้งานที่แตกต่างกันตามเวอร์ชัน OEM API ที่แอปรองรับเพื่อแก้ปัญหานี้

PluginVersionProviderOEMV1 มีเมธอดเดียวดังนี้

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

เมธอดนี้จะแสดงผลออบเจ็กต์ที่ใช้ PluginFactoryOEMV# เวอร์ชันสูงสุดที่ปลั๊กอินรองรับ ขณะที่ยังคงน้อยกว่าหรือเท่ากับ maxVersion หากปลั๊กอินไม่มีการใช้งาน PluginFactory ที่เก่าขนาดนั้น ระบบอาจแสดงผลเป็น null ซึ่งในกรณีนี้ ระบบจะใช้การใช้งานคอมโพเนนต์ CarUi ที่ลิงก์แบบคงที่

หากต้องการคงความเข้ากันได้แบบย้อนหลังกับแอปที่คอมไพล์กับไลบรารี UI ของรถยนต์แบบคงที่เวอร์ชันเก่า เราขอแนะนำให้รองรับ maxVersion เวอร์ชัน 2, 5 และเวอร์ชันที่ใหม่กว่าจากภายในการใช้งานคลาส PluginVersionProvider ของปลั๊กอิน โดยระบบไม่รองรับเวอร์ชัน 1, 3 และ 4 ดูข้อมูลเพิ่มเติมได้ที่ PluginVersionProviderImpl

PluginFactory คืออินเทอร์เฟซที่สร้างคอมโพเนนต์ CarUi อื่นๆ ทั้งหมด รวมถึงกำหนดเวอร์ชันของอินเทอร์เฟซที่ควรใช้ด้วย หากปลั๊กอินไม่ได้พยายามใช้คอมโพเนนต์เหล่านี้เลย ก็อาจแสดงผล null ในฟังก์ชันการสร้าง (ยกเว้นแถบเครื่องมือซึ่งมีฟังก์ชัน customizesBaseLayout() แยกต่างหาก)

pluginFactory จะจํากัดเวอร์ชันของคอมโพเนนต์ CarUi ที่ใช้ร่วมกันได้ ตัวอย่างเช่น จะไม่มี pluginFactory ที่สร้าง Toolbar เวอร์ชัน 100 และ RecyclerView เวอร์ชัน 1 ได้ เนื่องจากเราไม่สามารถรับประกันได้ว่าคอมโพเนนต์เวอร์ชันต่างๆ จะทำงานร่วมกันได้ หากต้องการใช้แถบเครื่องมือเวอร์ชัน 100 นักพัฒนาแอปจะต้องระบุการใช้งาน pluginFactory เวอร์ชันที่สร้างแถบเครื่องมือเวอร์ชัน 100 ซึ่งจะจำกัดตัวเลือกในเวอร์ชันของคอมโพเนนต์อื่นๆ ที่สร้างได้ เวอร์ชันของคอมโพเนนต์อื่นๆ อาจไม่เท่ากัน เช่น pluginFactoryOEMV100 อาจสร้าง ToolbarControllerOEMV100 และ RecyclerViewOEMV70

Toolbar

เลย์เอาต์พื้นฐาน

แถบเครื่องมือและ "เลย์เอาต์พื้นฐาน" มีความเกี่ยวข้องกันมาก ฟังก์ชันที่สร้างแถบเครื่องมือจึงเรียกว่า installBaseLayoutAround เลย์เอาต์พื้นฐานเป็นแนวคิดที่ช่วยให้วางแถบเครื่องมือไว้ที่ใดก็ได้รอบๆ เนื้อหาของแอป เพื่อให้มีแถบเครื่องมือที่ด้านบน/ด้านล่างของแอป แนวตั้งตามขอบ หรือแม้แต่แถบเครื่องมือแบบวงกลมที่ล้อมรอบทั้งแอป การดำเนินการนี้ทำได้โดยการส่งมุมมองไปยัง installBaseLayoutAround เพื่อให้เลย์เอาต์แถบเครื่องมือ/เลย์เอาต์พื้นฐานตัดขึ้น

ปลั๊กอินควรใช้มุมมองที่ระบุ แยกมุมมองนั้นออกจากมุมมองหลัก ขยายเลย์เอาต์ของปลั๊กอินเองในดัชนีเดียวกับของมุมมองหลักและLayoutParamsเดียวกับมุมมองที่เพิ่งแยกออก จากนั้นจึงแนบมุมมองนั้นอีกครั้งภายในเลย์เอาต์ที่เพิ่งขยาย เลย์เอาต์ที่ขยายจะมีแถบเครื่องมือหากแอปขอ

แอปสามารถขอเลย์เอาต์พื้นฐานที่ไม่มีแถบเครื่องมือได้ หากเป็นเช่นนั้น installBaseLayoutAround ควรแสดงผลเป็นค่าว่าง สำหรับปลั๊กอินส่วนใหญ่ การดำเนินการนี้ถือเป็นขั้นตอนสุดท้าย แต่หากผู้เขียนปลั๊กอินต้องการใช้การตกแต่งรอบๆ ขอบของแอป ก็ยังคงทำได้โดยใช้เลย์เอาต์พื้นฐาน การตกแต่งเหล่านี้มีประโยชน์อย่างยิ่งสำหรับอุปกรณ์ที่มีหน้าจอไม่ใช่สี่เหลี่ยมผืนผ้า เนื่องจากสามารถดันแอปให้อยู่ในพื้นที่สี่เหลี่ยมผืนผ้าและเพิ่มทรานซิชันที่ราบรื่นในพื้นที่ที่ไม่ใช่สี่เหลี่ยมผืนผ้า

installBaseLayoutAround จะได้รับ Consumer<InsetsOEMV1> ด้วย สามารถใช้ผู้บริโภคนี้เพื่อสื่อสารกับแอปว่าปลั๊กอินครอบคลุมเนื้อหาของแอปบางส่วน (ด้วยแถบเครื่องมือหรืออื่นๆ) จากนั้นแอปจะรู้ว่าต้องวาดในพื้นที่นี้ต่อไป แต่ต้องไม่วาดคอมโพเนนต์ที่สำคัญซึ่งผู้ใช้โต้ตอบได้ เอฟเฟกต์นี้ใช้ในการออกแบบอ้างอิงของเราเพื่อทำให้แถบเครื่องมือโปร่งแสงครึ่งหนึ่งและมีรายการที่เลื่อนอยู่ใต้แถบเครื่องมือ หากไม่ได้ใช้ฟีเจอร์นี้ รายการแรกในรายการจะติดอยู่ใต้แถบเครื่องมือและคลิกไม่ได้ หากไม่ต้องการเอฟเฟกต์นี้ ปลั๊กอินจะละเว้น Consumer ได้

เนื้อหาที่เลื่อนอยู่ใต้แถบเครื่องมือ รูปที่ 2 เนื้อหาที่เลื่อนอยู่ใต้แถบเครื่องมือ

จากมุมมองของแอป เมื่อปลั๊กอินส่งข้อมูลแทรกใหม่ แอปจะได้รับข้อมูลแทรกเหล่านั้นจากกิจกรรมหรือข้อมูลโค้ดที่ใช้งาน InsetsChangedListener หากกิจกรรมหรือฟragment ไม่ได้ใช้ InsetsChangedListener ไลบรารี UI ของรถยนต์จะจัดการส่วนตัดโดยค่าเริ่มต้นโดยใช้ส่วนตัดเป็นระยะห่างจากขอบของ Activity หรือ FragmentActivity ที่มีฟragment ไลบรารีจะไม่ใช้ส่วนตัดกับเศษข้อมูลโดยค่าเริ่มต้น ต่อไปนี้คือตัวอย่างข้อมูลโค้ดของการใช้งานที่ใช้ส่วนตัดเป็นระยะห่างจากขอบใน 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 ซึ่งใช้เพื่อระบุว่าวิวที่ควรตัดขึ้นบรรทัดใหม่นั้นกินพื้นที่ทั้งแอปหรือเพียงส่วนเล็กๆ ซึ่งใช้เพื่อหลีกเลี่ยงการใช้การตกแต่งบางอย่างตามขอบที่จะมีความหมายก็ต่อเมื่อปรากฏตามขอบของทั้งหน้าจอ แอปตัวอย่างที่ใช้เลย์เอาต์พื้นฐานแบบไม่เต็มหน้าจอคือการตั้งค่า ซึ่งแต่ละแผงของเลย์เอาต์แบบ 2 แผงจะมีแถบเครื่องมือของตัวเอง

เนื่องจากคาดว่า installBaseLayoutAround จะแสดงผล Null เมื่อ toolbarEnabled เป็น false ดังนั้นหากต้องการให้ปลั๊กอินระบุว่าไม่ต้องการปรับแต่งเลย์เอาต์พื้นฐาน ปลั๊กอินจะต้องแสดงผล false จาก customizesBaseLayout

เลย์เอาต์ฐานต้องมี FocusParkingView และ FocusArea เพื่อรองรับการควบคุมแบบหมุนอย่างเต็มรูปแบบ คุณละเว้นมุมมองเหล่านี้ได้ในอุปกรณ์ที่ไม่รองรับภาพสไลด์ FocusParkingView/FocusAreas ติดตั้งใช้งานในไลบรารี CarUi แบบคงที่ จึงใช้ setRotaryFactories เพื่อจัดหาโรงงานเพื่อสร้างมุมมองจากบริบท

บริบทที่ใช้สร้างมุมมองโฟกัสต้องเป็นบริบทของแหล่งที่มา ไม่ใช่บริบทของปลั๊กอิน FocusParkingView ควรอยู่ใกล้กับมุมมองแรกในแผนภูมิมากที่สุดเท่าที่เป็นไปได้ เนื่องจากเป็นมุมมองที่โฟกัสเมื่อผู้ใช้ไม่ควรเห็นโฟกัส FocusArea ต้องตัดแถบเครื่องมือในเลย์เอาต์พื้นฐานเพื่อระบุว่าเป็นโซนการแตะเพื่อเลื่อนแบบหมุน หากไม่มี FocusArea ผู้ใช้จะไปยังปุ่มใดๆ ในแถบเครื่องมือด้วยตัวควบคุมแบบหมุนไม่ได้

ตัวควบคุมแถบเครื่องมือ

ToolbarController ที่แสดงผลจริงควรติดตั้งใช้งานได้ง่ายกว่าเลย์เอาต์พื้นฐาน โดยมีหน้าที่รับข้อมูลที่ส่งไปยังตัวตั้งค่าและแสดงในเลย์เอาต์พื้นฐาน ดูข้อมูลเกี่ยวกับเมธอดส่วนใหญ่ได้ใน Javadoc วิธีการที่ซับซ้อนมากขึ้นบางส่วนจะอธิบายไว้ด้านล่าง

getImeSearchInterface ใช้สำหรับแสดงผลการค้นหาในหน้าต่าง IME (แป้นพิมพ์) ซึ่งอาจมีประโยชน์ในการแสดง/ภาพเคลื่อนไหวของผลการค้นหาควบคู่ไปกับแป้นพิมพ์ เช่น ในกรณีที่แป้นพิมพ์แสดงเพียงครึ่งหน้าจอ ฟังก์ชันการทํางานส่วนใหญ่ติดตั้งใช้งานในไลบรารี CarUi แบบคงที่ ส่วนอินเทอร์เฟซการค้นหาในปลั๊กอินมีไว้เพื่อระบุวิธีการสำหรับไลบรารีแบบคงที่ในการรับการเรียกกลับ TextView และ onPrivateIMECommand หากต้องการรองรับการดำเนินการนี้ ปลั๊กอินควรใช้คลาสย่อย TextView ที่ลบล้าง onPrivateIMECommand และส่งการเรียกไปยัง Listener ที่ระบุเป็น TextView ของแถบค้นหา

setMenuItems เพียงแค่แสดง MenuItems บนหน้าจอ แต่ระบบจะเรียกใช้บ่อยมาก เนื่องจาก API ของปลั๊กอินสำหรับ MenuItem เป็นแบบคงที่ เมื่อใดก็ตามที่มีการเปลี่ยนแปลง MenuItem ระบบจะเรียกใช้ setMenuItems ใหม่ทั้งหมด กรณีนี้อาจเกิดขึ้นได้กับเหตุการณ์เล็กๆ น้อยๆ เช่น ผู้ใช้คลิก MenuItem ที่เป็นสวิตช์ และการคลิกดังกล่าวทําให้สวิตช์เปิด/ปิด ทั้งในด้านประสิทธิภาพและภาพเคลื่อนไหว เราจึงขอแนะนำให้คำนวณความแตกต่างระหว่างรายการ MenuItems รายการเก่าและใหม่ และอัปเดตเฉพาะมุมมองที่มีการเปลี่ยนแปลงจริงเท่านั้น MenuItems จะมีช่อง key ที่ช่วยแก้ปัญหานี้ได้ เนื่องจากคีย์ควรเหมือนกันในการเรียกใช้ setMenuItems ที่แตกต่างกันสําหรับ MenuItem เดียวกัน

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)
//  }
}