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

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

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

ปลั๊กอินไลบรารี UI ของรถคือ APK ที่มีคลาสซึ่งใช้ชุด Plugin API 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 ระบบจะใช้การติดตั้งใช้งานเริ่มต้น แทนการติดตั้งใช้งานปลั๊กอิน ไม่จำเป็นต้องมีคลาส Content Provider ในกรณีนี้ อย่าลืมเพิ่ม 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 จะมีคลาส ที่เพิ่มลงใน ClassLoader ที่ใช้ร่วมกันระหว่างแอปโดยอัตโนมัติ เมื่อแอปที่ใช้ไลบรารี UI ของรถระบุการขึ้นต่อกันที่รันไทม์ในไลบรารีที่แชร์ของปลั๊กอิน ตัวโหลดคลาสของแอปจะเข้าถึงคลาสของไลบรารีที่แชร์ของปลั๊กอินได้ ปลั๊กอินที่ใช้งานเป็นแอป Android ปกติ (ไม่ใช่ไลบรารีที่ใช้ร่วมกัน) อาจส่งผลเสียต่อเวลาเริ่มต้นแอปแบบเย็น

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

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

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

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

การปรับแต่งแอปโดยใช้ไลบรารี Car 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 มีเมธอด 1 รายการดังนี้

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 ที่สร้างToolbarเวอร์ชัน 100 และRecyclerViewเวอร์ชัน 1 ได้ เนื่องจากเราไม่สามารถรับประกันได้ว่าคอมโพเนนต์เวอร์ชันต่างๆ จะทำงานร่วมกันได้ หากต้องการใช้แถบเครื่องมือเวอร์ชัน 100 นักพัฒนาแอปจะต้อง ใช้การติดตั้งใช้งานเวอร์ชันของ pluginFactory ที่สร้าง แถบเครื่องมือเวอร์ชัน 100 ซึ่งจะจำกัดตัวเลือกในเวอร์ชันของคอมโพเนนต์อื่นๆ ที่สร้างได้ เวอร์ชันของคอมโพเนนต์อื่นๆ อาจไม่เท่ากัน เช่น pluginFactoryOEMV100 อาจสร้าง ToolbarControllerOEMV100 และ RecyclerViewOEMV70

แถบเครื่องมือ

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

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

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

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

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

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

จากมุมมองของแอป เมื่อปลั๊กอินส่ง Inset ใหม่ แอปจะได้รับ Inset จากกิจกรรมหรือ Fragment ใดๆ ที่ใช้ InsetsChangedListener หากกิจกรรมหรือ Fragment ไม่ได้ใช้ InsetsChangedListener ไลบรารี Car Ui จะจัดการระยะขอบโดยค่าเริ่มต้นด้วยการใช้ระยะขอบเป็นระยะเว้นวรรคกับ Activity หรือ FragmentActivity ที่มี Fragment ไลบรารีจะไม่ ใช้ระยะขอบกับ Fragment โดยค่าเริ่มต้น ต่อไปนี้คือตัวอย่างข้อมูลโค้ดของการ ติดตั้งใช้งานที่ใช้ระยะขอบเป็นระยะเว้นวรรคใน 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 เพื่อจัดเตรียม Factory ให้ สร้างมุมมองจากบริบท

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

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

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

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

setMenuItems จะแสดง MenuItems บนหน้าจอเท่านั้น แต่จะมีการเรียกใช้ บ่อยอย่างไม่น่าเชื่อ เนื่องจาก API ปลั๊กอินสำหรับ MenuItem เปลี่ยนแปลงไม่ได้ ทุกครั้งที่มีการเปลี่ยนแปลง MenuItem ระบบจะเรียกใช้ setMenuItems ใหม่ทั้งหมด ซึ่งอาจเกิดขึ้นได้กับเรื่องเล็กๆ น้อยๆ เช่น ผู้ใช้คลิก MenuItem ของสวิตช์ และการคลิกนั้นทำให้สวิตช์เปิด/ปิด ด้วยเหตุผลด้านประสิทธิภาพและภาพเคลื่อนไหว เราจึงขอแนะนำให้คำนวณความแตกต่างระหว่างรายการ MenuItem เก่าและใหม่ และอัปเดตเฉพาะมุมมองที่มีการเปลี่ยนแปลงจริง 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)
//  }
}