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

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

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

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

ใช้งานและสร้างไลบรารีที่แบ่งใช้

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

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

การพึ่งพาไลบรารีแบบแบ่งใช้

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

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

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

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

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

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

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

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

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

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

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

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

PluginVersionProviderOEMV1 มีหนึ่งวิธีในนั้น:

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

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

เพื่อรักษาความเข้ากันได้แบบย้อนหลังกับแอปที่คอมไพล์กับไลบรารี Car Ui แบบคงที่เวอร์ชันเก่า ขอแนะนำให้รองรับ maxVersion s 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 การเลื่อนเนื้อหาใต้แถบเครื่องมือ

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

ปลั๊กอินสามารถใช้งานได้โดยใช้ Jetpack Compose แต่นี่เป็นฟีเจอร์ระดับอัลฟ่าและไม่ควรถือว่าเสถียร

ปลั๊กอินสามารถใช้ ComposeView เพื่อสร้างพื้นผิวที่เปิดใช้งานการเขียนเพื่อแสดงผล 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)
//  }
}