ใช้ปลั๊กอินไลบรารี 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 Flagshared-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
ปลั๊กอินพร็อกซี
การกำหนดค่า แอปที่ใช้ไลบรารี 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)
// }
}