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