ใช้ปลั๊กอินไลบรารี 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 ด้วย Flagshared-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) หากต้องการกำจัดข้อผิดพลาด ให้เปลี่ยนตัวเลือกเปิดเป็นไม่มีในการกำหนดค่าบิลด์
รูปที่ 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)
// }
}