このトピックでは、アプリ内で CameraX のユースケースを設定して、正しい回転情報が指定された画像を取得する方法を示します。ImageAnalysis
と ImageCapture
のどちらのユースケースにも該当します。それによって次のようになります。
ImageAnalysis
ユースケースのAnalyzer
は正しい回転が指定されたフレームを受信します。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
ImageAnalysis
の Analyzer
は、カメラから画像を ImageProxy
の形式で受け取ります。各画像には回転情報が含まれています。これは次のように確認できます。
val rotation = imageProxy.imageInfo.rotationDegrees
この値は、ImageAnalysis
のターゲットの回転に合わせて画像を時計回りに回転させる必要がある度数を表します。Android アプリのコンテキストでは、ImageAnalysis
のターゲットの回転は通常、画面の向きと一致します。
ImageCapture
キャプチャ結果が準備完了になったことを通知するために、ImageCapture
インスタンスにコールバックが追加されています。それにより、キャプチャされた画像またはエラーが返されます。
写真を撮る場合、提供されるコールバックは次のいずれかのタイプになります。
OnImageCapturedCallback
:ImageProxy
の形式で、メモリ内アクセスが可能な画像を受け取ります。OnImageSavedCallback
: キャプチャした画像が、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
は、画像とそれに関する情報(回転を含む)をラップします。この回転情報は、ユースケースのターゲット回転に合わせて画像を回転させる必要がある度数を表します。
ImageCapture / ImageAnalysis ターゲット回転ガイドライン
多くのデバイスはデフォルトで逆の縦向きまたは逆の横向きに回転できないため、これらの向きをサポートしていない Android アプリもあります。そのため、ユースケースのターゲット回転の更新方法は、アプリがこれをサポートしているかどうかによって変わります。
以下の 2 つの表は、ユースケースのターゲット回転とディスプレイの回転を同期する方法を示しています。最初の表は、4 つすべての向きをサポートしながらこの同期を行う方法を示しています。2 つ目の表は、デフォルトでデバイスが回転する向きだけを示しています。
アプリ内で従うガイドラインを選択するには:
アプリのカメラ
Activity
で向きがロックされているかどうか、また向きの設定変更がオーバーライドされるかどうかを確認します。アプリのカメラの
Activity
がデバイスの 4 つの向き(縦向き、逆の縦向き、横向き、逆の横向き)すべてを処理するか、デバイスでデフォルトでサポートされている向きだけを処理するかを決定します。
4 つすべての画面の向きをサポートする
この表は、デバイスが逆の縦向きに回転しない場合のガイドラインを示しています。逆の横向きに回転しないデバイスについても同様に適用できます。
シナリオ | ガイドライン | 単一ウィンドウ モード | マルチウィンドウ分割画面モード |
---|---|---|---|
画面の向きのロックを解除 |
Activity が作成されるたびにユースケースを設定します(Activity の onCreate() コールバックなど)。
|
||
OrientationEventListener の onOrientationChanged() を使用します。
コールバック内で、ユースケースのターゲット回転を更新します。デバイスが 180 度回転された場合など、向きの変更後も Activity が再作成されない場合に適用されます。 |
ディスプレイが逆の縦向きであるときに、デバイスがデフォルトで逆の縦向きに回転しない場合にも適用されます。 |
また、デバイスが(たとえば 90 度)回転しても Activity が再作成されない場合にも適用されます。これは、アプリが画面の半分を占めるようなフォーム ファクタが小さいデバイスや、3 分の 2 を占めるフォーム ファクタが大きいデバイスに該当します。
|
|
省略可: AndroidManifest ファイルで、Activity の screenOrientation プロパティを fullSensor に設定します。
|
それにより、デバイスが逆の縦向きのときに UI が縦向きになり、デバイスを 90 度回転したときに Activity が再作成されます。
|
デフォルトで逆の縦向きに回転しないデバイスには影響しません。ディスプレイが逆の縦向きである場合、マルチウィンドウ モードはサポートされません。 | |
画面の向きをロック |
Activity が最初に作成されたときに、ユースケースを 1 回だけ作成します(Activity の onCreate() コールバックなど)。
|
||
OrientationEventListener の onOrientationChanged() を使用します。
コールバック内で、プレビュー以外のユースケースのターゲット回転を更新します。
|
また、デバイスが(たとえば 90 度)回転しても Activity が再作成されない場合にも適用されます。これは、アプリが画面の半分を占めるようなフォーム ファクタが小さいデバイスや、3 分の 2 を占めるフォーム ファクタが大きいデバイスに該当します。
|
||
デバイスの向きの configChanges がオーバーライドされる |
Activity が最初に作成されたときに、ユースケースを 1 回だけ作成します(Activity の onCreate() コールバックなど)。
|
||
OrientationEventListener の onOrientationChanged() を使用します。
コールバック内で、ユースケースのターゲット回転を更新します。
|
また、デバイスが(たとえば 90 度)回転しても Activity が再作成されない場合にも適用されます。これは、アプリが画面の半分を占めるようなフォーム ファクタが小さいデバイスや、3 分の 2 を占めるフォーム ファクタが大きいデバイスに該当します。
|
||
省略可: AndroidManifest ファイルで、アクティビティの screenOrientation プロパティを fullSensor に設定します。 | デバイスが逆の縦向きのときに UI を縦向きにできるようにします。 | デフォルトで逆の縦向きに回転しないデバイスには影響しません。ディスプレイが逆の縦向きである場合、マルチウィンドウ モードはサポートされません。 |
デバイスでサポートされている画面の向きのみをサポートする
デバイスがデフォルトでサポートする画面の向きのみをサポートします(逆の縦向き / 逆の横向きを含む場合と含まない場合があります)。
シナリオ | ガイドライン | マルチウィンドウ分割画面モード |
---|---|---|
画面の向きのロックを解除 |
Activity が作成されるたびにユースケースを設定します(Activity の onCreate() コールバックなど)。
|
|
DisplayListener の onDisplayChanged() を使用します。コールバック内でユースケースのターゲット回転を更新します(デバイスが 180 度回転したときなど)。
|
また、デバイスが(たとえば 90 度)回転しても Activity が再作成されない場合にも適用されます。これは、アプリが画面の半分を占めるようなフォーム ファクタが小さいデバイスや、3 分の 2 を占めるフォーム ファクタが大きいデバイスに該当します。
|
|
画面の向きをロック |
Activity が最初に作成されたときに、ユースケースを 1 回だけ作成します(Activity の onCreate() コールバックなど)。
|
|
OrientationEventListener の onOrientationChanged() を使用します。
コールバック内で、ユースケースのターゲット回転を更新します。
|
また、デバイスが(たとえば 90 度)回転しても Activity が再作成されない場合にも適用されます。これは、アプリが画面の半分を占めるようなフォーム ファクタが小さいデバイスや、3 分の 2 を占めるフォーム ファクタが大きいデバイスに該当します。
|
|
デバイスの向きの configChanges がオーバーライドされる |
Activity が最初に作成されたときに、ユースケースを 1 回だけ作成します(Activity の onCreate() コールバックなど)。
|
|
DisplayListener の onDisplayChanged() を使用します。コールバック内でユースケースのターゲット回転を更新します(デバイスが 180 度回転したときなど)。
|
また、デバイスが(たとえば 90 度)回転しても Activity が再作成されない場合にも適用されます。これは、アプリが画面の半分を占めるようなフォーム ファクタが小さいデバイスや、3 分の 2 を占めるフォーム ファクタが大きいデバイスに該当します。
|
画面の向きのロックを解除
ディスプレイの向き(縦向き、横向きなど)とデバイスの物理的な向きが一致する場合に、Activity
で向きのロックが解除されます。ただし一部のデバイスでデフォルトでサポートされていない、逆の縦向き / 逆の横向きについては該当しません。デバイスが 4 つすべての向きに回転するように強制するには、Activity
の screenOrientation
プロパティを 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
によって向きの設定変更がオーバーライドされると、デバイスの物理的な向きが変わっても、設定が破棄されず、再作成されません。
ただし 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" />
カメラのユースケースの設定
上記のシナリオでは、Activity
が最初に作成されるときにカメラのユースケースを設定できます。
Activity
で向きのロックが解除されている場合は、向きが変わると Activity
が破棄されて再作成されるため、この設定はデバイスが回転されるたびに行われます。それによりユースケースでは、ディスプレイの向きに合わせて、デフォルトで毎回ターゲットの回転が設定されます。
Activity
で向きがロックされている場合、または向きの設定変更がオーバーライドされる場合には、この設定は Activity
が最初に作成されたときに 1 回だけ行われます。
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) } }