หัวข้อนี้จะแสดงวิธีตั้งค่า Use Case ของ CameraX ภายในแอปเพื่อให้ได้รูปภาพที่มีข้อมูลการหมุนที่ถูกต้อง ไม่ว่าจะมาจาก Use Case ImageAnalysis หรือ ImageCapture ดังนั้น
Analyzerของกรณีการใช้งานImageAnalysisควรได้รับเฟรมที่มีการหมุนที่ถูกต้อง- กรณีการใช้งาน
ImageCaptureควรถ่ายภาพโดยมีการหมุนที่ถูกต้อง
คำศัพท์
หัวข้อนี้ใช้คำศัพท์ต่อไปนี้ ดังนั้นการทำความเข้าใจความหมายของแต่ละคำจึงเป็นสิ่งสำคัญ
- การวางแนวจอแสดงผล
- ค่านี้หมายถึงด้านของอุปกรณ์ที่อยู่ในตำแหน่งหงายขึ้น และอาจเป็นค่าใดค่าหนึ่งจาก 4 ค่า ได้แก่ แนวตั้ง แนวนอน แนวตั้งกลับด้าน หรือแนวนอนกลับด้าน
- การหมุนจอแสดงผล
- ค่านี้เป็นค่าที่แสดงโดย
Display.getRotation()และแสดงองศาที่อุปกรณ์หมุนทวนเข็มนาฬิกาจากการวางแนวตามปกติ - การหมุนเป้าหมาย
- ค่านี้แสดงจำนวนองศาที่ต้องหมุนอุปกรณ์ตามเข็มนาฬิกาเพื่อให้ได้การวางแนวตามปกติ
วิธีกำหนดการหมุนเวียนเป้าหมาย
ตัวอย่างต่อไปนี้แสดงวิธีกำหนดการหมุนเป้าหมายสำหรับอุปกรณ์ตามการวางแนวตามธรรมชาติ
ตัวอย่างที่ 1: การวางแนวตั้งตามปกติ
| ตัวอย่างอุปกรณ์: Pixel 3 XL | |
|---|---|
|
การวางแนวปกติ = แนวตั้ง การหมุนจอแสดงผล = 0 |
|
|
การวางแนวตามปกติ = แนวตั้ง การหมุนจอแสดงผล = 90 |
|
ตัวอย่างที่ 2: การวางแนวตามธรรมชาติแบบแนวนอน
| ตัวอย่างอุปกรณ์: Pixel C | |
|---|---|
|
การวางแนวตามปกติ = แนวนอน การหมุนจอแสดงผล = 0 |
|
|
การวางแนวตามธรรมชาติ = แนวนอน การหมุนจอแสดงผล = 270 |
|
การหมุนรูปภาพ
ด้านไหนอยู่ด้านบน การวางแนวเซ็นเซอร์จะกำหนดไว้ใน Android เป็นค่าคงที่ ซึ่งแสดงถึงองศา (0, 90, 180, 270) ที่เซ็นเซอร์หมุนจากด้านบนของอุปกรณ์เมื่ออุปกรณ์อยู่ในตำแหน่งปกติ สำหรับทุกกรณีในแผนภาพ การหมุนรูปภาพจะอธิบายวิธีหมุนข้อมูลตามเข็มนาฬิกาเพื่อให้ปรากฏในแนวตั้ง
ตัวอย่างต่อไปนี้แสดงให้เห็นว่าการหมุนรูปภาพควรเป็นอย่างไร โดยขึ้นอยู่กับการวางแนวเซ็นเซอร์ตรวจจับผ่านกล้อง และยังถือว่าการหมุนเป้าหมายได้รับการตั้งค่าเป็นการหมุนจอแสดงผลด้วย
ตัวอย่างที่ 1: เซ็นเซอร์หมุน 90 องศา
| ตัวอย่างอุปกรณ์: Pixel 3 XL | |
|---|---|
|
การหมุนจอแสดงผล = 0 |
|
|
การหมุนจอแสดงผล = 90 |
|
ตัวอย่างที่ 2: เซ็นเซอร์หมุน 270 องศา
| ตัวอย่างอุปกรณ์: Nexus 5X | |
|---|---|
|
การหมุนจอแสดงผล = 0 |
|
|
การหมุนจอแสดงผล = 90 |
|
ตัวอย่างที่ 3: เซ็นเซอร์หมุน 0 องศา
| ตัวอย่างอุปกรณ์: Pixel C (แท็บเล็ต) | |
|---|---|
|
การหมุนจอแสดงผล = 0 |
|
|
การหมุนจอแสดงผล = 270 |
|
กำลังคำนวณการหมุนของรูปภาพ
ImageAnalysis
Analyzer ของ ImageAnalysis จะได้รับรูปภาพจากกล้องในรูปแบบImageProxy รูปภาพแต่ละรูปจะมีข้อมูลการหมุน ซึ่งเข้าถึงได้ผ่านทาง:
val rotation = imageProxy.imageInfo.rotationDegrees
ค่านี้แสดงองศาที่ต้องหมุนรูปภาพตามเข็มนาฬิกาเพื่อให้ตรงกับการหมุนเป้าหมายของ ImageAnalysis ในบริบทของแอป Android การหมุนเป้าหมายของ ImageAnalysis มักจะตรงกับการวางแนวของหน้าจอ
ImageCapture
ระบบจะแนบ Callback กับอินสแตนซ์ ImageCapture เพื่อส่งสัญญาณเมื่อผลลัพธ์การจับภาพพร้อมใช้งาน ผลลัพธ์อาจเป็นรูปภาพที่จับภาพไว้หรือข้อผิดพลาด
เมื่อถ่ายภาพ Callback ที่ระบุอาจเป็นประเภทใดประเภทหนึ่งต่อไปนี้
OnImageCapturedCallback: รับรูปภาพที่มีสิทธิ์เข้าถึงในหน่วยความจำในรูปแบบImageProxyOnImageSavedCallback: เรียกใช้เมื่อจัดเก็บรูปภาพที่ถ่ายไว้ในตำแหน่งที่ระบุโดยImageCapture.OutputFileOptionsเรียบร้อยแล้ว ตัวเลือกสามารถระบุFile,OutputStreamหรือตำแหน่งในMediaStore
การหมุนของรูปภาพที่ถ่าย ไม่ว่าจะเป็นรูปแบบใด (ImageProxy, File, OutputStream, MediaStore Uri) จะแสดงองศาการหมุนที่ต้องหมุนรูปภาพที่ถ่ายตามเข็มนาฬิกาเพื่อให้ตรงกับการหมุนเป้าหมายของ ImageCapture ซึ่งในบริบทของแอป Android มักจะตรงกับการวางแนวของหน้าจอ
คุณสามารถดึงข้อมูลการหมุนของรูปภาพที่ถ่ายได้ด้วยวิธีใดวิธีหนึ่งต่อไปนี้
ImageProxy
val rotation = imageProxy.imageInfo.rotationDegrees
File
val exif = Exif.createFromFile(file) val rotation = exif.rotation
OutputStream
val byteArray = outputStream.toByteArray() val exif = Exif.createFromInputStream(ByteArrayInputStream(byteArray)) val rotation = exif.rotation
MediaStore uri
val inputStream = contentResolver.openInputStream(outputFileResults.savedUri) val exif = Exif.createFromInputStream(inputStream) val rotation = exif.rotation
ยืนยันการหมุนรูปภาพ
กรณีการใช้งาน ImageAnalysis และ ImageCapture จะได้รับ ImageProxy จากกล้องหลังจากส่งคำขอจับภาพสำเร็จ ImageProxy จะรวมรูปภาพและข้อมูลเกี่ยวกับรูปภาพ รวมถึงการหมุน ข้อมูลการหมุนนี้แสดงถึงองศาที่ต้องหมุนรูปภาพเพื่อให้ตรงกับการหมุนเป้าหมายของ Use Case
หลักเกณฑ์การหมุนเป้าหมาย ImageCapture/ImageAnalysis
เนื่องจากอุปกรณ์จำนวนมากไม่หมุนเป็นแนวตั้งกลับด้านหรือแนวนอนกลับด้านโดยค่าเริ่มต้น แอป Android บางแอปจึงไม่รองรับการวางแนวเหล่านี้ การที่แอปรองรับหรือไม่รองรับจะเปลี่ยนวิธีอัปเดตการหมุนเวียนเป้าหมายของ Use Case
ด้านล่างนี้คือตาราง 2 ตารางที่กำหนดวิธีซิงค์การหมุนเป้าหมายของ Use Case กับการหมุนจอแสดงผล วิธีแรกแสดงวิธีดำเนินการขณะที่รองรับการวางแนวทั้ง 4 แบบ ส่วนวิธีที่ 2 จะจัดการเฉพาะการวางแนวที่อุปกรณ์หมุนไปโดยค่าเริ่มต้น
วิธีเลือกหลักเกณฑ์ที่จะปฏิบัติตามในแอป
ตรวจสอบว่ากล้อง
Activityของแอปมีการล็อกการวางแนว ไม่ล็อกการวางแนว หรือลบล้างการเปลี่ยนแปลงการกำหนดค่าการวางแนวหรือไม่ตัดสินใจว่ากล้องของแอป
Activityควรจัดการการวางแนวอุปกรณ์ทั้ง 4 แบบ (แนวตั้ง แนวตั้งกลับด้าน แนวนอน และแนวนอนกลับด้าน) หรือควรจัดการเฉพาะการวางแนวที่อุปกรณ์ที่แอปทำงานอยู่รองรับโดยค่าเริ่มต้น
รองรับการวางแนวทั้ง 4 แบบ
ตารางนี้จะระบุหลักเกณฑ์บางอย่างที่ควรปฏิบัติตามในกรณีที่อุปกรณ์ไม่หมุนเป็นแนวตั้งแบบกลับหัว และยังใช้กับอุปกรณ์ที่หมุนเป็นแนวนอนกลับด้านไม่ได้ด้วย
| สถานการณ์ | หลักเกณฑ์ | โหมดหน้าต่างเดียว | โหมดแยกหน้าจอแบบหลายหน้าต่าง |
|---|---|---|---|
| การวางแนวแบบปลดล็อก |
ตั้งค่ากรณีการใช้งานทุกครั้งที่สร้าง Activity เช่น ใน Callback onCreate() ของ Activity
|
||
ใช้onOrientationChanged()ของ OrientationEventListener
ภายใน Callback ให้อัปเดตการหมุนเป้าหมายของ Use Case ซึ่งจะจัดการกรณีที่ระบบไม่สร้าง Activity ขึ้นมาใหม่แม้ว่าจะมีการเปลี่ยนการวางแนว เช่น เมื่อหมุนอุปกรณ์ 180 องศา
|
นอกจากนี้ยังจัดการเมื่อจอแสดงผลอยู่ในแนวตั้งแบบกลับหัวและอุปกรณ์ไม่หมุนเป็นแนวตั้งแบบกลับหัวโดยค่าเริ่มต้น |
นอกจากนี้ยังจัดการกรณีที่ไม่มีการสร้าง Activity ขึ้นมาใหม่เมื่ออุปกรณ์หมุน (เช่น 90 องศา) ซึ่งจะเกิดขึ้นในอุปกรณ์รูปแบบขนาดเล็กเมื่อแอปใช้พื้นที่ครึ่งหน้าจอ และในอุปกรณ์ขนาดใหญ่เมื่อแอปใช้พื้นที่ 2 ใน 3 ของหน้าจอ
|
|
ไม่บังคับ: ตั้งค่าพร็อพเพอร์ตี้ screenOrientation ของ Activity เป็น fullSensor ในไฟล์ AndroidManifest
|
ซึ่งจะช่วยให้ UI ตั้งตรงเมื่ออุปกรณ์อยู่ในแนวตั้งแบบกลับหัว และช่วยให้ระบบสร้าง Activity ขึ้นมาใหม่ได้ทุกครั้งที่หมุนอุปกรณ์ 90 องศา
|
ไม่มีผลกับอุปกรณ์ที่ไม่หมุนเป็นแนวตั้งแบบกลับหัวโดยค่าเริ่มต้น ระบบไม่รองรับโหมดหลายหน้าต่างขณะที่จอแสดงผลอยู่ในแนวตั้งแบบกลับด้าน | |
| ล็อกการวางแนว |
ตั้งค่า Use Case เพียงครั้งเดียวเมื่อสร้าง Activity เป็นครั้งแรก เช่น ใน Callback onCreate() ของ Activity
|
||
ใช้onOrientationChanged()ของ OrientationEventListener
ภายใน Callback ให้อัปเดตการหมุนเป้าหมายของ Use Case ยกเว้นตัวอย่าง
|
นอกจากนี้ยังจัดการกรณีที่ไม่มีการสร้าง Activity ขึ้นมาใหม่เมื่ออุปกรณ์หมุน (เช่น 90 องศา) ซึ่งจะเกิดขึ้นในอุปกรณ์รูปแบบขนาดเล็กเมื่อแอปใช้พื้นที่ครึ่งหน้าจอ และในอุปกรณ์ขนาดใหญ่เมื่อแอปใช้พื้นที่ 2 ใน 3 ของหน้าจอ
|
||
| ลบล้าง configChanges ของการวางแนว |
ตั้งค่า Use Case เพียงครั้งเดียวเมื่อสร้าง Activity เป็นครั้งแรก เช่น ใน Callback onCreate() ของ Activity
|
||
ใช้onOrientationChanged()ของ OrientationEventListener
ภายใน Callback ให้อัปเดตการหมุนเป้าหมายของ Use Case
|
นอกจากนี้ยังจัดการกรณีที่ไม่มีการสร้าง Activity ขึ้นมาใหม่เมื่ออุปกรณ์หมุน (เช่น 90 องศา) ซึ่งจะเกิดขึ้นในอุปกรณ์รูปแบบขนาดเล็กเมื่อแอปใช้พื้นที่ครึ่งหน้าจอ และในอุปกรณ์ขนาดใหญ่เมื่อแอปใช้พื้นที่ 2 ใน 3 ของหน้าจอ
|
||
| ไม่บังคับ: ตั้งค่าพร็อพเพอร์ตี้ screenOrientation ของกิจกรรมเป็น fullSensor ในไฟล์ AndroidManifest | ช่วยให้ UI ตั้งตรงเมื่ออุปกรณ์อยู่ในแนวตั้งแบบกลับหัว | ไม่มีผลกับอุปกรณ์ที่ไม่หมุนเป็นแนวตั้งแบบกลับหัวโดยค่าเริ่มต้น ระบบไม่รองรับโหมดหลายหน้าต่างขณะที่จอแสดงผลอยู่ในแนวตั้งแบบกลับด้าน |
รองรับเฉพาะการวางแนวที่อุปกรณ์รองรับ
รองรับเฉพาะการวางแนวที่อุปกรณ์รองรับโดยค่าเริ่มต้น (ซึ่งอาจรวมหรือไม่รวมแนวตั้งกลับด้าน/แนวนอนกลับด้าน)
| สถานการณ์ | หลักเกณฑ์ | โหมดแยกหน้าจอแบบหลายหน้าต่าง |
|---|---|---|
| การวางแนวแบบปลดล็อก |
ตั้งค่ากรณีการใช้งานทุกครั้งที่สร้าง Activity เช่น ใน Callback onCreate() ของ Activity
|
|
ใช้ onDisplayChanged() ของ DisplayListener ภายใน Callback ให้อัปเดตการหมุนเป้าหมายของ Use Case เช่น เมื่ออุปกรณ์หมุน 180 องศา
|
นอกจากนี้ยังจัดการกรณีที่ไม่มีการสร้าง Activity ขึ้นมาใหม่เมื่ออุปกรณ์หมุน (เช่น 90 องศา) ซึ่งจะเกิดขึ้นในอุปกรณ์รูปแบบขนาดเล็กเมื่อแอปใช้พื้นที่ครึ่งหน้าจอ และในอุปกรณ์ขนาดใหญ่เมื่อแอปใช้พื้นที่ 2 ใน 3 ของหน้าจอ
|
|
| ล็อกการวางแนว |
ตั้งค่า Use Case เพียงครั้งเดียวเมื่อสร้าง Activity เป็นครั้งแรก เช่น ใน Callback onCreate() ของ Activity
|
|
ใช้onOrientationChanged()ของ OrientationEventListener
ภายใน Callback ให้อัปเดตการหมุนเป้าหมายของ Use Case
|
นอกจากนี้ยังจัดการกรณีที่ไม่มีการสร้าง Activity ขึ้นมาใหม่เมื่ออุปกรณ์หมุน (เช่น 90 องศา) ซึ่งจะเกิดขึ้นในอุปกรณ์รูปแบบขนาดเล็กเมื่อแอปใช้พื้นที่ครึ่งหน้าจอ และในอุปกรณ์ขนาดใหญ่เมื่อแอปใช้พื้นที่ 2 ใน 3 ของหน้าจอ
|
|
| ลบล้าง configChanges ของการวางแนว |
ตั้งค่า Use Case เพียงครั้งเดียวเมื่อสร้าง Activity เป็นครั้งแรก เช่น ใน Callback onCreate() ของ Activity
|
|
ใช้ onDisplayChanged() ของ DisplayListener ภายใน Callback ให้อัปเดตการหมุนเป้าหมายของ Use Case เช่น เมื่อหมุนอุปกรณ์ 180 องศา
|
นอกจากนี้ยังจัดการกรณีที่ไม่มีการสร้าง Activity ขึ้นมาใหม่เมื่ออุปกรณ์หมุน (เช่น 90 องศา) ซึ่งจะเกิดขึ้นในอุปกรณ์รูปแบบขนาดเล็กเมื่อแอปใช้พื้นที่ครึ่งหน้าจอ และในอุปกรณ์ขนาดใหญ่เมื่อแอปใช้พื้นที่ 2 ใน 3 ของหน้าจอ
|
การวางแนวที่ปลดล็อก
Activity จะมีการวางแนวที่ปลดล็อกเมื่อการวางแนวการแสดงผล (เช่น แนวตั้งหรือแนวนอน) ตรงกับการวางแนวทางกายภาพของอุปกรณ์ ยกเว้นแนวตั้ง/แนวนอนแบบกลับด้าน ซึ่งอุปกรณ์บางรุ่นไม่รองรับโดยค่าเริ่มต้น หากต้องการบังคับให้อุปกรณ์หมุนไปทั้ง 4 ทิศทาง ให้ตั้งค่าพร็อพเพอร์ตี้ screenOrientation ของ Activity เป็น fullSensor
ในโหมดหลายหน้าต่าง อุปกรณ์ที่ไม่รองรับแนวตั้ง/แนวนอนแบบกลับด้านโดยค่าเริ่มต้นจะไม่หมุนเป็นแนวตั้ง/แนวนอนแบบกลับด้าน แม้ว่าพร็อพเพอร์ตี้ screenOrientation จะตั้งค่าเป็น fullSensor ก็ตาม
<!-- The Activity has an unlocked orientation, but might not rotate to reverse portrait/landscape in single-window mode if the device doesn't support it by default. --> <activity android:name=".UnlockedOrientationActivity" /> <!-- The Activity has an unlocked orientation, and will rotate to all four orientations in single-window mode. --> <activity android:name=".UnlockedOrientationActivity" android:screenOrientation="fullSensor" />
ล็อกการวางแนว
จอแสดงผลจะมีการวางแนวที่ล็อกไว้เมื่อยังคงอยู่ในการวางแนวการแสดงผลเดียวกัน (เช่น แนวตั้งหรือแนวนอน) ไม่ว่าอุปกรณ์จะมีการวางแนวทางกายภาพอย่างไรก็ตาม ซึ่งทำได้โดยระบุพร็อพเพอร์ตี้ screenOrientation ของ Activity ไว้ในการประกาศในไฟล์ AndroidManifest.xml
เมื่อจอแสดงผลมีการล็อกการวางแนว ระบบจะไม่ทำลายและสร้าง Activity ขึ้นมาใหม่เมื่อหมุนอุปกรณ์
<!-- The Activity keeps a portrait orientation even as the device rotates. --> <activity android:name=".LockedOrientationActivity" android:screenOrientation="portrait" />
ลบล้างการเปลี่ยนแปลงการกำหนดค่าการวางแนวแล้ว
เมื่อ Activity ลบล้างการเปลี่ยนแปลงการกำหนดค่าการวางแนว ระบบจะไม่ทำลายและสร้างใหม่เมื่อการวางแนวทางกายภาพของอุปกรณ์เปลี่ยนแปลง
อย่างไรก็ตาม ระบบจะอัปเดต UI ให้ตรงกับการวางแนวอุปกรณ์
<!-- The Activity's UI might not rotate in reverse portrait/landscape if the device doesn't support it by default. --> <activity android:name=".OrientationConfigChangesOverriddenActivity" android:configChanges="orientation|screenSize" /> <!-- The Activity's UI will rotate to all 4 orientations in single-window mode. --> <activity android:name=".OrientationConfigChangesOverriddenActivity" android:configChanges="orientation|screenSize" android:screenOrientation="fullSensor" />
การตั้งค่ากรณีการใช้งานกล้อง
ในสถานการณ์ที่อธิบายไว้ข้างต้น คุณสามารถตั้งค่า Use Case ของกล้องได้เมื่อสร้าง Activity เป็นครั้งแรก
ในกรณีของ Activity ที่มีการวางแนวแบบปลดล็อก การตั้งค่านี้จะดำเนินการทุกครั้งที่มีการหมุนอุปกรณ์ เนื่องจากระบบจะทำลายและสร้าง Activity ขึ้นมาใหม่เมื่อมีการเปลี่ยนแปลงการวางแนว ซึ่งส่งผลให้ Use Case ตั้งค่าการหมุนเป้าหมายให้ตรงกับการวางแนวของจอแสดงผลโดยค่าเริ่มต้นในแต่ละครั้ง
ในกรณีที่ Activity มีการล็อกการวางแนวหรือลบล้างการเปลี่ยนแปลงการกำหนดค่าการวางแนว การตั้งค่านี้จะทำเพียงครั้งเดียวเมื่อสร้าง Activity เป็นครั้งแรก
class CameraActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val cameraProcessFuture = ProcessCameraProvider.getInstance(this) cameraProcessFuture.addListener( Runnable { val cameraProvider = cameraProcessFuture.get() // By default, the use cases set their target rotation to match the // display’s rotation. val preview = buildPreview() val imageAnalysis = buildImageAnalysis() val imageCapture = buildImageCapture() cameraProvider.bindToLifecycle( this, cameraSelector, preview, imageAnalysis, imageCapture ) }, mainExecutor ) } }
การตั้งค่า OrientationEventListener
การใช้ OrientationEventListener ช่วยให้คุณอัปเดตการหมุนเป้าหมายของ Use Case ของกล้องได้อย่างต่อเนื่องเมื่อการวางแนวของอุปกรณ์เปลี่ยนแปลง
class CameraActivity : AppCompatActivity() { private val orientationEventListener by lazy { object : OrientationEventListener(this) { override fun onOrientationChanged(orientation: Int) { if (orientation == ORIENTATION_UNKNOWN) { return } val rotation = when (orientation) { in 45 until 135 -> Surface.ROTATION_270 in 135 until 225 -> Surface.ROTATION_180 in 225 until 315 -> Surface.ROTATION_90 else -> Surface.ROTATION_0 } imageAnalysis.targetRotation = rotation imageCapture.targetRotation = rotation } } } override fun onStart() { super.onStart() orientationEventListener.enable() } override fun onStop() { super.onStop() orientationEventListener.disable() } }
การตั้งค่า DisplayListener
การใช้ DisplayListener ช่วยให้คุณอัปเดตการหมุนเป้าหมายของ Use Case ของกล้องในบางสถานการณ์ได้ เช่น เมื่อระบบไม่ทำลายและสร้าง Activity ขึ้นมาใหม่หลังจากที่อุปกรณ์หมุน 180 องศา
class CameraActivity : AppCompatActivity() { private val displayListener = object : DisplayManager.DisplayListener { override fun onDisplayChanged(displayId: Int) { if (rootView.display.displayId == displayId) { val rotation = rootView.display.rotation imageAnalysis.targetRotation = rotation imageCapture.targetRotation = rotation } } override fun onDisplayAdded(displayId: Int) { } override fun onDisplayRemoved(displayId: Int) { } } override fun onStart() { super.onStart() val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager displayManager.registerDisplayListener(displayListener, null) } override fun onStop() { super.onStop() val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager displayManager.unregisterDisplayListener(displayListener) } }