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 中的位置。

無論所拍攝圖像的格式為 ImageProxyFileOutputStream 還是 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

驗證圖像的旋轉資訊

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 == 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 在某些情況下更新相機用途的目標旋轉角度;比方說,在裝置旋轉 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)
    }
}