CameraX 用途旋轉資訊

本主題說明如何在應用程式中設定 CameraX 用途,以取得附帶正確旋轉資訊的圖像,無論是透過 ImageAnalysisImageCapture 用途。因此:

  • ImageAnalysis 用途的 Analyzer 應接收到附帶正確旋轉資訊的畫面。
  • ImageCapture 用途應拍攝附帶正確旋轉資訊的相片。

術語

本主題會使用下列術語,因此請務必瞭解各個詞彙的意義:

顯示螢幕方向
這是指裝置的哪一邊朝上,可能是以下四個值之一:直向、橫向、反向直向或反向橫向。
螢幕旋轉
這是指由 Display.getRotation() 傳回的值,表示裝置從自然方向逆時針旋轉的角度值。
目標旋轉
這是指裝置以順時針方向旋轉,以達成自然方向的度數。

如何判斷目標旋轉

以下範例說明如何根據裝置的自然方向來判斷裝置的目標旋轉。

範例 1:直向自然方向

裝置範例:Pixel 3 XL

自然螢幕方向 = 直向
目前螢幕方向 = 直向

螢幕旋轉角度 = 0
目標旋轉角度 = 0

自然螢幕方向 = 直向
目前螢幕方向 = 橫向

螢幕旋轉角度 = 90
目標旋轉角度 = 90

範例 2:橫向自然螢幕方向

裝置範例:Pixel C

自然螢幕方向 = 橫向
目前螢幕方向 = 橫向

螢幕旋轉角度 = 0
目標旋轉角度 = 0

自然螢幕方向 = 橫向
目前螢幕方向 = 直向

螢幕旋轉角度 = 270
目標旋轉角度 = 270

圖像旋轉角度

哪一邊朝上?螢幕方向感應器在 Android 中定義為一個常數值,代表當裝置處於自然位置時,感應器從裝置頂端旋轉的角度值 (0、90、180、270)。針對圖表中的所有案例,圖像旋轉說明了資料應如何順時針旋轉,以顯示的直立圖像。

以下範例說明依據相機感應器方向而定的圖像旋轉度數。此外,這些範例也會假設目標旋轉已設為螢幕旋轉。

範例 1:感應器旋轉 90 度

裝置範例:Pixel 3 XL

螢幕旋轉角度 = 0
螢幕顯示方向 = 直向
圖像旋轉角度 = 90

螢幕旋轉角度 = 90
螢幕顯示方向 = 橫向
圖像旋轉角度 = 0

範例 2:感應器旋轉 270 度

裝置範例:Nexus 5X

螢幕旋轉 = 0
螢幕顯示方向 = 直向
圖像旋轉角度 = 270

螢幕旋轉角度 = 90
螢幕顯示方向 = 橫向
圖像旋轉角度 = 180

範例 3:感應器旋轉 0 度

裝置範例:Pixel C (平板電腦)

螢幕旋轉角度 = 0
螢幕顯示方向 = 橫向
圖像旋轉角度 = 0

螢幕旋轉角度 = 270
螢幕顯示方向 = 直向
圖像旋轉角度 = 90

計算圖像的旋轉角度

ImageAnalysis

ImageAnalysisAnalyzer 會從相機接收 ImageProxy 格式的圖像。每張圖像都包含旋轉資訊,這項資訊可透過下列方式存取:

val rotation = imageProxy.imageInfo.rotationDegrees

這個值表示圖像必須順時針旋轉的角度,以符合 ImageAnalysis 的目標旋轉。就 Android 應用程式而言,ImageAnalysis 的目標旋轉通常會與螢幕的方向一致。

ImageCapture

ImageCapture 例項會附加回呼,以便在拍攝結果就緒時傳送信號。這個結果可能是拍攝的圖像或是錯誤。

拍照時,提供的回呼可以是下列其中一種類型:

  • OnImageCapturedCallbackImageProxy 格式接收具有記憶體內存取權的圖像。
  • OnImageSavedCallback於所拍攝圖像已成功儲存在 ImageCapture.OutputFileOptions 指定的位置時叫用。相關選項可指定 FileOutputStreamMediaStore 中的位置。

同樣地,就 Android 應用程式而言,所拍攝圖像的旋轉角度 (無論格式為 ImageProxyFileOutputStreamMediaStore Uri) 表示所拍攝圖像必須順時針旋轉的角度,以符合 ImageCapture 的目標旋轉,且通常會與螢幕的方向一致。

您可以透過下列其中一種方式擷取所拍攝圖像的旋轉資訊:

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

驗證圖像的旋轉資訊

ImageAnalysisImageCapture 用途會在擷取要求成功執行後,從相機接收 ImageProxyImageProxy 會包裝圖像以及相關資訊 (包括旋轉資訊)。這項旋轉資訊表示圖像必須順時針旋轉的度數,以符合該用途的目標旋轉。

圖像旋轉資訊驗證流程

ImageCapture/ImageAnslysis 目標旋轉相關規範

根據預設,多數裝置都不會旋轉至反向直向或反向橫向的角度,因此部分 Android 應用程式也不支援這些螢幕方向。用途的目標旋轉更新方式,依應用程式是否支援這些螢幕方向而異。

以下兩個表格定義如何讓用途的目標旋轉與螢幕旋轉保持同步。第一個表格說明在支援所有四個螢幕方向的情況下可採取的做法,第二個表格則只處理裝置在預設情況下的旋轉方向。

如何選擇要在應用程式中遵循哪些規範:

  1. 確認應用程式的相機 Activity 是否已鎖定螢幕方向、已解鎖螢幕方向,或者是否會覆寫螢幕方向的設定變更。

  2. 決定應用程式的相機 Activity 是否應處理所有四個裝置螢幕方向 (直向、反向直向、橫向和反向橫向),或者只應處理執行所在裝置預設支援的螢幕方向。

支援所有四個方向

下表提及在裝置不會旋轉至反向直向的情況下必須遵循的特定規範。這些規範也適用於不會旋轉至反向橫向的裝置。

情境 規範 單一視窗模式 多視窗分割畫面模式
已解鎖螢幕方向 在每次建立 Activity 時設定用途 (例如在 ActivityonCreate() 回呼中)。
使用 OrientationEventListeneronOrientationChanged()。在回呼中,更新用途的目標旋轉。這樣做可以處理螢幕方向變更之後 (例如裝置旋轉 180 度),系統沒有重新建立 Activity 的情況。 同時處理螢幕處於反向直向狀態,以及裝置預設不會旋轉至反向直向的情況。 同時處理當裝置旋轉 (例如 90 度) 時,不會重新建立 Activity 的情況。這種情況會發生於應用程式在小型板型規格的裝置上佔用半個螢幕,以及在大型裝置上佔用螢幕的三分之二時。
選用:在 AndroidManifest 檔案中,將 ActivityscreenOrientation 屬性設為 fullSensor 這樣做可讓使用者介面在裝置處於反向直向狀態時保持直立,並讓系統在每次裝置旋轉 90 度時重新建立 Activity 對預設不會旋轉至反向直向的裝置沒有任何作用。螢幕處於反向直向的狀態時,不支援多視窗模式。
已鎖定螢幕方向 僅在首次建立 Activity 時設定用途一次,例如在 ActivityonCreate() 回呼中。
使用 OrientationEventListeneronOrientationChanged()。在回呼中,更新用途的目標旋轉。 同時處理當裝置旋轉 (例如 90 度) 時,不會重新建立 Activity 的情況。這種情況會發生於應用程式在小型板型規格的裝置上佔用半個螢幕,以及在大型裝置上佔用螢幕的三分之二時。
覆寫螢幕方向 configChanges 僅在首次建立 Activity 時設定用途一次,例如在 ActivityonCreate() 回呼中。
使用 OrientationEventListeneronOrientationChanged()。在回呼中,更新用途的目標旋轉。 同時處理當裝置旋轉 (例如 90 度) 時,不會重新建立 Activity 的情況。這種情況會發生於應用程式在小型板型規格的裝置上佔用半個螢幕,以及在大型裝置上佔用螢幕的三分之二時。
選用:在 AndroidManifest 檔案中將 Activity 的 screenOrientation 屬性設為 fullSensor。 這項設定可讓使用者介面在裝置處於反向直向狀態時保持直立。 對預設不會旋轉至反向直向的裝置沒有任何作用。螢幕處於反向直向的狀態時,不支援多視窗模式。

僅支援裝置支援的螢幕方向

僅支援裝置預設支援的螢幕方向 (不一定包括反向直向/反向橫向)。

情境 規範 多視窗分割畫面模式
已解鎖螢幕方向 在每次建立 Activity 時設定用途 (例如在 ActivityonCreate() 回呼中)。
使用 DisplayListeneronDisplayChanged()。在回呼中,更新用途的目標旋轉,例如當裝置旋轉 180 度時。 同時處理當裝置旋轉 (例如 90 度) 時,不會重新建立 Activity 的情況。這種情況會發生於應用程式在小型板型規格的裝置上佔用半個螢幕,以及在大型裝置上佔用螢幕的三分之二時。
已鎖定螢幕方向 僅在首次建立 Activity 時設定用途一次,例如在 ActivityonCreate() 回呼中。
使用 OrientationEventListeneronOrientationChanged()。在回呼中,更新用途的目標旋轉。 同時處理當裝置旋轉 (例如 90 度) 時,不會重新建立 Activity 的情況。這種情況會發生於應用程式在小型板型規格的裝置上佔用半個螢幕,以及在大型裝置上佔用螢幕的三分之二時。
覆寫螢幕方向 configChanges 僅在首次建立 Activity 時設定用途一次,例如在 ActivityonCreate() 回呼中。
使用 DisplayListeneronDisplayChanged()。在回呼中,更新用途的目標旋轉,例如當裝置旋轉 180 度時。 同時處理當裝置旋轉 (例如 90 度) 時,不會重新建立 Activity 的情況。這種情況會發生於應用程式在小型板型規格的裝置上佔用半個螢幕,以及在大型裝置上佔用螢幕的三分之二時。

已解鎖螢幕方向

當活動的螢幕方向 (例如直向或橫向) 與裝置的實際方向一致時,Activity 就擁有已解鎖螢幕顯示方向 (但部分裝置預設不支援的反向直向/橫向除外)。如要強制讓裝置能旋轉至所有四個方向,請將 ActivityscreenOrientation 屬性設為 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" />

已鎖定螢幕方向

無論裝置螢幕的實際方向為何,螢幕都會在保持相同螢幕顯示方向 (例如直向或橫向) 時擁有已鎖定螢幕方向。方法是在 AndroidManifest.xml 檔案的 Activity 宣告中指定 screenOrientation 屬性。

當螢幕方向已鎖定時,系統在裝置旋轉時不會刪除並重新建立 Activity

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

覆寫螢幕方向設定變更

Activity 覆寫螢幕方向設定變更時,系統並不會在裝置的實際方向變更時刪除並重新建立該活動,但系統會根據裝置的實際方向更新使用者介面。

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

相機用途設定

在上述情境中,您可以在首次建立 Activity 時設定相機用途。

Activity 具有已解鎖螢幕方向的情況下,由於系統會在螢幕方向變更時刪除並重新建立 Activity,因此每次裝置旋轉時都會進行這項設定。這會導致用途每次都將目標旋轉預設為與螢幕方向一致。

Activity 具有已鎖定螢幕方向,或覆寫螢幕方向設定變更的情況下,則會在 Activity 首次建立時進行這項設定一次。

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

OrientationEventListener 設定

您可以利用 OrientationEventListener,隨著裝置螢幕方向變更,持續更新相機用途的目標旋轉。

class CameraActivity : AppCompatActivity() {

    private val orientationEventListener by lazy {
        object : OrientationEventListener(this) {
            override fun onOrientationChanged(orientation: Int) {
                if (orientation == UNKNOWN_ORIENTATION) {
                    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 在某些情況下更新相機用途的目標旋轉;比方說,在裝置旋轉 180 度後,系統並未刪除並重新建立 Activity 的情況下。

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