Các chế độ xoay trong trường hợp sử dụng CameraX

Chủ đề này trình bày cách thiết lập trường hợp sử dụng CameraX bên trong ứng dụng để nhận được hình ảnh với thông tin xoay chính xác, cho dù đó là từ trường hợp sử dụng ImageAnalysis hay ImageCapture. Vì vậy:

  • Analyzer của trường hợp sử dụng ImageAnalysis sẽ nhận được khung với chế độ xoay chính xác.
  • Trường hợp sử dụng ImageCapture phải chụp ảnh với chế độ xoay chính xác.

Thuật ngữ

Chủ đề này sử dụng các thuật ngữ sau, do vậy, việc hiểu rõ nghĩa từng thuật ngữ là rất quan trọng:

Hướng của màn hình
Cụm từ này muốn chỉ cạnh nào của thiết bị đang ở vị trí hướng lên và có thể là một trong bốn giá trị: dọc, ngang, dọc lộn ngược hoặc ngang lộn ngược.
Độ xoay màn hình
Đây là giá trị được Display.getRotation() trả về và cho biết số độ mà thiết bị được xoay ngược chiều kim đồng hồ từ hướng tự nhiên của thiết bị.
Độ xoay mục tiêu
Cụm từ này cho biết số độ cần xoay theo chiều kim đồng hồ để thiết bị trở về hướng tự nhiên.

Cách xác định độ xoay mục tiêu

Các ví dụ dưới đây cho thấy cách để xác định độ xoay mục tiêu cho thiết bị dựa trên hướng tự nhiên của thiết bị.

Ví dụ 1: Hướng tự nhiên theo chiều dọc

Ví dụ về thiết bị: Pixel 3 XL

Hướng tự nhiên = Chiều dọc
Hướng hiện tại = Chiều dọc

Độ xoay màn hình = 0
Độ xoay mục tiêu = 0

Hướng tự nhiên = Chiều dọc
Hướng hiện tại = Chiều ngang

Độ xoay màn hình = 90
Độ xoay mục tiêu = 90

Ví dụ 2: Hướng tự nhiên theo chiều ngang

Ví dụ về thiết bị: Pixel C

Hướng tự nhiên = Chiều ngang
Hướng hiện tại = Chiều ngang

Độ xoay màn hình = 0
Độ xoay mục tiêu = 0

Hướng tự nhiên = Chiều ngang
Hướng hiện tại = Chiều dọc

Độ xoay màn hình = 270
Độ xoay mục tiêu = 270

Độ xoay hình ảnh

Đầu nào đang hướng lên? Hướng cảm biến được xác định trong Android là một giá trị không đổi. Giá trị này biểu thị số độ (0, 90, 180, 270) mà cảm biến xoay tính từ đầu thiết bị khi thiết bị đang ở vị trí tự nhiên. Đối với tất cả các trường hợp trong sơ đồ, độ xoay hình ảnh mô tả cách dữ liệu nên được xoay theo chiều kim đồng hồ để hiển thị theo chiều thẳng đứng.

Các ví dụ sau đây cho thấy độ xoay hình ảnh nên là gì tuỳ thuộc vào hướng cảm biến của camera. Các ví dụ này cũng giả định rằng độ xoay mục tiêu được đặt là độ xoay màn hình.

Ví dụ 1: Cảm biến được xoay 90 độ

Ví dụ về thiết bị: Pixel 3 XL

Độ xoay màn hình = 0
Hướng màn hình = Chiều dọc
Độ xoay hình ảnh = 90

Độ xoay màn hình = 90
Hướng màn hình = Chiều ngang
Độ xoay hình ảnh = 0

Ví dụ 2: Cảm biến được xoay 270 độ

Ví dụ về thiết bị: Nexus 5X

Độ xoay màn hình = 0
Hướng màn hình = Chiều dọc
Độ xoay hình ảnh = 270

Độ xoay màn hình = 90
Hướng màn hình = Chiều ngang
Độ xoay hình ảnh = 180

Ví dụ 3: Cảm biến được xoay 0 độ

Ví dụ về thiết bị: Pixel C (Máy tính bảng)

Độ xoay màn hình = 0
Hướng màn hình = Chiều ngang
Độ xoay hình ảnh = 0

Độ xoay màn hình = 270
Hướng màn hình = Chiều dọc
Độ xoay hình ảnh = 90

Tính toán độ xoay hình ảnh

ImageAnalysis

Analyzer của ImageAnalysis nhận hình ảnh từ camera dưới dạng ImageProxy. Mỗi hình ảnh đều chứa thông tin độ xoay, mà có thể truy cập được qua:

val rotation = imageProxy.imageInfo.rotationDegrees

Giá trị này biểu thị số độ mà hình ảnh cần được xoay theo chiều kim đồng hồ để khớp với độ xoay mục tiêu của ImageAnalysis. Trong bối cảnh của ứng dụng Android, độ xoay mục tiêu của ImageAnalysis thường sẽ khớp với hướng của màn hình.

ImageCapture

Lệnh gọi lại được đính kèm vào một thực thể ImageCapture để gửi tín hiệu khi có kết quả chụp. Kết quả có thể là ảnh đã chụp hoặc lỗi.

Khi chụp ảnh, lệnh gọi lại được cung cấp có thể thuộc một trong những loại sau:

  • OnImageCapturedCallback: Nhận hình ảnh với quyền truy cập vào bộ nhớ dưới dạng ImageProxy.
  • OnImageSavedCallback: Được gọi khi ảnh chụp được lưu trữ thành công tại vị trí được ImageCapture.OutputFileOptions chỉ định. Tuỳ chọn có thể chỉ định File, OutputStream hoặc một vị trí trong MediaStore.

Việc xoay ảnh chụp, bất kể định dạng của ảnh là gì (ImageProxy, File, OutputStream, MediaStore Uri) có nghĩa là số độ mà ảnh đã chụp cần xoay theo chiều kim đồng hồ để khớp với độ xoay mục tiêu của ImageCapture. Xin nhắc lại, trong ngữ cảnh của một ứng dụng Android độ xoay mục tiêu thường sẽ khớp với hướng của màn hình.

Việc truy xuất độ xoay của ảnh chụp có thể được thực hiện theo một trong các cách sau:

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

Xác định hướng xoay của hình ảnh

Các trường hợp sử dụng ImageAnalysisImageCapture nhận ImageProxy từ camera sau khi chụp ảnh thành công. ImageProxy gói ảnh và thông tin về ảnh đó, bao gồm cả độ xoay. Thông tin về độ xoay này thể hiện số độ mà bạn phải xoay hình ảnh để khớp với độ xoay mục tiêu của trường hợp sử dụng.

Quy trình xác minh độ xoay của hình ảnh

Nguyên tắc về độ xoay mục tiêu trong ImageCapture/ImageAnalysis

Vì nhiều thiết bị không xoay để đảo chiều dọc hoặc đảo chiều ngang theo mặc định, nên một số ứng dụng Android không hỗ trợ các chiều này. Việc ứng dụng có hỗ trợ việc này hay không sẽ thay đổi cách mà độ xoay mục tiêu của trường hợp sử dụng được cập nhật.

Dưới đây là hai bảng xác định cách đồng bộ hoá độ xoay mục tiêu của trường hợp sử dụng với độ xoay màn hình. Bảng đầu tiên hướng dẫn cách thực hiện việc này trong khi hỗ trợ cả bốn hướng; bảng thứ hai chỉ xử lý hướng thiết bị xoay theo mặc định.

Để chọn thực hiện nguyên tắc nào trong ứng dụng của bạn:

  1. Xác minh liệu camera Activity trong ứng dụng của bạn có hướng bị khoá hay không, hướng đã mở khoá chưa hay camera ghi đè chế độ thay đổi cấu hình hướng.

  2. Quyết định liệu camera trong ứng dụng Activity có nên xử lý cả bốn hướng thiết bị (dọc, dọc lộn ngược, ngang và ngang lộn ngược) hay liệu camera chỉ nên xử lý các hướng được thiết bị, mà ứng dụng chay trên đó, hỗ trợ theo mặc định.

Hỗ trợ cả bốn hướng

Bảng này đề cập đến một số nguyên tắc nhất định cần tuân theo trong trường hợp thiết bị không xoay để đảo chiều dọc. Áp dụng điều tương tự với các thiết bị không xoay để đảo chiều ngang.

Trường hợp Nguyên tắc Chế độ một cửa sổ Chế độ chia đôi màn hình thành nhiều cửa sổ
Hướng đã được mở khoá Thiết lập các trường hợp sử dụng mỗi khi tạo Activity, chẳng hạn như trong lệnh gọi lại onCreate() của Activity.
Sử dụng onOrientationChanged() của OrientationEventListener. Trong lệnh gọi lại, cập nhật độ xoay mục tiêu của trường hợp sử dụng. Thao tác này xử lý các trường hợp tại đó hệ thống không tái tạo Activity ngay cả sau khi một hướng thay đổi, chẳng hạn như khi thiết bị xoay 180 độ. Cũng xử lý các trường hợp khi màn hình nằm theo chiều dọc lộn ngược và thiết bị không xoay để đảo ngược chiều dọc theo mặc định. Cũng xử lý các trường hợp trong đó Activity không được tái tạo khi thiết bị xoay (90 độ chẳng hạn). Điều này xảy ra trên các thiết bị có kiểu dáng nhỏ khi ứng dụng chiếm một nửa màn hình và trên những thiết bị lớn hơn khi ứng dụng chiếm hai phần ba màn hình.
Tuỳ chọn: Đặt thuộc tính screenOrientation của Activity thành fullSensor trong tệp AndroidManifest. Thao tác này cho phép giao diện người dùng là thẳng đứng khi thiết bị ở chế độ dọc lộn ngược và cho phép hệ thống tái tạo Activity bất cứ khi nào thiết bị xoay 90 độ. Không ảnh hưởng đến các thiết bị không xoay để đảo ngược màn hình dọc theo mặc định. Không hỗ trợ chế độ nhiều cửa sổ khi màn hình hiển thị theo chiều dọc lộn ngược.
Hướng bị khoá Chỉ thiết lập các trường hợp sử dụng một lần khi Activity được tạo ra lần đầu tiên, chẳng hạn như trong lệnh gọi lại onCreate() của Activity.
Sử dụng onOrientationChanged() của OrientationEventListener. Trong lệnh gọi lại, hãy cập nhật độ xoay mục tiêu của trường hợp sử dụng, ngoại trừ Bản xem trước. Cũng xử lý các trường hợp trong đó Activity không được tái tạo khi thiết bị xoay (90 độ chẳng hạn). Điều này xảy ra trên các thiết bị có kiểu dáng nhỏ khi ứng dụng chiếm một nửa màn hình và trên những thiết bị lớn hơn khi ứng dụng chiếm hai phần ba màn hình.
Ghi đè hướng configChanges Chỉ thiết lập các trường hợp sử dụng một lần khi Activity được tạo ra lần đầu tiên, chẳng hạn như trong lệnh gọi lại onCreate() của Activity.
Sử dụng onOrientationChanged() của OrientationEventListener. Trong lệnh gọi lại, cập nhật độ xoay mục tiêu của trường hợp sử dụng. Cũng xử lý các trường hợp trong đó Activity không được tái tạo khi thiết bị xoay (90 độ chẳng hạn). Điều này xảy ra trên các thiết bị có kiểu dáng nhỏ khi ứng dụng chiếm một nửa màn hình và trên những thiết bị lớn hơn khi ứng dụng chiếm hai phần ba màn hình.
Tuỳ chọn: Đặt thuộc tính screenOrientation của Hoạt động thành fullSensor trong tệp AndroidManifest. Cho phép giao diện người dùng là thẳng đứng khi thiết bị nằm dọc lộn ngược. Không ảnh hưởng đến các thiết bị không xoay để đảo ngược màn hình dọc theo mặc định. Không hỗ trợ chế độ nhiều cửa sổ khi màn hình theo chiều dọc lộn ngược.

Chỉ hỗ trợ các hướng được thiết bị hỗ trợ

Chỉ hỗ trợ hướng mà thiết bị hỗ trợ theo mặc định (có thể bao gồm hoặc không bao gồm dọc lộn ngược/ngang lộn ngược).

Trường hợp Nguyên tắc Chế độ chia đôi màn hình thành nhiều cửa sổ
Hướng đã được mở khoá Thiết lập các trường hợp sử dụng mỗi khi tạo Activity, chẳng hạn như trong lệnh gọi lại onCreate() của Activity.
Sử dụng onDisplayChanged() của DisplayListener. Trong lệnh gọi lại, cập nhật độ xoay mục tiêu của trường hợp sử dụng, chẳng hạn như khi xoay thiết bị 180 độ. Cũng xử lý các trường hợp trong đó Activity không được tái tạo khi thiết bị xoay (90 độ chẳng hạn). Điều này xảy ra trên các thiết bị có kiểu dáng nhỏ khi ứng dụng chiếm một nửa màn hình và trên những thiết bị lớn hơn khi ứng dụng chiếm hai phần ba màn hình.
Hướng bị khoá Chỉ thiết lập các trường hợp sử dụng một lần khi Activity được tạo ra lần đầu tiên, chẳng hạn như trong lệnh gọi lại onCreate() của Activity.
Sử dụng onOrientationChanged() của OrientationEventListener. Trong lệnh gọi lại, cập nhật độ xoay mục tiêu của trường hợp sử dụng. Cũng xử lý các trường hợp trong đó Activity không được tái tạo khi thiết bị xoay (90 độ chẳng hạn). Điều này xảy ra trên các thiết bị có kiểu dáng nhỏ khi ứng dụng chiếm một nửa màn hình và trên những thiết bị lớn hơn khi ứng dụng chiếm hai phần ba màn hình.
Ghi đè hướng configChanges Chỉ thiết lập các trường hợp sử dụng một lần khi Activity được tạo ra lần đầu tiên, chẳng hạn như trong lệnh gọi lại onCreate() của Activity.
Sử dụng onDisplayChanged() của DisplayListener. Trong lệnh gọi lại, cập nhật độ xoay mục tiêu của trường hợp sử dụng, chẳng hạn như khi xoay thiết bị 180 độ. Cũng xử lý các trường hợp trong đó Activity không được tái tạo khi thiết bị xoay (90 độ chẳng hạn). Điều này xảy ra trên các thiết bị có kiểu dáng nhỏ khi ứng dụng chiếm một nửa màn hình và trên những thiết bị lớn hơn khi ứng dụng chiếm hai phần ba màn hình.

Hướng đã được mở khoá

Một Activity có hướng được mở khoá khi hướng của màn hình (như dọc hoặc ngang) khớp với hướng thực của thiết bị, ngoại trừ trường hợp dọc/ngang lộn ngược, mà một số thiết bị không hỗ trợ theo mặc định. Để buộc thiết bị xoay sang cả bốn hướng, hãy đặt thuộc tính screenOrientation của Activity thành fullSensor.

Ở chế độ nhiều cửa sổ, theo mặc định một thiết bị không hỗ trợ chế độ dọc/ngang lộn ngược sẽ không xoay thành dọc/ngang lộn ngược, ngay cả khi thuộc tính screenOrientation của thiết bị được đặt thành 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" />

Hướng bị khoá

Màn hình có hướng bị khoá khi vẫn ở cùng một hướng hiển thị (chẳng hạn như dọc hoặc ngang) bất kể hướng thực của thiết bị là gì. Bạn có thể thực hiện việc này bằng cách chỉ định thuộc tính screenOrientation của Activity bên trong phần khai báo của nó trong tệp AndroidManifest.xml.

Khi màn hình có hướng bị khoá, hệ thống sẽ không huỷ và tạo lại Activity khi xoay thiết bị.

<!-- The Activity keeps a portrait orientation even as the device rotates. -->
<activity
   android:name=".LockedOrientationActivity"
   android:screenOrientation="portrait" />

Ghi đè thay đổi về cấu hình hướng

Khi Activity ghi đè thay đổi về cấu hình hướng, hệ thống sẽ không huỷ và tái tạo cấu hình đó khi hướng thực của thiết bị thay đổi. Tuy nhiên, hệ thống sẽ cập nhật giao diện người dùng để phù hợp với hướng thực của thiết bị.

<!-- 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" />

Thiết lập các trường hợp sử dụng camera

Trong các trường hợp được mô tả ở trên, các trường hợp sử dụng camera có thể được thiết lập khi Activity được tạo lần đầu tiên.

Trong trường hợp Activity có hướng được mở khoá, việc thiết lập này sẽ được thực hiện mỗi khi xoay thiết bị, vì hệ thống sẽ huỷ và tái tạo Activity khi hướng thay đổi. Kết quả là lần nào trường hợp sử dụng cũng sẽ cài đặt độ xoay mục tiêu cho phù hợp với hướng của màn hình theo mặc định.

Trong trường hợp Activity có hướng bị hoá hoặc hoạt động ghi đè thay đổi cấu hình hướng, thì việc thiết lập này sẽ được thực hiện một lần khi Activity được tạo lần đầu tiên.

class CameraActivity : AppCompatActivity() {
   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)
   }
}

Thiết lập OrientationEventListener

Việc sử dụng OrientationEventListener cho phép bạn liên tục cập nhật độ xoay mục tiêu của các trường hợp sử dụng camera khi hướng thiết bị thay đổi.

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()
    }
}

Thiết lập DisplayListener

Việc sử dụng DisplayListener cho phép bạn cập nhật độ xoay mục tiêu của trường hợp sử dụng camera trong một số trường hợp, chẳng hạn như khi hệ thống không huỷ và tái tạo Activity sau khi thiết bị xoay 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)
    }
}