คลังแอป Android for Cars ช่วยให้คุณนำแอปการนำทาง จุดที่น่าสนใจ (POI) อินเทอร์เน็ตของสรรพสิ่ง (IoT) หรือ สภาพอากาศไปใช้ในรถยนต์ได้ โดยการจัดชุดเทมเพลตที่ออกแบบมาเพื่อตอบสนองมาตรฐานการรบกวนผู้ขับ และดูแลรายละเอียดต่างๆ เช่น ปัจจัยหน้าจอรถยนต์ที่หลากหลาย และรูปแบบการป้อนข้อมูล
คู่มือนี้จะแสดงภาพรวมของฟีเจอร์และแนวคิดหลักของไลบรารี และ แนะนำขั้นตอนการตั้งค่าแอปพื้นฐาน
ก่อนเริ่มต้น
- อ่านหน้าออกแบบเพื่อการขับขี่
ซึ่งครอบคลุม Car App Library
- ภาพรวมหมวดหมู่แอปนำทาง และแอปอื่นๆ ที่เกี่ยวข้องกับการขับรถ
- ภาพรวมสร้างแอปด้วยเทมเพลต
- ตัวต่อ ครอบคลุมเทมเพลตและคอมโพเนนต์ของเทมเพลต
- โฟลว์ตัวอย่าง ที่แสดงรูปแบบ UX ทั่วไป
- ข้อกำหนดของแอปที่ใช้เทมเพลต
- โปรดอ่านคำศัพท์และแนวคิดสำคัญในส่วนต่อไปนี้
- ทำความคุ้นเคยกับ UI ของระบบ Android Auto และการออกแบบ Android Automotive OS
- อ่านบันทึกประจำรุ่น
- ดูตัวอย่าง
คำและแนวคิดสำคัญ
- โมเดลและเทมเพลต
- อินเทอร์เฟซผู้ใช้แสดงด้วยกราฟของออบเจ็กต์โมเดลที่สามารถ จัดเรียงร่วมกันได้หลายวิธีตามที่เทมเพลตที่ออบเจ็กต์นั้นสังกัดอยู่ อนุญาต เทมเพลตเป็นกลุ่มย่อยของโมเดลที่ทำหน้าที่เป็นรูทในกราฟเหล่านั้นได้ โมเดลมีข้อมูลที่จะแสดงต่อผู้ใช้ในรูปแบบของข้อความและรูปภาพ รวมถึงแอตทริบิวต์เพื่อกำหนดค่าลักษณะที่มองเห็นได้ของข้อมูลดังกล่าว เช่น สีข้อความหรือขนาดรูปภาพ โฮสต์จะแปลงโมเดลเป็นมุมมองที่ออกแบบมาเพื่อให้เป็นไปตาม มาตรฐานการรบกวนสมาธิของผู้ขับขี่ และดูแลรายละเอียดต่างๆ เช่น ความหลากหลาย ของปัจจัยหน้าจอรถยนต์และรูปแบบการป้อนข้อมูล
- เป็นเจ้าภาพ
- โฮสต์คือคอมโพเนนต์แบ็กเอนด์ที่ใช้ฟังก์ชันการทำงานที่ API ของไลบรารีมีให้ เพื่อให้แอปของคุณทำงานในรถได้ ความรับผิดชอบของโฮสต์มีตั้งแต่การค้นหาแอปและการจัดการ วงจรของแอป ไปจนถึงการแปลงโมเดลเป็นมุมมองและการแจ้งเตือนแอป เกี่ยวกับการโต้ตอบของผู้ใช้ ในอุปกรณ์เคลื่อนที่ Android Auto จะเป็นผู้ติดตั้งใช้งานโฮสต์นี้ ใน Android Automotive OS ระบบจะติดตั้งโฮสต์นี้เป็นแอประบบ
- ข้อจำกัดของเทมเพลต
- เทมเพลตต่างๆ จะบังคับใช้ข้อจำกัดในเนื้อหาของโมเดล ตัวอย่างเช่น เทมเพลตรายการมีขีดจำกัดจำนวนรายการที่แสดงต่อผู้ใช้ได้ นอกจากนี้ เทมเพลตยังมีข้อจำกัดในลักษณะที่สามารถ เชื่อมต่อเพื่อสร้างขั้นตอนของงานได้ด้วย เช่น แอปจะพุชเทมเพลตได้สูงสุด 5 รายการไปยังสแต็กหน้าจอ ดูรายละเอียดเพิ่มเติมได้ที่ข้อจำกัดของเทมเพลต
Screen
Screen
คือคลาสที่ไลบรารี จัดเตรียมไว้ให้ ซึ่งแอปจะใช้เพื่อจัดการอินเทอร์เฟซผู้ใช้ที่แสดงต่อ ผู้ใช้Screen
มีวงจรการใช้งานและมีกลไกให้แอป ส่งเทมเพลตเพื่อแสดงเมื่อหน้าจอแสดงอยู่ นอกจากนี้ คุณยังพุชScreen
อินสแตนซ์ และป๊อปไปยังและจากScreen
สแต็กได้ด้วย ซึ่ง จะช่วยให้มั่นใจได้ว่าอินสแตนซ์เป็นไปตามข้อจำกัดของโฟลว์เทมเพลตCarAppService
CarAppService
เป็นคลาสService
แบบนามธรรมที่แอปของคุณ ต้องใช้และส่งออกเพื่อให้โฮสต์ค้นพบและจัดการได้CarAppService
ของแอปมีหน้าที่ตรวจสอบว่าการเชื่อมต่อโฮสต์เชื่อถือได้โดยใช้createHostValidator
และระบุอินสแตนซ์Session
สำหรับการเชื่อมต่อแต่ละรายการโดยใช้onCreateSession
Session
Session
เป็นคลาสแบบนามธรรมที่แอปของคุณต้องใช้และส่งคืนโดยใช้CarAppService.onCreateSession
โดยเป็นจุดเริ่มต้นในการแสดงข้อมูลบนหน้าจอรถยนต์ โดยมีวงจรการทำงานที่แจ้งสถานะปัจจุบันของแอปบนหน้าจอรถยนต์ เช่น เมื่อแอปของคุณแสดงหรือซ่อนอยู่เมื่อเริ่ม
Session
เช่น เมื่อเปิดแอปเป็นครั้งแรก โฮสต์จะขอScreen
เริ่มต้นเพื่อแสดงโดยใช้วิธีonCreateScreen
ติดตั้ง Car App Library
ดูวิธีการเพิ่มไลบรารีลงในแอปได้ที่หน้าเผยแพร่ของไลบรารี Jetpack
กำหนดค่าไฟล์ Manifest ของแอป
ก่อนที่จะสร้างแอปในรถยนต์ได้ ให้กำหนดค่าไฟล์ Manifest ของแอปดังนี้
ประกาศ CarAppService
โฮสต์จะเชื่อมต่อกับแอปของคุณผ่านการติดตั้งใช้งาน CarAppService
คุณ
ประกาศบริการนี้ในไฟล์ Manifest เพื่อให้โฮสต์ค้นพบและเชื่อมต่อ
กับแอปของคุณได้
นอกจากนี้ คุณยังต้องประกาศหมวดหมู่ของแอปในองค์ประกอบ
<category>
ของ
ตัวกรอง Intent ของแอปด้วย ดูรายการ
หมวดหมู่แอปที่รองรับสำหรับค่าที่อนุญาตสำหรับ
องค์ประกอบนี้
ข้อมูลโค้ดต่อไปนี้แสดงวิธีประกาศบริการแอปในรถยนต์สำหรับแอปจุดที่น่าสนใจในไฟล์ Manifest
<application>
...
<service
...
android:name=".MyCarAppService"
android:exported="true">
<intent-filter>
<action android:name="androidx.car.app.CarAppService"/>
<category android:name="androidx.car.app.category.POI"/>
</intent-filter>
</service>
...
<application>
หมวดหมู่แอปที่รองรับ
ประกาศหมวดหมู่ของแอปโดยเพิ่มค่าหมวดหมู่ต่อไปนี้อย่างน้อย 1 ค่า
ในตัวกรอง Intent เมื่อประกาศ CarAppService
ตามที่อธิบายไว้
ในส่วนก่อนหน้า
androidx.car.app.category.NAVIGATION
: แอปที่ให้เส้นทางการนำทางแบบเลี้ยวต่อเลี้ยว ดูสร้างแอปนำทางสำหรับ รถยนต์androidx.car.app.category.POI
: แอปที่มีฟังก์ชันที่เกี่ยวข้อง กับการค้นหาสถานที่น่าสนใจ เช่น จุดจอดรถ สถานีชาร์จ และ ปั๊มน้ำมัน ดูสร้างแอปจุดที่น่าสนใจสำหรับ รถยนต์androidx.car.app.category.IOT
: แอปที่ช่วยให้ผู้ใช้ดำเนินการที่เกี่ยวข้อง ในอุปกรณ์ที่เชื่อมต่อจากภายในรถได้ ดูสร้างแอปอินเทอร์เน็ตของสรรพสิ่งสำหรับรถยนต์androidx.car.app.category.WEATHER
: แอปที่ช่วยให้ผู้ใช้ดูข้อมูลสภาพอากาศที่เกี่ยวข้อง กับตำแหน่งปัจจุบันหรือตามเส้นทาง ดูสร้างแอปสภาพอากาศสำหรับรถยนต์androidx.car.app.category.MEDIA
: แอปที่ให้ผู้ใช้เรียกดูและเล่น เพลง วิทยุ หนังสือเสียง และเนื้อหาเสียงอื่นๆ ในรถยนต์ ดูสร้างแอปสื่อที่ใช้เทมเพลตสำหรับรถยนต์androidx.car.app.category.MESSAGING
: แอปที่ช่วยให้ผู้ใช้สื่อสารกันได้ โดยใช้ข้อความขนาดสั้น ดูสร้างประสบการณ์การรับส่งข้อความที่ใช้เทมเพลตสำหรับ Android Autoandroidx.car.app.category.CALLING
: แอปที่ช่วยให้ผู้ใช้สื่อสาร โดยใช้การโทรด้วยเสียง ดูสร้างประสบการณ์การโทรสำหรับ Android Auto
ดูคุณภาพแอป Android สำหรับรถยนต์เพื่อดูคำอธิบายโดยละเอียดของแต่ละหมวดหมู่และเกณฑ์สำหรับแอปที่จะอยู่ในหมวดหมู่เหล่านั้น
ระบุชื่อและไอคอนแอป
คุณต้องระบุชื่อและไอคอนแอปที่โฮสต์ใช้เพื่อแสดงแอปของคุณใน UI ของระบบ
คุณระบุชื่อและไอคอนของแอปที่จะใช้เป็นตัวแทนของแอปได้โดยใช้แอตทริบิวต์ label
และ icon
ของ CarAppService
ดังนี้
...
<service
android:name=".MyCarAppService"
android:exported="true"
android:label="@string/my_app_name"
android:icon="@drawable/my_app_icon">
...
</service>
...
หากไม่ได้ประกาศป้ายกำกับหรือไอคอนในองค์ประกอบ
<service>
โฮสต์จะ
กลับไปใช้ค่าที่ระบุสำหรับองค์ประกอบ
<application>
ตั้งค่าธีมที่กำหนดเอง
หากต้องการตั้งค่าธีมที่กำหนดเองสำหรับแอปในรถยนต์ ให้เพิ่มองค์ประกอบ
<meta-data>
ในไฟล์ Manifest ดังนี้
<meta-data android:name="androidx.car.app.theme" android:resource="@style/MyCarAppTheme />
จากนั้นประกาศแหล่งข้อมูลสไตล์เพื่อตั้งค่าแอตทริบิวต์ต่อไปนี้สำหรับธีมแอปในรถยนต์ที่กำหนดเอง
<resources> <style name="MyCarAppTheme"> <item name="carColorPrimary">@layout/my_primary_car_color</item> <item name="carColorPrimaryDark">@layout/my_primary_dark_car_color</item> <item name="carColorSecondary">@layout/my_secondary_car_color</item> <item name="carColorSecondaryDark">@layout/my_secondary_dark_car_color</item> <item name="carPermissionActivityLayout">@layout/my_custom_background</item> </style> </resources>
ระดับ API ของแอปในรถยนต์
ไลบรารีแอปในรถยนต์จะกำหนดระดับ API ของตัวเองเพื่อให้คุณทราบว่าฟีเจอร์ของไลบรารีใดบ้างที่โฮสต์เทมเพลตในรถยนต์รองรับ
หากต้องการดึงข้อมูลระดับ Car App API สูงสุดที่โฮสต์รองรับ ให้ใช้เมธอด
getCarAppApiLevel()
ประกาศระดับ API ขั้นต่ำของแอปในรถยนต์ที่แอปของคุณรองรับในไฟล์
AndroidManifest.xml
ดังนี้
<manifest ...>
<application ...>
<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="1"/>
</application>
</manifest>
ดูรายละเอียดเกี่ยวกับวิธีรักษาความเข้ากันได้แบบย้อนหลังและประกาศระดับ API ขั้นต่ำที่จำเป็นต่อการใช้ฟีเจอร์ได้ในเอกสารประกอบสำหรับคำอธิบายประกอบ
RequiresCarApi
หากต้องการดูคำจำกัดความของระดับ API ที่จำเป็นในการใช้ฟีเจอร์บางอย่างของ Car App Library โปรดดูเอกสารอ้างอิงสำหรับ CarAppApiLevels
สร้าง CarAppService และเซสชัน
แอปของคุณต้องขยายคลาส
CarAppService
และใช้เมธอด
onCreateSession
ซึ่งจะแสดงผลอินสแตนซ์ Session
ที่สอดคล้องกับการเชื่อมต่อปัจจุบันกับโฮสต์
Kotlin
class HelloWorldService : CarAppService() { ... override fun onCreateSession(): Session { return HelloWorldSession() } ... }
Java
public final class HelloWorldService extends CarAppService { ... @Override @NonNull public Session onCreateSession() { return new HelloWorldSession(); } ... }
Session
อินสแตนซ์มีหน้าที่
ส่งคืนอินสแตนซ์ Screen
เพื่อใช้
ครั้งแรกที่เริ่มแอป
Kotlin
class HelloWorldSession : Session() { ... override fun onCreateScreen(intent: Intent): Screen { return HelloWorldScreen(carContext) } ... }
Java
public final class HelloWorldSession extends Session { ... @Override @NonNull public Screen onCreateScreen(@NonNull Intent intent) { return new HelloWorldScreen(getCarContext()); } ... }
หากต้องการจัดการสถานการณ์ที่แอปในรถต้องเริ่มจากหน้าจอที่ไม่ใช่หน้าจอหลักหรือหน้า Landing Page ของแอป เช่น การจัดการ Deep Link คุณสามารถ
กำหนดค่าสแต็กย้อนกลับของหน้าจอไว้ล่วงหน้าได้โดยใช้
ScreenManager.push
ก่อนที่จะกลับจาก
onCreateScreen
การกำหนดค่าล่วงหน้าช่วยให้ผู้ใช้กลับไปยังหน้าจอก่อนหน้าจากหน้าจอแรก
ที่แอปของคุณแสดงได้
สร้างหน้าจอเริ่มต้น
คุณสร้างหน้าจอที่แอปแสดงโดยการกำหนดคลาสที่ขยายคลาส
Screen
และใช้เมธอด
onGetTemplate
ซึ่งจะแสดงอินสแตนซ์
Template
ที่แสดงถึง
สถานะของ UI ที่จะแสดงในหน้าจอรถ
ข้อมูลโค้ดต่อไปนี้แสดงวิธีประกาศ Screen
ที่ใช้เทมเพลต PaneTemplate
เพื่อ
แสดงสตริง "Hello world!" อย่างง่าย
Kotlin
class HelloWorldScreen(carContext: CarContext) : Screen(carContext) { override fun onGetTemplate(): Template { val row = Row.Builder().setTitle("Hello world!").build() val pane = Pane.Builder().addRow(row).build() return PaneTemplate.Builder(pane) .setHeaderAction(Action.APP_ICON) .build() } }
Java
public class HelloWorldScreen extends Screen { @NonNull @Override public Template onGetTemplate() { Row row = new Row.Builder().setTitle("Hello world!").build(); Pane pane = new Pane.Builder().addRow(row).build(); return new PaneTemplate.Builder(pane) .setHeaderAction(Action.APP_ICON) .build(); } }
คลาส CarContext
คลาส CarContext
เป็นคลาสย่อย ContextWrapper
ที่เข้าถึงได้สำหรับอินสแตนซ์ Session
และ
Screen
โดยจะให้สิทธิ์เข้าถึง
บริการของรถยนต์ เช่น
ScreenManager
สำหรับการจัดการสแต็กหน้าจอ
AppManager
สำหรับฟังก์ชันการทำงานทั่วไปที่เกี่ยวข้องกับแอป เช่น การเข้าถึงออบเจ็กต์ Surface
เพื่อวาดแผนที่
และ NavigationManager
ที่แอปนำทางแบบเลี้ยวต่อเลี้ยวใช้เพื่อสื่อสารข้อมูลเมตาของการนำทางและเหตุการณ์อื่นๆ ที่เกี่ยวข้องกับการนำทางกับโฮสต์
ดูรายการฟังก์ชันการทำงานของคลังทั้งหมดที่พร้อมใช้งานสำหรับแอปนำทางได้ที่เข้าถึงเทมเพลตการนำทาง
CarContext
ยังมีฟังก์ชันอื่นๆ เช่น การโหลดทรัพยากร Drawable โดยใช้การกำหนดค่าจากหน้าจอรถยนต์ การเริ่มแอปในรถยนต์โดยใช้ Intent และการส่งสัญญาณว่าแอปควรแสดงแผนที่ในธีมมืดหรือไม่
ใช้การนำทางบนหน้าจอ
แอปมักจะแสดงหน้าจอต่างๆ จำนวนมาก ซึ่งแต่ละหน้าจออาจใช้เทมเพลตที่แตกต่างกันซึ่งผู้ใช้สามารถไปยังส่วนต่างๆ ได้ขณะโต้ตอบกับอินเทอร์เฟซที่แสดงในหน้าจอ
คลาส ScreenManager
มี
สแต็กหน้าจอที่คุณใช้เพื่อพุชหน้าจอที่สามารถป๊อปอัปโดยอัตโนมัติ
เมื่อผู้ใช้เลือกปุ่มย้อนกลับในหน้าจอรถยนต์หรือใช้ปุ่มย้อนกลับของฮาร์ดแวร์
ที่มีในรถยนต์บางรุ่น
ข้อมูลโค้ดต่อไปนี้แสดงวิธีเพิ่มการดำเนินการย้อนกลับลงในเทมเพลตข้อความ รวมถึงการดำเนินการที่แสดงหน้าจอใหม่เมื่อผู้ใช้เลือก
Kotlin
val template = MessageTemplate.Builder("Hello world!") .setHeaderAction(Action.BACK) .addAction( Action.Builder() .setTitle("Next screen") .setOnClickListener { screenManager.push(NextScreen(carContext)) } .build()) .build()
Java
MessageTemplate template = new MessageTemplate.Builder("Hello world!") .setHeaderAction(Action.BACK) .addAction( new Action.Builder() .setTitle("Next screen") .setOnClickListener( () -> getScreenManager().push(new NextScreen(getCarContext()))) .build()) .build();
ออบเจ็กต์ Action.BACK
คือAction
มาตรฐานที่เรียกใช้ ScreenManager.pop
โดยอัตโนมัติ
คุณลบล้างลักษณะการทำงานนี้ได้โดยใช้
OnBackPressedDispatcher
อินสแตนซ์ที่พร้อมใช้งานจาก
CarContext
เพื่อช่วยให้มั่นใจว่าแอปปลอดภัยที่จะใช้ขณะขับรถ สแต็กหน้าจอจึงมีความลึก สูงสุด ได้ 5 หน้าจอ ดูรายละเอียดเพิ่มเติมได้ที่ส่วนข้อจำกัดของเทมเพลต
รีเฟรชเนื้อหาของเทมเพลต
แอปของคุณสามารถขอให้ระบบยกเลิกเนื้อหาของ
Screen
ได้โดยเรียกใช้เมธอด
Screen.invalidate
จากนั้นโฮสต์จะเรียกกลับไปยังเมธอด Screen.onGetTemplate
ของแอปเพื่อดึงข้อมูลเทมเพลตที่มีเนื้อหาใหม่
เมื่อรีเฟรช Screen
คุณควรทำความเข้าใจเนื้อหาที่เฉพาะเจาะจงในเทมเพลตที่อัปเดตได้
เพื่อไม่ให้โฮสต์นับเทมเพลตใหม่รวมกับโควต้าเทมเพลต
ดูรายละเอียดเพิ่มเติมได้ที่ส่วนข้อจำกัดของเทมเพลต
เราขอแนะนำให้คุณจัดโครงสร้างหน้าจอเพื่อให้มีการแมปแบบหนึ่งต่อหนึ่ง
ระหว่าง Screen
กับประเภทของ
เทมเพลตที่แสดงผ่านการติดตั้งใช้งาน onGetTemplate
วาดแผนที่
แอปการนำทาง จุดที่น่าสนใจ (POI) และสภาพอากาศที่ใช้เทมเพลตต่อไปนี้
จะวาดแผนที่ได้โดยการเข้าถึง
Surface
หากต้องการใช้เทมเพลตต่อไปนี้ แอปของคุณต้องมีสิทธิ์ที่เกี่ยวข้องอย่างใดอย่างหนึ่งซึ่งประกาศไว้ในองค์ประกอบ <uses-permission>
ในไฟล์ AndroidManifest.xml
เทมเพลต | สิทธิ์ของเทมเพลต | คำแนะนำเกี่ยวกับหมวดหมู่ |
---|---|---|
NavigationTemplate |
androidx.car.app.NAVIGATION_TEMPLATES |
การนำทาง |
MapWithContentTemplate |
androidx.car.app.NAVIGATION_TEMPLATES หรือ androidx.car.app.MAP_TEMPLATES |
การนำทาง POI สภาพอากาศ |
MapTemplate (เลิกใช้งานแล้ว) |
androidx.car.app.NAVIGATION_TEMPLATES |
การนำทาง |
PlaceListNavigationTemplate (เลิกใช้งานแล้ว) |
androidx.car.app.NAVIGATION_TEMPLATES |
การนำทาง |
RoutePreviewNavigationTemplate (เลิกใช้งานแล้ว) |
androidx.car.app.NAVIGATION_TEMPLATES |
การนำทาง |
ประกาศสิทธิ์พื้นผิว
นอกเหนือจากสิทธิ์ที่จำเป็นสำหรับเทมเพลตที่แอปของคุณใช้แล้ว
แอปของคุณต้องประกาศสิทธิ์ androidx.car.app.ACCESS_SURFACE
ในไฟล์
AndroidManifest.xml
เพื่อรับสิทธิ์เข้าถึงพื้นผิว
<manifest ...>
...
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
...
</manifest>
เข้าถึงพื้นผิว
หากต้องการเข้าถึง Surface
ที่โฮสต์ระบุ คุณต้องใช้ SurfaceCallback
และระบุการใช้งานดังกล่าวให้กับบริการรถยนต์ของ AppManager
ระบบจะส่ง Surface
ปัจจุบันไปยัง
SurfaceCallback
ในพารามิเตอร์ SurfaceContainer
ของ
onSurfaceAvailable()
และการเรียกกลับ onSurfaceDestroyed()
Kotlin
carContext.getCarService(AppManager::class.java).setSurfaceCallback(surfaceCallback)
Java
carContext.getCarService(AppManager.class).setSurfaceCallback(surfaceCallback);
ทำความเข้าใจพื้นที่ที่มองเห็นได้ของพื้นผิว
โฮสต์สามารถวาดองค์ประกอบของอินเทอร์เฟซผู้ใช้สำหรับเทมเพลตไว้ด้านบนของแผนที่ได้
โฮสต์จะสื่อสารพื้นที่ของพื้นผิวที่รับประกันว่า
ไม่มีสิ่งกีดขวางและผู้ใช้มองเห็นได้อย่างเต็มที่โดยการเรียกใช้เมธอด
SurfaceCallback.onVisibleAreaChanged
นอกจากนี้ เพื่อลดจำนวนการเปลี่ยนแปลง โฮสต์จะเรียกใช้เมธอด
SurfaceCallback.onStableAreaChanged
ด้วยสี่เหลี่ยมผืนผ้าที่เล็กที่สุด ซึ่งจะมองเห็นได้เสมอตามเทมเพลตปัจจุบัน
ตัวอย่างเช่น เมื่อแอปนำทางใช้
NavigationTemplate
ร่วมกับแถบการดำเนินการที่ด้านบน แถบการดำเนินการจะซ่อน
ตัวเองได้เมื่อผู้ใช้ไม่ได้โต้ตอบกับหน้าจอเป็นระยะเวลาหนึ่ง เพื่อให้มี
พื้นที่สำหรับแผนที่มากขึ้น ในกรณีนี้ มีการเรียกกลับไปยัง onStableAreaChanged
และ
onVisibleAreaChanged
ที่มีสี่เหลี่ยมผืนผ้าเดียวกัน เมื่อแถบการทำงานซ่อนอยู่
ระบบจะเรียกใช้เฉพาะ onVisibleAreaChanged
ในพื้นที่ที่ใหญ่ขึ้น หากผู้ใช้โต้ตอบกับหน้าจอ ระบบจะเรียกใช้ onVisibleAreaChanged
อีกครั้งโดยมี
สี่เหลี่ยมผืนผ้าแรก
รองรับธีมมืด
แอปต้องวาดแผนที่ใหม่ลงในอินสแตนซ์ Surface
โดยใช้สีเข้มที่เหมาะสม
เมื่อโฮสต์พิจารณาว่ามีเงื่อนไขที่รับประกันได้ ตามที่อธิบายไว้ใน
คุณภาพแอป Android สำหรับรถยนต์
หากต้องการเลือกว่าจะวาดแผนที่มืดหรือไม่ คุณสามารถใช้
CarContext.isDarkMode
วิธี เมื่อใดก็ตามที่สถานะธีมมืดมีการเปลี่ยนแปลง คุณจะได้รับการเรียกใช้ไปยัง
Session.onCarConfigurationChanged
วาดแผนที่บนจอแสดงผลคลัสเตอร์
นอกจากจะวาดแผนที่บนจอแสดงผลหลักแล้ว แอปการนำทางยังรองรับ การวาดแผนที่บนจอแสดงผลแผงหน้าปัดด้านหลังพวงมาลัยด้วย ดูคำแนะนำเพิ่มเติมได้ที่การวาดภาพไปยังจอแสดงผลคลัสเตอร์
อนุญาตให้ผู้ใช้โต้ตอบกับแผนที่
เมื่อใช้เทมเพลตต่อไปนี้ คุณจะเพิ่มการรองรับเพื่อให้ผู้ใช้โต้ตอบกับแผนที่ที่คุณวาดได้ เช่น อนุญาตให้ผู้ใช้ดูส่วนต่างๆ ของแผนที่โดยการซูมและเลื่อน
เทมเพลต | การโต้ตอบที่รองรับตั้งแต่ระดับ API ของแอปในรถยนต์ |
---|---|
NavigationTemplate |
2 |
PlaceListNavigationTemplate (เลิกใช้งานแล้ว) |
4 |
RoutePreviewNavigationTemplate (เลิกใช้งานแล้ว) |
4 |
MapTemplate (เลิกใช้งานแล้ว) |
5 (introduction of template) |
MapWithContentTemplate |
7 (introduction of template) |
ใช้การเรียกกลับของการโต้ตอบ
อินเทอร์เฟซ SurfaceCallback
มีเมธอดเรียกกลับหลายรายการที่คุณใช้เพื่อเพิ่มการโต้ตอบลงในแผนที่ที่สร้าง
ด้วยเทมเพลตในส่วนก่อนหน้าได้
การโต้ตอบ | SurfaceCallback วิธี |
รองรับตั้งแต่ระดับ API ของแอปในรถยนต์ |
---|---|---|
แตะ | onClick |
5 |
บีบและกางนิ้วเพื่อซูม | onScale |
2 |
การลากด้วยการสัมผัสครั้งเดียว | onScroll |
2 |
การปัดด้วยการแตะเพียงครั้งเดียว | onFling |
2 |
แตะสองครั้ง | onScale (โดยมีตัวคูณมาตราส่วนที่กำหนดโดยโฮสต์เทมเพลต) |
2 |
การเลื่อนแบบหมุนในโหมดเลื่อน | onScroll (โดยมีปัจจัยด้านระยะทางที่กำหนดโดยโฮสต์เทมเพลต) |
2 |
เพิ่มแถบการทำงานของแผนที่
เทมเพลตเหล่านี้มีแถบการทำงานของแผนที่สำหรับการทำงานที่เกี่ยวข้องกับแผนที่ เช่น การซูมเข้าและออก การจัดกึ่งกลางใหม่ การแสดงเข็มทิศ และการทำงานอื่นๆ ที่คุณ เลือกแสดง แถบการทำงานของแผนที่สามารถมีปุ่มที่มีไอคอนเท่านั้นได้สูงสุด 4 ปุ่ม ซึ่งรีเฟรชได้โดยไม่ส่งผลต่อความลึกของงาน โดยจะซ่อนในสถานะว่าง และจะปรากฏอีกครั้งในสถานะใช้งาน
หากต้องการรับการเรียกกลับแบบอินเทอร์แอกทีฟของแผนที่ คุณต้องเพิ่มปุ่ม Action.PAN
ในแถบการดำเนินการของแผนที่ เมื่อผู้ใช้
กดปุ่มแพน โฮสต์จะเข้าสู่โหมดแพนตามที่อธิบายไว้ในส่วน
ต่อไปนี้
หากแอปของคุณไม่มีAction.PAN
ปุ่มในแถบการทำงานของแผนที่ แอปจะไม่ได้รับข้อมูลจากผู้ใช้จากเมธอด
SurfaceCallback
และโฮสต์จะออกจากโหมดแพนที่เปิดใช้งานก่อนหน้านี้
ในหน้าจอสัมผัส ปุ่มเลื่อนจะไม่แสดง
ทำความเข้าใจโหมดเลื่อน
ในโหมดแพน โฮสต์เทมเพลตจะแปลอินพุตของผู้ใช้จากอุปกรณ์อินพุตที่ไม่ใช่แบบสัมผัส
เช่น ตัวควบคุมแบบหมุนและทัชแพด เป็นเมธอด SurfaceCallback
ที่เหมาะสม ตอบสนองต่อการดำเนินการของผู้ใช้เพื่อเข้าหรือออกจากโหมดแพน
ด้วยเมธอด
setPanModeListener
ใน NavigationTemplate.Builder
โฮสต์สามารถซ่อนคอมโพเนนต์ UI อื่นๆ
ในเทมเพลตขณะที่ผู้ใช้อยู่ในโหมดแพน
โต้ตอบกับผู้ใช้
แอปของคุณสามารถโต้ตอบกับผู้ใช้โดยใช้รูปแบบที่คล้ายกับแอปบนอุปกรณ์เคลื่อนที่
จัดการอินพุตของผู้ใช้
แอปของคุณสามารถตอบสนองต่ออินพุตของผู้ใช้ได้โดยการส่ง Listener ที่เหมาะสมไปยังโมเดลที่รองรับ ข้อมูลโค้ดต่อไปนี้แสดงวิธีสร้างโมเดล
Action
ที่ตั้งค่า
OnClickListener
ที่
เรียกกลับไปยังเมธอดที่กำหนดโดยโค้ดของแอป
Kotlin
val action = Action.Builder() .setTitle("Navigate") .setOnClickListener(::onClickNavigate) .build()
Java
Action action = new Action.Builder() .setTitle("Navigate") .setOnClickListener(this::onClickNavigate) .build();
จากนั้นวิธี onClickNavigate
จะเริ่มแอปนำทางในรถยนต์เริ่มต้นได้โดยใช้วิธี
CarContext.startCarApp
ดังนี้
Kotlin
private fun onClickNavigate() { val intent = Intent(CarContext.ACTION_NAVIGATE, Uri.parse("geo:0,0?q=" + address)) carContext.startCarApp(intent) }
Java
private void onClickNavigate() { Intent intent = new Intent(CarContext.ACTION_NAVIGATE, Uri.parse("geo:0,0?q=" + address)); getCarContext().startCarApp(intent); }
ดูรายละเอียดเพิ่มเติมเกี่ยวกับวิธีเริ่มแอป รวมถึงรูปแบบของ
ACTION_NAVIGATE
Intent ได้ที่ส่วนเริ่มแอปในรถยนต์ด้วย Intent
การดำเนินการบางอย่าง เช่น การดำเนินการที่ต้องนำผู้ใช้ไปโต้ตอบต่อในอุปกรณ์เคลื่อนที่ จะอนุญาตให้ทำได้เมื่อจอดรถเท่านั้น
คุณใช้
ParkedOnlyOnClickListener
เพื่อใช้การดำเนินการเหล่านั้นได้ หากไม่ได้จอดรถ โฮสต์จะแสดง
ข้อบ่งชี้แก่ผู้ใช้ว่าระบบไม่อนุญาตให้ดำเนินการในกรณีนี้ หากรถจอดอยู่ ระบบจะเรียกใช้โค้ดตามปกติ ข้อมูลโค้ดต่อไปนี้แสดงวิธี
ใช้ ParkedOnlyOnClickListener
เพื่อเปิดหน้าจอการตั้งค่าในอุปกรณ์เคลื่อนที่
Kotlin
val row = Row.Builder() .setTitle("Open Settings") .setOnClickListener(ParkedOnlyOnClickListener.create(::openSettingsOnPhone)) .build()
Java
Row row = new Row.Builder() .setTitle("Open Settings") .setOnClickListener(ParkedOnlyOnClickListener.create(this::openSettingsOnPhone)) .build();
แสดงการแจ้งเตือน
การแจ้งเตือนที่ส่งไปยังอุปกรณ์เคลื่อนที่จะแสดงบนหน้าจอรถยนต์ก็ต่อเมื่อมีการขยายด้วย CarAppExtender
เท่านั้น
คุณตั้งค่าแอตทริบิวต์การแจ้งเตือนบางอย่าง เช่น ชื่อเนื้อหา ข้อความ ไอคอน และการดำเนินการ ได้ใน CarAppExtender
ซึ่งจะลบล้างแอตทริบิวต์ของการแจ้งเตือนเมื่อปรากฏบนหน้าจอรถยนต์
ข้อมูลโค้ดต่อไปนี้แสดงวิธีส่งการแจ้งเตือนไปยังหน้าจอรถยนต์ซึ่ง แสดงชื่อที่แตกต่างจากชื่อที่แสดงบนอุปกรณ์เคลื่อนที่
Kotlin
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) .setContentTitle(titleOnThePhone) .extend( CarAppExtender.Builder() .setContentTitle(titleOnTheCar) ... .build()) .build()
Java
Notification notification = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) .setContentTitle(titleOnThePhone) .extend( new CarAppExtender.Builder() .setContentTitle(titleOnTheCar) ... .build()) .build();
การแจ้งเตือนอาจส่งผลต่อส่วนต่อไปนี้ของอินเทอร์เฟซผู้ใช้
- ระบบอาจแสดงการแจ้งเตือนล่วงหน้า (HUN) ต่อผู้ใช้
- อาจมีการเพิ่มรายการในศูนย์การแจ้งเตือนพร้อมป้าย (ไม่บังคับ) ที่มองเห็นได้ในแถบ
- สำหรับแอปนำทาง การแจ้งเตือนอาจแสดงในวิดเจ็ตแถบข้างตามที่อธิบายไว้ในการแจ้งเตือนแบบเลี้ยวต่อเลี้ยว
คุณเลือกวิธีตั้งค่าการแจ้งเตือนของแอปให้มีผลต่อองค์ประกอบอินเทอร์เฟซผู้ใช้แต่ละรายการได้โดยใช้ลำดับความสำคัญของการแจ้งเตือนตามที่อธิบายไว้ในเอกสารประกอบCarAppExtender
หากมีการเรียกใช้
NotificationCompat.Builder.setOnlyAlertOnce
โดยมีค่าเป็น true
การแจ้งเตือนที่มีลำดับความสำคัญสูงจะแสดงเป็น
HUN เพียงครั้งเดียว
ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีออกแบบการแจ้งเตือนของแอปในรถยนต์ได้ที่ คู่มือการออกแบบของ Google สำหรับการขับรถเกี่ยวกับ การแจ้งเตือน
แสดงข้อความโทสต์
แอปของคุณสามารถแสดงข้อความโทสต์โดยใช้
CarToast
ดังที่แสดงในข้อมูลโค้ดนี้
Kotlin
CarToast.makeText(carContext, "Hello!", CarToast.LENGTH_SHORT).show()
Java
CarToast.makeText(getCarContext(), "Hello!", CarToast.LENGTH_SHORT).show();
ขอสิทธิ์
หากแอปต้องเข้าถึงข้อมูลหรือการดำเนินการที่ถูกจำกัด เช่น
ตำแหน่ง กฎมาตรฐานของสิทธิ์
ใน Android
จะมีผล หากต้องการขอสิทธิ์ คุณสามารถใช้เมธอด
CarContext.requestPermissions()
ได้
ข้อดีของการใช้
CarContext.requestPermissions()
เมื่อเทียบกับการใช้
Android API มาตรฐานคือ
คุณไม่จำเป็นต้องเปิดตัว Activity
ของคุณเองเพื่อ
สร้างกล่องโต้ตอบสิทธิ์ นอกจากนี้ คุณยังใช้โค้ดเดียวกันได้ทั้งใน Android Auto และ Android Automotive OS แทนที่จะต้องสร้างโฟลว์ที่ขึ้นอยู่กับแพลตฟอร์ม
จัดรูปแบบกล่องโต้ตอบสิทธิ์ใน Android Auto
ใน Android Auto กล่องโต้ตอบสิทธิ์สำหรับผู้ใช้จะปรากฏในโทรศัพท์
โดยค่าเริ่มต้น จะไม่มีพื้นหลังด้านหลังกล่องโต้ตอบ หากต้องการตั้งค่า
พื้นหลังที่กำหนดเอง ให้ประกาศธีมแอปในรถยนต์ในไฟล์
AndroidManifest.xml
แล้วตั้งค่าแอตทริบิวต์ carPermissionActivityLayout
สำหรับธีมแอปในรถยนต์
<meta-data android:name="androidx.car.app.theme" android:resource="@style/MyCarAppTheme />
จากนั้นตั้งค่าแอตทริบิวต์ carPermissionActivityLayout
สำหรับธีมแอปในรถยนต์
<resources> <style name="MyCarAppTheme"> <item name="carPermissionActivityLayout">@layout/my_custom_background</item> </style> </resources>
เริ่มแอปในรถยนต์ด้วย Intent
คุณสามารถเรียกใช้เมธอด
CarContext.startCarApp
เพื่อดำเนินการอย่างใดอย่างหนึ่งต่อไปนี้
- เปิดแป้นโทรศัพท์เพื่อโทรออก
- เริ่มการนำทางแบบเลี้ยวต่อเลี้ยวไปยังตำแหน่งด้วยแอปนำทางในรถยนต์เริ่มต้น
- เริ่มสร้างแอปของคุณเองด้วย Intent
ตัวอย่างต่อไปนี้แสดงวิธีสร้างการแจ้งเตือนพร้อมการดำเนินการที่
เปิดแอปของคุณด้วยหน้าจอที่แสดงรายละเอียดการจองที่จอดรถ
คุณขยายอินสแตนซ์การแจ้งเตือนด้วย Intent ของเนื้อหาที่มีPendingIntent
ซึ่งครอบคลุม Intent ที่ชัดเจน
ไปยังการดำเนินการของแอป
Kotlin
val notification = notificationBuilder ... .extend( CarAppExtender.Builder() .setContentIntent( PendingIntent.getBroadcast( context, ACTION_VIEW_PARKING_RESERVATION.hashCode(), Intent(ACTION_VIEW_PARKING_RESERVATION) .setComponent(ComponentName(context, MyNotificationReceiver::class.java)), 0)) .build())
Java
Notification notification = notificationBuilder ... .extend( new CarAppExtender.Builder() .setContentIntent( PendingIntent.getBroadcast( context, ACTION_VIEW_PARKING_RESERVATION.hashCode(), new Intent(ACTION_VIEW_PARKING_RESERVATION) .setComponent(new ComponentName(context, MyNotificationReceiver.class)), 0)) .build());
แอปของคุณต้องประกาศ
BroadcastReceiver
ที่
เรียกใช้เพื่อประมวลผล Intent เมื่อผู้ใช้เลือกการดำเนินการใน
อินเทอร์เฟซการแจ้งเตือนและเรียกใช้
CarContext.startCarApp
ด้วย Intent ที่มี URI ของข้อมูลด้วย
Kotlin
class MyNotificationReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val intentAction = intent.action if (ACTION_VIEW_PARKING_RESERVATION == intentAction) { CarContext.startCarApp( intent, Intent(Intent.ACTION_VIEW) .setComponent(ComponentName(context, MyCarAppService::class.java)) .setData(Uri.fromParts(MY_URI_SCHEME, MY_URI_HOST, intentAction))) } } }
Java
public class MyNotificationReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String intentAction = intent.getAction(); if (ACTION_VIEW_PARKING_RESERVATION.equals(intentAction)) { CarContext.startCarApp( intent, new Intent(Intent.ACTION_VIEW) .setComponent(new ComponentName(context, MyCarAppService.class)) .setData(Uri.fromParts(MY_URI_SCHEME, MY_URI_HOST, intentAction))); } } }
สุดท้ายนี้ เมธอด
Session.onNewIntent
ในแอปจะจัดการ Intent นี้โดยการพุชหน้าจอการจองที่จอดรถ
ในสแต็ก หากยังไม่ได้อยู่ด้านบน
Kotlin
override fun onNewIntent(intent: Intent) { val screenManager = carContext.getCarService(ScreenManager::class.java) val uri = intent.data if (uri != null && MY_URI_SCHEME == uri.scheme && MY_URI_HOST == uri.schemeSpecificPart && ACTION_VIEW_PARKING_RESERVATION == uri.fragment ) { val top = screenManager.top if (top !is ParkingReservationScreen) { screenManager.push(ParkingReservationScreen(carContext)) } } }
Java
@Override public void onNewIntent(@NonNull Intent intent) { ScreenManager screenManager = getCarContext().getCarService(ScreenManager.class); Uri uri = intent.getData(); if (uri != null && MY_URI_SCHEME.equals(uri.getScheme()) && MY_URI_HOST.equals(uri.getSchemeSpecificPart()) && ACTION_VIEW_PARKING_RESERVATION.equals(uri.getFragment()) ) { Screen top = screenManager.getTop(); if (!(top instanceof ParkingReservationScreen)) { screenManager.push(new ParkingReservationScreen(getCarContext())); } } }
ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีจัดการการแจ้งเตือนสำหรับแอปในรถยนต์ได้ที่ส่วนแสดงการแจ้งเตือน
ข้อจำกัดของเทมเพลต
โฮสต์จำกัดจำนวนเทมเพลตที่จะแสดงสำหรับงานหนึ่งๆ ไว้ที่ 5 รายการ โดยเทมเพลตสุดท้ายต้องเป็นประเภทใดประเภทหนึ่งต่อไปนี้
NavigationTemplate
PaneTemplate
MessageTemplate
MediaPlaybackTemplate
SignInTemplate
LongMessageTemplate
โปรดทราบว่าขีดจำกัดนี้มีผลกับจำนวนเทมเพลต ไม่ใช่จำนวนอินสแตนซ์ Screen
ในสแต็ก ตัวอย่างเช่น หากแอปส่งเทมเพลต 2 รายการขณะอยู่ในหน้าจอ A แล้วพุชหน้าจอ B ตอนนี้แอปจะส่งเทมเพลตได้อีก 3 รายการ หรือหากแต่ละหน้าจอมีโครงสร้าง
เพื่อส่งเทมเพลตเดียว แอปจะสามารถพุชอินสแตนซ์ของหน้าจอ 5 รายการไปยังScreenManager
สแต็กได้
ข้อจำกัดเหล่านี้มีข้อยกเว้นในกรณีพิเศษ ได้แก่ การรีเฟรชเทมเพลตและการดำเนินการสำรองข้อมูลและรีเซ็ต
การรีเฟรชเทมเพลต
การอัปเดตเนื้อหาบางอย่างจะไม่นับรวมในขีดจำกัดของเทมเพลต โดยทั่วไป
หากแอปพุชเทมเพลตใหม่ซึ่งเป็นประเภทเดียวกันและมี
เนื้อหาหลักเดียวกันกับเทมเพลตก่อนหน้า ระบบจะไม่นับเทมเพลตใหม่
รวมในโควต้า เช่น การอัปเดตสถานะเปิด/ปิดของแถวใน
ListTemplate
จะไม่นับรวม
ในโควต้า โปรดดูเอกสารประกอบของเทมเพลตแต่ละรายการเพื่อดูข้อมูลเพิ่มเติม
เกี่ยวกับประเภทการอัปเดตเนื้อหาที่ถือว่าเป็นการรีเฟรช
การดำเนินการย้อนกลับ
หากต้องการเปิดใช้โฟลว์ย่อยภายในงาน โฮสต์จะตรวจหาเมื่อแอปแสดงScreen
จากสแต็กScreenManager
และอัปเดตโควต้าที่เหลือตามจำนวนเทมเพลตที่แอปย้อนกลับ
ตัวอย่างเช่น หากแอปส่งเทมเพลต 2 รายการขณะอยู่ในหน้าจอ ก จากนั้นพุชหน้าจอ ข และส่งเทมเพลตอีก 2 รายการ แอปจะมีโควต้าเหลืออยู่ 1 รายการ หาก จากนั้นแอปกลับไปที่หน้าจอ ก. อีกครั้ง โฮสต์จะรีเซ็ตโควต้าเป็น 3 เนื่องจาก แอปย้อนกลับไป 2 เทมเพลต
โปรดทราบว่าเมื่อกลับไปที่หน้าจอ แอปจะต้องส่งเทมเพลตที่มี ประเภทเดียวกันกับเทมเพลตที่หน้าจอนั้นส่งล่าสุด การส่งเทมเพลตประเภทอื่นๆ จะทำให้เกิดข้อผิดพลาด อย่างไรก็ตาม ตราบใดที่ประเภทยังคงเหมือนเดิมในระหว่างการดำเนินการย้อนกลับ แอปจะแก้ไขเนื้อหาของเทมเพลตได้อย่างอิสระโดยไม่ส่งผลต่อโควต้า
การดำเนินการรีเซ็ต
เทมเพลตบางรายการมีความหมายพิเศษที่บ่งบอกถึงการสิ้นสุดของงาน ตัวอย่างเช่น
NavigationTemplate
คือมุมมองที่คาดว่าจะยังคงอยู่บนหน้าจอและได้รับการรีเฟรชด้วย
คำแนะนำแบบเลี้ยวต่อเลี้ยวใหม่เพื่อให้ผู้ใช้ได้ดู เมื่อถึงเทมเพลตใดเทมเพลตหนึ่งเหล่านี้
โฮสต์จะรีเซ็ตโควต้าเทมเพลต โดยถือว่าเทมเพลตนั้นเป็น
ขั้นตอนแรกของงานใหม่ ซึ่งจะช่วยให้แอปเริ่มงานใหม่ได้
ดูเอกสารประกอบของเทมเพลตแต่ละรายการเพื่อดูว่าเทมเพลตใดที่ทริกเกอร์การรีเซ็ต
ในโฮสต์
หากโฮสต์ได้รับความตั้งใจที่จะเริ่มแอปจากการดำเนินการในการแจ้งเตือนหรือ จากตัวเรียกใช้ ระบบจะรีเซ็ตโควต้าด้วย กลไกนี้ช่วยให้แอป เริ่มโฟลว์งานใหม่จากการแจ้งเตือนได้ และยังคงใช้ได้แม้ว่าแอปจะ ผูกไว้และอยู่ในเบื้องหน้าแล้วก็ตาม
ดูรายละเอียดเพิ่มเติมเกี่ยวกับวิธีแสดงการแจ้งเตือนของแอปในหน้าจอรถยนต์ได้ที่ส่วนแสดงการแจ้งเตือน ดูข้อมูลเกี่ยวกับวิธีเริ่มแอปจาก การดำเนินการในการแจ้งเตือนได้ที่ส่วนเริ่มแอปในรถยนต์ด้วย Intent
Connection API
คุณสามารถตรวจสอบว่าแอปทำงานบน Android Auto หรือ Android
Automotive OS ได้โดยใช้
CarConnection
API เพื่อ
เรียกข้อมูลการเชื่อมต่อขณะรันไทม์
เช่น ใน Session
ของแอปในรถยนต์ ให้เริ่มต้น CarConnection
และ
สมัครรับข้อมูลอัปเดต LiveData
Kotlin
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
Java
new CarConnection(getCarContext()).getType().observe(this, this::onConnectionStateUpdated);
จากนั้นคุณจะตอบสนองต่อการเปลี่ยนแปลงสถานะการเชื่อมต่อได้ใน Observer
Kotlin
fun onConnectionStateUpdated(connectionState: Int) { val message = when(connectionState) { CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not connected to a head unit" CarConnection.CONNECTION_TYPE_NATIVE -> "Connected to Android Automotive OS" CarConnection.CONNECTION_TYPE_PROJECTION -> "Connected to Android Auto" else -> "Unknown car connection type" } CarToast.makeText(carContext, message, CarToast.LENGTH_SHORT).show() }
Java
private void onConnectionStateUpdated(int connectionState) { String message; switch(connectionState) { case CarConnection.CONNECTION_TYPE_NOT_CONNECTED: message = "Not connected to a head unit"; break; case CarConnection.CONNECTION_TYPE_NATIVE: message = "Connected to Android Automotive OS"; break; case CarConnection.CONNECTION_TYPE_PROJECTION: message = "Connected to Android Auto"; break; default: message = "Unknown car connection type"; break; } CarToast.makeText(getCarContext(), message, CarToast.LENGTH_SHORT).show(); }
Constraints API
รถยนต์แต่ละคันอาจอนุญาตให้แสดงอินสแตนซ์ของ
Item
ต่อผู้ใช้ในแต่ละครั้งได้แตกต่างกัน ใช้
ConstraintManager
เพื่อตรวจสอบขีดจำกัดเนื้อหาขณะรันไทม์และตั้งค่าจำนวนรายการที่เหมาะสม
ในเทมเพลต
เริ่มต้นด้วยการรับ ConstraintManager
จาก CarContext
โดยทำดังนี้
Kotlin
val manager = carContext.getCarService(ConstraintManager::class.java)
Java
ConstraintManager manager = getCarContext().getCarService(ConstraintManager.class);
จากนั้นคุณจะค้นหาออบเจ็กต์ ConstraintManager
ที่ดึงข้อมูลมาเพื่อดูขีดจำกัดเนื้อหาที่เกี่ยวข้องได้
เช่น หากต้องการรับจำนวนรายการที่แสดงใน
ตารางกริด ให้เรียกใช้
getContentLimit
ด้วย
CONTENT_LIMIT_TYPE_GRID
Kotlin
val gridItemLimit = manager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_GRID)
Java
int gridItemLimit = manager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_GRID);
เพิ่มโฟลว์การลงชื่อเข้าใช้
หากแอปมีประสบการณ์การใช้งานแบบลงชื่อเข้าใช้สำหรับผู้ใช้ คุณสามารถใช้เทมเพลต เช่น
SignInTemplate
และ LongMessageTemplate
ที่มี Car App API ระดับ 2 ขึ้นไปเพื่อจัดการการลงชื่อเข้าใช้แอปใน
ยูนิตหลักของรถยนต์
หากต้องการสร้าง SignInTemplate
ให้กำหนด SignInMethod
ปัจจุบัน Car
App Library รองรับวิธีการลงชื่อเข้าใช้ต่อไปนี้
InputSignInMethod
สำหรับการลงชื่อเข้าใช้ด้วยชื่อผู้ใช้/รหัสผ่านPinSignInMethod
สำหรับการลงชื่อเข้าใช้ด้วย PIN ซึ่งผู้ใช้จะลิงก์บัญชีจากโทรศัพท์ โดยใช้ PIN ที่แสดงในยูนิตหลักProviderSignInMethod
สำหรับการลงชื่อเข้าใช้ของผู้ให้บริการ เช่น การลงชื่อเข้าใช้ด้วย Google และ One TapQRCodeSignInMethod
สำหรับการลงชื่อเข้าใช้ด้วยคิวอาร์โค้ด ซึ่งผู้ใช้จะสแกนคิวอาร์โค้ดเพื่อลงชื่อเข้าใช้ให้เสร็จสมบูรณ์ใน โทรศัพท์ ฟีเจอร์นี้พร้อมใช้งานใน Car API ระดับ 4 ขึ้นไป
ตัวอย่างเช่น หากต้องการใช้เทมเพลตที่รวบรวมรหัสผ่านของผู้ใช้ ให้เริ่มต้นด้วยการ
สร้าง InputCallback
เพื่อประมวลผลและตรวจสอบอินพุตของผู้ใช้
Kotlin
val callback = object : InputCallback { override fun onInputSubmitted(text: String) { // You will receive this callback when the user presses Enter on the keyboard. } override fun onInputTextChanged(text: String) { // You will receive this callback as the user is typing. The update // frequency is determined by the host. } }
Java
InputCallback callback = new InputCallback() { @Override public void onInputSubmitted(@NonNull String text) { // You will receive this callback when the user presses Enter on the keyboard. } @Override public void onInputTextChanged(@NonNull String text) { // You will receive this callback as the user is typing. The update // frequency is determined by the host. } };
ต้องระบุ InputCallback
สำหรับ InputSignInMethod
Builder
Kotlin
val passwordInput = InputSignInMethod.Builder(callback) .setHint("Password") .setInputType(InputSignInMethod.INPUT_TYPE_PASSWORD) ... .build()
Java
InputSignInMethod passwordInput = new InputSignInMethod.Builder(callback) .setHint("Password") .setInputType(InputSignInMethod.INPUT_TYPE_PASSWORD) ... .build();
สุดท้าย ให้ใช้ InputSignInMethod
ใหม่เพื่อสร้าง SignInTemplate
Kotlin
SignInTemplate.Builder(passwordInput) .setTitle("Sign in with username and password") .setInstructions("Enter your password") .setHeaderAction(Action.BACK) ... .build()
Java
new SignInTemplate.Builder(passwordInput) .setTitle("Sign in with username and password") .setInstructions("Enter your password") .setHeaderAction(Action.BACK) ... .build();
ใช้ AccountManager
แอป Android Automotive OS ที่มีการตรวจสอบสิทธิ์ต้องใช้ AccountManager ด้วยเหตุผลต่อไปนี้
- UX ที่ดีขึ้นและการจัดการบัญชีที่ง่ายขึ้น: ผู้ใช้สามารถจัดการบัญชีทั้งหมดได้ง่ายๆ จากเมนูบัญชีในการตั้งค่าระบบ ซึ่งรวมถึงการลงชื่อเข้าใช้และลงชื่อออก
- ประสบการณ์การใช้งานแบบ"ผู้ใช้ชั่วคราว": เนื่องจากรถยนต์เป็นอุปกรณ์ที่ใช้ร่วมกัน OEM จึงเปิดใช้ ประสบการณ์การใช้งานแบบผู้ใช้ชั่วคราวในรถยนต์ได้ ซึ่งจะเพิ่มบัญชีไม่ได้
เพิ่มตัวแปรสตริงข้อความ
หน้าจอรถยนต์ขนาดต่างๆ อาจแสดงข้อความในปริมาณที่แตกต่างกัน เมื่อใช้ Car App API
ระดับ 2 ขึ้นไป คุณจะระบุข้อความหลายรูปแบบเพื่อให้
พอดีกับหน้าจอมากที่สุดได้ หากต้องการดูว่าระบบยอมรับข้อความที่แตกต่างกันที่ใด ให้มองหาเทมเพลตและ
คอมโพเนนต์ที่ใช้ CarText
คุณเพิ่มรูปแบบสตริงข้อความลงใน CarText
ได้โดยใช้เมธอด
CarText.Builder.addVariant()
Kotlin
val itemTitle = CarText.Builder("This is a very long string") .addVariant("Shorter string") ... .build()
Java
CarText itemTitle = new CarText.Builder("This is a very long string") .addVariant("Shorter string") ... .build();
จากนั้นคุณจะใช้ CarText
นี้ได้ เช่น เป็นข้อความหลักของ
GridItem
Kotlin
GridItem.Builder() .addTitle(itemTitle) ... .build()
Java
new GridItem.Builder() .addTitle(itemTitle) ... build();
เพิ่มสตริงตามลำดับจากสตริงที่ต้องการมากที่สุดไปน้อยที่สุด เช่น จากสตริงที่ยาวที่สุดไป สั้นที่สุด โฮสต์จะเลือกสตริงที่มีความยาวเหมาะสมตามจำนวนพื้นที่ว่างบนหน้าจอรถยนต์
เพิ่ม CarIcon ในบรรทัดสำหรับแถว
คุณเพิ่มไอคอนในข้อความเพื่อเพิ่มความน่าสนใจของแอปได้โดยใช้
CarIconSpan
ดูข้อมูลเพิ่มเติมเกี่ยวกับการสร้างช่วงเหล่านี้ได้ในเอกสารประกอบสำหรับ
CarIconSpan.create
ดูภาพรวมของวิธีการทำงานของการจัดรูปแบบข้อความด้วยช่วงได้ที่การจัดรูปแบบข้อความ Spantastic ด้วยช่วง
Kotlin
val rating = SpannableString("Rating: 4.5 stars") rating.setSpan( CarIconSpan.create( // Create a CarIcon with an image of four and a half stars CarIcon.Builder(...).build(), // Align the CarIcon to the baseline of the text CarIconSpan.ALIGN_BASELINE ), // The start index of the span (index of the character '4') 8, // The end index of the span (index of the last 's' in "stars") 16, Spanned.SPAN_INCLUSIVE_INCLUSIVE ) val row = Row.Builder() ... .addText(rating) .build()
Java
SpannableString rating = new SpannableString("Rating: 4.5 stars"); rating.setSpan( CarIconSpan.create( // Create a CarIcon with an image of four and a half stars new CarIcon.Builder(...).build(), // Align the CarIcon to the baseline of the text CarIconSpan.ALIGN_BASELINE ), // The start index of the span (index of the character '4') 8, // The end index of the span (index of the last 's' in "stars") 16, Spanned.SPAN_INCLUSIVE_INCLUSIVE ); Row row = new Row.Builder() ... .addText(rating) .build();
API ฮาร์ดแวร์ของรถยนต์
ตั้งแต่ Car App API ระดับ 3 เป็นต้นไป ไลบรารีแอปในรถยนต์จะมี API ที่คุณ ใช้เพื่อเข้าถึงพร็อพเพอร์ตี้และเซ็นเซอร์ของยานพาหนะได้
ข้อกำหนด
หากต้องการใช้ API กับ Android Auto ให้เริ่มต้นด้วยการเพิ่มทรัพยากร Dependency ใน
androidx.car.app:app-projected
ไปยังไฟล์ build.gradle
สำหรับโมดูล Android
Auto สำหรับ Android Automotive OS ให้เพิ่มการอ้างอิงใน
androidx.car.app:app-automotive
ไปยังไฟล์ build.gradle
สำหรับโมดูล Android
Automotive OS
นอกจากนี้ ในไฟล์ AndroidManifest.xml
คุณต้อง
ประกาศสิทธิ์ที่เกี่ยวข้องซึ่งจำเป็นต่อการ
ขอข้อมูลรถยนต์ที่ต้องการใช้ โปรดทราบว่าผู้ใช้ต้องให้สิทธิ์เหล่านี้แก่คุณด้วย คุณสามารถใช้โค้ดเดียวกันได้ทั้งใน Android Auto และ Android Automotive OS โดยไม่ต้องสร้างโฟลว์ที่ขึ้นอยู่กับแพลตฟอร์ม อย่างไรก็ตาม สิทธิ์ที่จำเป็น
จะแตกต่างกัน
CarInfo
ตารางนี้อธิบายพร็อพเพอร์ตี้ที่แสดงโดย API ของ
CarInfo
และสิทธิ์ที่คุณต้องขอเพื่อใช้พร็อพเพอร์ตี้เหล่านั้น
วิธีการ | คุณสมบัติ | สิทธิ์ของ Android Auto | สิทธิ์ของ Android Automotive OS | รองรับตั้งแต่ระดับ API ของแอปในรถยนต์ |
---|---|---|---|---|
fetchModel |
ยี่ห้อ รุ่น ปี | android.car.permission.CAR_INFO |
3 | |
fetchEnergyProfile |
ประเภทหัวชาร์จไฟฟ้า EV, ประเภทเชื้อเพลิง | com.google.android.gms.permission.CAR_FUEL |
android.car.permission.CAR_INFO |
3 |
fetchExteriorDimensions
ข้อมูลนี้ใช้ได้เฉพาะในรถยนต์ Android Automotive OS บางรุ่น ที่ใช้ API 30 ขึ้นไป |
ขนาดภายนอก | ไม่มี | android.car.permission.CAR_INFO |
7 |
addTollListener
removeTollListener |
สถานะบัตรค่าผ่านทาง ประเภทบัตรค่าผ่านทาง | 3 | ||
addEnergyLevelListener
removeEnergyLevelListener |
ระดับแบตเตอรี่ ระดับเชื้อเพลิง ระดับเชื้อเพลิงต่ำ ระยะทางที่เหลือ | com.google.android.gms.permission.CAR_FUEL |
android.car.permission.CAR_ENERGY ,android.car.permission.CAR_ENERGY_PORTS ,android.car.permission.READ_CAR_DISPLAY_UNITS
|
3 |
addSpeedListener
removeSpeedListener |
ความเร็วจริง ความเร็วที่แสดง (แสดงบนแผงหน้าปัดของรถ) | com.google.android.gms.permission.CAR_SPEED |
android.car.permission.CAR_SPEED ,android.car.permission.READ_CAR_DISPLAY_UNITS |
3 |
addMileageListener
removeMileageListener
คำเตือน: วิธีการ |
ระยะทางของเครื่องวัดระยะทาง | com.google.android.gms.permission.CAR_MILEAGE |
ข้อมูลนี้ไม่พร้อมใช้งานใน Android Automotive OS สำหรับแอปที่ติดตั้งจาก Play Store | 3 |
เช่น หากต้องการรับช่วงที่เหลือ ให้สร้างออบเจ็กต์
CarInfo
จากนั้น
สร้างและลงทะเบียน OnCarDataAvailableListener
ดังนี้
Kotlin
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo val listener = OnCarDataAvailableListener<EnergyLevel> { data -> if (data.rangeRemainingMeters.status == CarValue.STATUS_SUCCESS) { val rangeRemaining = data.rangeRemainingMeters.value } else { // Handle error } } carInfo.addEnergyLevelListener(carContext.mainExecutor, listener) … // Unregister the listener when you no longer need updates carInfo.removeEnergyLevelListener(listener)
Java
CarInfo carInfo = getCarContext().getCarService(CarHardwareManager.class).getCarInfo(); OnCarDataAvailableListener<EnergyLevel> listener = (data) -> { if(data.getRangeRemainingMeters().getStatus() == CarValue.STATUS_SUCCESS) { float rangeRemaining = data.getRangeRemainingMeters().getValue(); } else { // Handle error } }; carInfo.addEnergyLevelListener(getCarContext().getMainExecutor(), listener); … // Unregister the listener when you no longer need updates carInfo.removeEnergyLevelListener(listener);
อย่าคิดว่าข้อมูลจากรถจะพร้อมใช้งานตลอดเวลา
หากได้รับข้อผิดพลาด ให้ตรวจสอบสถานะของค่าที่คุณขอเพื่อทำความเข้าใจได้ดียิ่งขึ้นว่าเหตุใดจึงไม่สามารถดึงข้อมูลที่คุณขอได้ ดูคำจำกัดความของคลาส CarInfo
ทั้งหมดได้ในเอกสารอ้างอิง
CarSensors
คลาส CarSensors
ช่วยให้คุณเข้าถึงตัวตรวจวัดความเร่ง เครื่องวัดการหมุน เข็มทิศ และ
ข้อมูลตำแหน่งของรถได้ ความพร้อมใช้งานของค่าเหล่านี้อาจขึ้นอยู่กับ
OEM รูปแบบของข้อมูลจากตัวตรวจวัดความเร่ง เครื่องวัดการหมุน และเข็มทิศจะเหมือนกับที่คุณได้รับจาก SensorManager
API
เช่น หากต้องการตรวจสอบทิศทางของยานพาหนะ ให้ทำดังนี้
Kotlin
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors val listener = OnCarDataAvailableListener<Compass> { data -> if (data.orientations.status == CarValue.STATUS_SUCCESS) { val orientation = data.orientations.value } else { // Data not available, handle error } } carSensors.addCompassListener(CarSensors.UPDATE_RATE_NORMAL, carContext.mainExecutor, listener) … // Unregister the listener when you no longer need updates carSensors.removeCompassListener(listener)
Java
CarSensors carSensors = getCarContext().getCarService(CarHardwareManager.class).getCarSensors(); OnCarDataAvailableListener<Compass> listener = (data) -> { if (data.getOrientations().getStatus() == CarValue.STATUS_SUCCESS) { List<Float> orientations = data.getOrientations().getValue(); } else { // Data not available, handle error } }; carSensors.addCompassListener(CarSensors.UPDATE_RATE_NORMAL, getCarContext().getMainExecutor(), listener); … // Unregister the listener when you no longer need updates carSensors.removeCompassListener(listener);
หากต้องการเข้าถึงข้อมูลตำแหน่งจากรถยนต์ คุณต้องประกาศและขอสิทธิ์ android.permission.ACCESS_FINE_LOCATION
ด้วย
การทดสอบ
หากต้องการจำลองข้อมูลเซ็นเซอร์เมื่อทดสอบใน Android Auto โปรดดูส่วนเซ็นเซอร์และการกำหนดค่าเซ็นเซอร์ใน คู่มือเครื่องเล่นวิทยุบนเดสก์ท็อป หากต้องการจำลองข้อมูลเซ็นเซอร์เมื่อทดสอบใน Android Automotive OS โปรดดูส่วนจำลองสถานะฮาร์ดแวร์ในคู่มือโปรแกรมจำลอง Android Automotive OS
วงจร CarAppService, เซสชัน และหน้าจอ
คลาส Session
และ
Screen
จะใช้
อินเทอร์เฟซ LifecycleOwner
เมื่อผู้ใช้โต้ตอบกับแอป ระบบจะเรียกใช้การเรียกกลับของวงจรSession
และออบเจ็กต์ Screen
ตามที่อธิบายไว้ในไดอะแกรมต่อไปนี้
วงจรของ CarAppService และเซสชัน

Session
วงจรการใช้งานดูรายละเอียดทั้งหมดได้ในเอกสารประกอบสำหรับเมธอด
Session.getLifecycle
วงจรการใช้งานของหน้าจอ

Screen
วงจรการใช้งานดูรายละเอียดทั้งหมดได้ในเอกสารประกอบสำหรับเมธอด
Screen.getLifecycle
บันทึกจากไมโครโฟนในรถ
การใช้ CarAppService
ของแอปและ API ของ CarAudioRecord
จะช่วยให้แอปเข้าถึงไมโครโฟนในรถของผู้ใช้ได้ ผู้ใช้ต้องให้สิทธิ์แอปของคุณในการเข้าถึงไมโครโฟนของรถ แอปของคุณสามารถบันทึกและ
ประมวลผลข้อมูลที่ผู้ใช้ป้อนภายในแอปได้
สิทธิ์ในการบันทึก
ก่อนบันทึกเสียง คุณต้องประกาศสิทธิ์ในการบันทึกใน AndroidManifest.xml
ก่อน และขอให้ผู้ใช้ให้สิทธิ์ดังกล่าว
<manifest ...>
...
<uses-permission android:name="android.permission.RECORD_AUDIO" />
...
</manifest>
คุณต้องขอสิทธิ์ในการบันทึกที่รันไทม์ ดูรายละเอียดเกี่ยวกับวิธีขอสิทธิ์ในแอปในรถยนต์ได้ที่ส่วนขอสิทธิ์
บันทึกเสียง
หลังจากที่ผู้ใช้ให้สิทธิ์บันทึกแล้ว คุณจะบันทึกเสียงและประมวลผล การบันทึกได้
Kotlin
val carAudioRecord = CarAudioRecord.create(carContext) carAudioRecord.startRecording() val data = ByteArray(CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE) while(carAudioRecord.read(data, 0, CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE) >= 0) { // Use data array // Potentially call carAudioRecord.stopRecording() if your processing finds end of speech } carAudioRecord.stopRecording()
Java
CarAudioRecord carAudioRecord = CarAudioRecord.create(getCarContext()); carAudioRecord.startRecording(); byte[] data = new byte[CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE]; while (carAudioRecord.read(data, 0, CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE) >= 0) { // Use data array // Potentially call carAudioRecord.stopRecording() if your processing finds end of speech } carAudioRecord.stopRecording();
โฟกัสอัตโนมัติ
เมื่อบันทึกจากไมโครโฟนในรถยนต์ ให้รับโฟกัสเสียงก่อนเพื่อ ให้แน่ใจว่าระบบจะหยุดสื่อที่กำลังเล่นอยู่ หากสูญเสียโฟกัสเสียง ให้ หยุดบันทึก
ตัวอย่างวิธีรับโฟกัสเสียงมีดังนี้
Kotlin
val carAudioRecord = CarAudioRecord.create(carContext) // Take audio focus so that user's media is not recorded val audioAttributes = AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) // Use the most appropriate usage type for your use case .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE) .build() val audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) .setAudioAttributes(audioAttributes) .setOnAudioFocusChangeListener { state: Int -> if (state == AudioManager.AUDIOFOCUS_LOSS) { // Stop recording if audio focus is lost carAudioRecord.stopRecording() } } .build() if (carContext.getSystemService(AudioManager::class.java) .requestAudioFocus(audioFocusRequest) != AudioManager.AUDIOFOCUS_REQUEST_GRANTED ) { // Don't record if the focus isn't granted return } carAudioRecord.startRecording() // Process the audio and abandon the AudioFocusRequest when done
Java
CarAudioRecord carAudioRecord = CarAudioRecord.create(getCarContext()); // Take audio focus so that user's media is not recorded AudioAttributes audioAttributes = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) // Use the most appropriate usage type for your use case .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE) .build(); AudioFocusRequest audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) .setAudioAttributes(audioAttributes) .setOnAudioFocusChangeListener(state -> { if (state == AudioManager.AUDIOFOCUS_LOSS) { // Stop recording if audio focus is lost carAudioRecord.stopRecording(); } }) .build(); if (getCarContext().getSystemService(AudioManager.class).requestAudioFocus(audioFocusRequest) != AUDIOFOCUS_REQUEST_GRANTED) { // Don't record if the focus isn't granted return; } carAudioRecord.startRecording(); // Process the audio and abandon the AudioFocusRequest when done
ไลบรารีการทดสอบ
ไลบรารี
การทดสอบของ Android for Cars มีคลาสเสริม
ที่คุณใช้เพื่อตรวจสอบลักษณะการทำงานของแอปในสภาพแวดล้อมการทดสอบได้
เช่น
SessionController
ช่วยให้คุณจำลองการเชื่อมต่อกับโฮสต์และยืนยันว่ามีการสร้างและ
ส่งคืนScreen
และ
Template
ที่ถูกต้อง
ดูตัวอย่างการใช้งานได้ที่ ตัวอย่าง
รายงานปัญหาเกี่ยวกับไลบรารีแอป Android สำหรับรถยนต์
หากพบปัญหาเกี่ยวกับไลบรารี ให้รายงานโดยใช้เครื่องมือติดตามปัญหาของ Google โปรดกรอกข้อมูลที่ขอทั้งหมดในเทมเพลตปัญหา
ก่อนยื่นปัญหาใหม่ โปรดตรวจสอบว่าปัญหาดังกล่าวอยู่ในหมายเหตุประจำรุ่นของไลบรารี หรือมีการรายงานในรายการปัญหาแล้ว คุณสามารถติดตามและโหวตปัญหาได้โดย คลิกดาวสำหรับปัญหาในเครื่องมือติดตาม ดูข้อมูลเพิ่มเติมได้ที่ การติดตามปัญหา