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

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

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

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

ดูตัวอย่างใน Soong และ Gradle

ซูง

ลองดูตัวอย่างของ 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",
}

เกรเดิล

ดูไฟล์ 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 ค่าเริ่มต้น จะถูกนำมาใช้แทนการติดตั้งปลั๊กอิน เนื้อหา โดยไม่ต้องมีคลาส 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 Static Library ที่คอมไพล์ลงในแอปโดยตรง ไลบรารีที่ใช้ร่วมกันของ Android คอมไพล์เป็น APK แบบสแตนด์อโลนที่มีการอ้างอิง แอปอื่นๆ ขณะรันไทม์

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

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

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

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

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

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

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

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

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

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

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

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

Toolbar

เลย์เอาต์ฐาน

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

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

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

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

การเลื่อนเนื้อหาใต้แถบเครื่องมือ รูปที่ 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 ซึ่งใช้เพื่อระบุว่า มุมมองที่ควรรวมอาจใช้ทั้งแอปหรือเพียงส่วนเล็กๆ สามารถใช้เพื่อหลีกเลี่ยงการใช้การตกแต่งที่ขอบ จะเหมาะสมเมื่อแสดงที่ขอบของทั้งหน้าจอเท่านั้น ตัวอย่าง แอปที่ใช้เลย์เอาต์พื้นฐานที่ไม่ใช่แบบเต็มหน้าจอคือ "การตั้งค่า" ซึ่งแต่ละแผงของ เลย์เอาต์แบบ 2 แผงมีแถบเครื่องมือของตัวเอง

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

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

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

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

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

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

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

มุมมองรูปแบบแอป

AppStyledView เป็นคอนเทนเนอร์สําหรับข้อมูลพร็อพเพอร์ตี้ที่ไม่ได้ปรับแต่งเลย ทั้งนี้ สามารถใช้เพื่อระบุขอบรอบมุมมองนั้นให้โดดเด่น ส่วนอื่นๆ ของแอป และระบุให้ผู้ใช้ทราบว่าเป็นคนละประเภท ของ Google มุมมองที่รวมโดย 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 คือการตั้งค่าแท็กในมุมมองรูท ในเลย์เอาต์เพื่อจัดเก็บตัวแปรร่วมที่มีการใช้ร่วมกัน ComposeViews ที่ต่างกันในลําดับชั้น เนื่องจากรหัสทรัพยากรของปลั๊กอินไม่ใช่ แยกต่างหากจากเนมสเปซของแอป อาจทำให้เกิดข้อขัดแย้งเมื่อทั้ง แอปและปลั๊กอินตั้งค่าแท็กในมุมมองเดียวกัน กำหนดเอง 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)
//  }
}