注: このページでは、Camera2 パッケージについて説明します。アプリが Camera2 の特定の低レベルの機能を必要とする場合を除き、CameraX を使用することをおすすめします。CameraX と Camera2 は、どちらも Android 5.0(API レベル 21)以降に対応しています。
Android デバイスでカメラとカメラ プレビューの向きが常に同じになるとは限りません。
カメラは、デバイスがスマートフォン、タブレット、パソコンのいずれでも、デバイス上の固定位置にあります。デバイスの向きが変わると、カメラの向きも変わります。
そのため、カメラアプリは通常、デバイスの向きとカメラ プレビューのアスペクト比の間に一定の関係があると想定します。スマートフォンが縦向きの場合、カメラ プレビューの高さは幅よりも高くなります。スマートフォン(とカメラ)を横向きに回転すると、カメラ プレビューは高さよりも幅が広いことが想定されます。
しかし、これらの前提条件は、折りたたみ式デバイスなどの新しいフォーム ファクタや、マルチウィンドウやマルチディスプレイなどの表示モードにより、困難になります。折りたたみ式デバイスは、画面の向きを変えずにディスプレイ サイズとアスペクト比を変更します。マルチウィンドウ モードでは、カメラアプリが画面の一部に制限され、デバイスの向きに関係なくカメラ プレビューがスケーリングされます。マルチディスプレイ モードでは、プライマリ ディスプレイと同じ向きでない可能性があるセカンダリ ディスプレイを使用できます。
カメラの向き
Android 互換性定義では、カメラのイメージ センサーの向きは、カメラの長辺と画面の長辺が平行となる向きにしなければならないと規定されています。つまり、デバイスが横向きで保持されている場合、カメラは横向きで画像をキャプチャしなければなりません。これは、デバイスの自然な向きに関係なく適用されます。つまり、横向き主体のデバイスと、縦向き主体のデバイスに適用されます。」
カメラから画面への配置により、カメラアプリのカメラ ビューファインダーの表示領域が最大化されます。また、イメージ センサーは通常、横向きのアスペクト比(4:3 が最も一般的)でデータを出力します。
カメラセンサーの自然な向きは横向きです。図 1 では、Android 互換性定義に準拠するために、前面カメラ(ディスプレイと同じ方向を向いているカメラ)のセンサーは、スマートフォンに対して 270 度回転しています。
センサーの回転をアプリに公開するために、camera2 API には SENSOR_ORIENTATION
定数が含まれています。ほとんどのスマートフォンとタブレットでは、センサーの向きが、前面カメラの場合は 270 度、背面カメラの場合は 90 度(デバイスの背面からの視点)がレポートされます。これにより、センサーの長辺がデバイスの長辺に合わせられます。通常、ノートパソコンのカメラはセンサーの向きが 0 度または 180 度であると報告します。
カメラの画像センサーは、センサーの自然な向き(横向き)でデータ(画像バッファ)を出力するため、デバイスの自然な向きでカメラ プレビューが直立するように、画像バッファを SENSOR_ORIENTATION
で指定された角度だけ回転させる必要があります。前面カメラの場合は反時計回り、背面カメラの場合は時計回りです。
たとえば、図 1 の前面カメラの場合、カメラセンサーによって生成される画像バッファは次のようになります。
プレビューの向きをデバイスの向きに合わせ、画像を反時計回りに 270 度回転させる必要があります。
背面カメラは上記のバッファと同じ向きの画像バッファを生成しますが、SENSOR_ORIENTATION
は 90 度です。その結果、バッファが時計回りに 90 度回転します。
デバイスの回転
デバイスの回転は、デバイスが自然な向きから回転する度数です。たとえば、横向きのスマートフォンは、回転方向に応じて 90 度または 270 度回転します。
カメラ プレビューが縦に表示されるようにするには、カメラセンサーの画像バッファを(センサーの向きだけでなく)デバイスの回転と同じ角度だけ回転させる必要があります。
向きの計算
カメラ プレビューの適切な向きでは、センサーの向きとデバイスの回転が考慮されます。
センサー イメージ バッファの全体的な回転は、次の式を使用して計算できます。
rotation = (sensorOrientationDegrees - deviceOrientationDegrees * sign + 360) % 360
ここで、sign
は、前面カメラの場合は 1
、背面カメラの場合は -1
です。
前面カメラの場合、画像バッファは(センサーの自然な向きから)反時計回りに回転します。背面カメラの場合、センサーの画像バッファは時計回りに回転します。
式 deviceOrientationDegrees * sign + 360
は、背面カメラでデバイスの回転を反時計回りから時計回りに変換します(たとえば、反時計回り 270 度を時計回り 90 度に変換します)。モジュロ演算は、結果を 360 度未満にスケーリングします(たとえば、540 度回転を 180 度にスケーリング)。
API によって、デバイスの回転を報告する方法が異なります。
Display#getRotation()
は、ユーザーの視点からデバイスを反時計回りに回転させます。この値は上記の式にそのまま挿入されます。OrientationEventListener#onOrientationChanged()
は、ユーザーの視点からデバイスの時計回りの回転を返します。上の数式で使用する値を否定します。
前面カメラ
図 2 のカメラセンサーによって生成された画像バッファは次のとおりです。
センサーの向きを調整するには、バッファを反時計回りに 270 度回転する必要があります(上記のカメラの向きをご覧ください)。
次に、デバイスの回転に合わせて、バッファが反時計回りにさらに 90 度回転します。これにより、図 2 のカメラ プレビューの正しい向きになります。
ここでは、カメラを右に向け、横向きに向けています。
画像バッファは次のとおりです。
センサーの向きを調整するには、バッファを反時計回りに 270 度回転する必要があります。
次に、デバイスの回転に合わせて、バッファが反時計回りにさらに 270 度回転します。
背面カメラ
背面カメラのセンサーの向きは通常 90 度です(デバイスの背面から見た場合)。カメラ プレビューの向きを調整すると、センサーの画像バッファは(前面カメラのように反時計回りではなく)センサーの回転量だけ時計回りに回転した後、デバイスの回転量だけ反時計回りに回転します。
カメラセンサーからの画像バッファを図 4 に示します。
センサーの向きを調整するには、バッファを時計回りに 90 度回転する必要があります。
次に、デバイスの回転に合わせて、バッファが反時計回りに 270 度回転します。
アスペクト比
ディスプレイのアスペクト比は、デバイスの向きが変わるときだけでなく、折りたたみ式デバイスの折りたたみと展開、マルチウィンドウ環境でのウィンドウ サイズ変更、セカンダリ ディスプレイでアプリが開いたときにも変化します。
カメラセンサーの画像バッファの向きとスケーリングは、デバイスの向きが変わっても UI が動的に向きが変わるため、ビューファインダー UI 要素の向きとアスペクト比に合わせて調整する必要があります。
新しいフォーム ファクタ、またはマルチウィンドウまたはマルチディスプレイ環境で、カメラ プレビューの向きがデバイスと同じである(縦向きまたは横向き)とアプリで想定されている場合、プレビューの向きが正しくないか、正しくスケーリングされていない可能性があります。
図 5 では、アプリはデバイスが反時計回りに 90 度回転したと誤認していたため、プレビューが同じ量だけ回転しました。
図 6 では、カメラ プレビュー UI 要素の新しい寸法に合わせて適切に拡大縮小できるように、アプリで画像バッファのアスペクト比を調整していません。
固定向きのカメラアプリでは、通常、折りたたみ式デバイスや大画面デバイス(ノートパソコンなど)で問題が発生します。
図 7 では、アプリの向きが縦向きのみに制限されているため、カメラアプリの UI が横向きになっています。ビューファインダーの画像がカメラセンサーに対して正しい向きで表示されます。
ポートレート モードのインセット
マルチウィンドウ モード(resizeableActivity="false"
)をサポートせず、画面の向きを制限するカメラアプリ(screenOrientation="portrait"
または screenOrientation="landscape"
)は、大画面デバイスでインセット 縦向きモードにして、カメラ プレビューの向きを適切に調整できます。
ディスプレイのアスペクト比が横向きであっても、縦向きモードのレターボックス(インセット)縦向きのアプリを縦向きでインセットします。横向き専用のアプリは、ディスプレイのアスペクト比が縦向きであっても、横向きではレターボックス表示されます。カメラ画像は、アプリの UI に合わせて回転され、カメラ プレビューのアスペクト比に合わせて切り抜き、プレビューに合わせて拡大縮小されます。
インセットの縦向きモードは、カメラ画像センサーのアスペクト比とアプリのプライマリ アクティビティのアスペクト比が一致しない場合にトリガーされます。
図 8 では、縦向き専用のカメラアプリが回転し、ノートパソコンのディスプレイに UI が縦向きで表示されています。縦向きのアプリと横向きのディスプレイではアスペクト比が異なるため、アプリがレターボックス表示されます。カメラ プレビュー画像は、アプリの UI の回転を補正するために回転し(インセットの縦向きモードにより)、縦向きの向きに合わせて画像が切り抜かれ、拡大縮小されて画角が縮小されています。
回転、切り抜き、拡大縮小
インセットの縦向きモードは、横向きのアスペクト比のディスプレイ上の縦向き専用のカメラアプリで呼び出されます。
アプリは縦向きでレターボックス表示されます。
アプリの向きに合わせてカメラの画像が 90 度回転します。
画像はカメラ プレビューのアスペクト比に合わせて切り抜かれ、プレビューに合わせて拡大縮小されます(視野が縮小されます)。
折りたたみ式デバイスでは、カメラセンサーの向きを縦向きにし、ディスプレイのアスペクト比を横向きにすることができます。
カメラ プレビューはセンサーの向きに合わせて回転するため、画像のビューファインダーは適切な向きになりますが、縦向き専用アプリは横向きになります。
インセットの縦向きモードでは、アプリを縦向きにレターボックス表示するだけで、アプリとカメラのプレビューの向きを適切に調整できます。
API
Android 12(API レベル 31)以降では、CaptureRequest
クラスの SCALER_ROTATE_AND_CROP
プロパティを使用して、インセット 縦向きモードを明示的に制御することもできます。
デフォルト値は SCALER_ROTATE_AND_CROP_AUTO
です。これにより、システムはインセット縦向きモードを呼び出すことができます。SCALER_ROTATE_AND_CROP_90
は、前述のインセット 縦向きモードの動作です。
すべてのデバイスがすべての SCALER_ROTATE_AND_CROP
値をサポートしているわけではありません。サポートされている値の一覧については、CameraCharacteristics#SCALER_AVAILABLE_ROTATE_AND_CROP_MODES
をご覧ください。
CameraX
Jetpack CameraX ライブラリを使用すると、センサーの向きとデバイスの回転に対応するカメラのビューファインダーを簡単に作成できます。
PreviewView
レイアウト要素はカメラ プレビューを作成し、センサーの向き、デバイスの回転、スケーリングを自動的に調整します。PreviewView
は、FILL_CENTER
スケールタイプを適用して、カメラ画像のアスペクト比を維持します。これにより、画像は中央に配置されますが、PreviewView
のサイズに合わせてトリミングされる場合があります。カメラ画像をレターボックス表示するには、スケールタイプを FIT_CENTER
に設定します。
PreviewView
を使用してカメラ プレビューを作成する基本的な情報については、プレビューを実装するをご覧ください。
完全なサンプル実装については、GitHub の CameraXBasic
リポジトリをご覧ください。
カメラビューファインダー
プレビューのユースケースと同様に、CameraViewfinder ライブラリには、カメラ プレビューの作成を簡素化するツールセットが用意されています。CameraX Core に依存しないため、既存の Camera2 コードベースにシームレスに統合できます。
Surface
を直接使用する代わりに、CameraViewfinder
ウィジェットを使用して Camera2 のカメラフィードを表示できます。
CameraViewfinder
は内部で TextureView
または SurfaceView
を使用してカメラフィードを表示し、必要な変換を適用してビューファインダーを正しく表示します。この操作では、アスペクト比、縮尺、回転を修正します。
CameraViewfinder
オブジェクトからサーフェスをリクエストするには、ViewfinderSurfaceRequest
を作成する必要があります。
このリクエストには、CameraCharacteristics
からのサーフェス解像度とカメラデバイス情報の要件が含まれます。
requestSurfaceAsync()
を呼び出すと、サーフェス プロバイダ(TextureView
または SurfaceView
)にリクエストが送信され、Surface
の ListenableFuture
を取得します。
markSurfaceSafeToRelease()
を呼び出すと、サーフェスは不要であり、関連リソースを解放できることがサーフェス プロバイダに通知されます。
Kotlin
fun startCamera(){ val previewResolution = Size(width, height) val viewfinderSurfaceRequest = ViewfinderSurfaceRequest(previewResolution, characteristics) val surfaceListenableFuture = cameraViewfinder.requestSurfaceAsync(viewfinderSurfaceRequest) Futures.addCallback(surfaceListenableFuture, object : FutureCallback<Surface> { override fun onSuccess(surface: Surface) { /* create a CaptureSession using this surface as usual */ } override fun onFailure(t: Throwable) { /* something went wrong */} }, ContextCompat.getMainExecutor(context)) }
Java
void startCamera(){ Size previewResolution = new Size(width, height); ViewfinderSurfaceRequest viewfinderSurfaceRequest = new ViewfinderSurfaceRequest(previewResolution, characteristics); ListenableFuture<Surface> surfaceListenableFuture = cameraViewfinder.requestSurfaceAsync(viewfinderSurfaceRequest); Futures.addCallback(surfaceListenableFuture, new FutureCallback<Surface>() { @Override public void onSuccess(Surface result) { /* create a CaptureSession using this surface as usual */ } @Override public void onFailure(Throwable t) { /* something went wrong */} }, ContextCompat.getMainExecutor(context)); }
SurfaceView
プレビューに処理が不要で、アニメーションを使用しない場合は、SurfaceView
で簡単にカメラ プレビューを作成できます。
SurfaceView
は、センサーの向きとデバイスの回転の両方を考慮して、ディスプレイの向きに合わせてカメラセンサーの画像バッファを自動的に回転させます。ただし、画像バッファは、アスペクト比を考慮せずに SurfaceView
サイズに合わせてスケーリングされます。
画像バッファのアスペクト比を SurfaceView
のアスペクト比と一致させる必要があります。これは、コンポーネントの onMeasure()
メソッドで SurfaceView
のコンテンツをスケーリングすることで実現できます。
(computeRelativeRotation()
ソースコードは、以下の相対ローテーションにあります)。
Kotlin
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val width = MeasureSpec.getSize(widthMeasureSpec) val height = MeasureSpec.getSize(heightMeasureSpec) val relativeRotation = computeRelativeRotation(characteristics, surfaceRotationDegrees) if (previewWidth > 0f && previewHeight > 0f) { /* Scale factor required to scale the preview to its original size on the x-axis. */ val scaleX = if (relativeRotation % 180 == 0) { width.toFloat() / previewWidth } else { width.toFloat() / previewHeight } /* Scale factor required to scale the preview to its original size on the y-axis. */ val scaleY = if (relativeRotation % 180 == 0) { height.toFloat() / previewHeight } else { height.toFloat() / previewWidth } /* Scale factor required to fit the preview to the SurfaceView size. */ val finalScale = min(scaleX, scaleY) setScaleX(1 / scaleX * finalScale) setScaleY(1 / scaleY * finalScale) } setMeasuredDimension(width, height) }
Java
@Override void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); int relativeRotation = computeRelativeRotation(characteristics, surfaceRotationDegrees); if (previewWidth > 0f && previewHeight > 0f) { /* Scale factor required to scale the preview to its original size on the x-axis. */ float scaleX = (relativeRotation % 180 == 0) ? (float) width / previewWidth : (float) width / previewHeight; /* Scale factor required to scale the preview to its original size on the y-axis. */ float scaleY = (relativeRotation % 180 == 0) ? (float) height / previewHeight : (float) height / previewWidth; /* Scale factor required to fit the preview to the SurfaceView size. */ float finalScale = Math.min(scaleX, scaleY); setScaleX(1 / scaleX * finalScale); setScaleY(1 / scaleY * finalScale); } setMeasuredDimension(width, height); }
SurfaceView
をカメラ プレビューとして実装する方法については、カメラの向きをご覧ください。
TextureView
TextureView
は SurfaceView
よりパフォーマンスは劣りますが、作業量は多くなりますが、TextureView
を使用することでカメラ プレビューを最大限に制御できます。
TextureView
は、センサーの向きに基づいてセンサーの画像バッファを回転しますが、デバイスの回転やプレビューのスケーリングは処理しません。
スケーリングと回転は行列変換でエンコードできます。TextureView
のスケーリングと回転を正しく行う方法については、カメラアプリでサイズ変更可能なサーフェスをサポートするをご覧ください。
相対回転
カメラセンサーの相対回転は、カメラセンサーの出力をデバイスの向きに合わせるために必要な回転の量です。
相対回転は、SurfaceView
や TextureView
などのコンポーネントで使用され、プレビュー画像の x スケーリング ファクタと y スケーリング ファクタが決定されます。また、センサー イメージ バッファの回転の指定にも使用されます。
CameraCharacteristics
クラスと Surface
クラスを使用すると、カメラセンサーの相対回転を計算できます。
Kotlin
/** * Computes rotation required to transform the camera sensor output orientation to the * device's current orientation in degrees. * * @param characteristics The CameraCharacteristics to query for the sensor orientation. * @param surfaceRotationDegrees The current device orientation as a Surface constant. * @return Relative rotation of the camera sensor output. */ public fun computeRelativeRotation( characteristics: CameraCharacteristics, surfaceRotationDegrees: Int ): Int { val sensorOrientationDegrees = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! // Reverse device orientation for back-facing cameras. val sign = if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT ) 1 else -1 // Calculate desired orientation relative to camera orientation to make // the image upright relative to the device orientation. return (sensorOrientationDegrees - surfaceRotationDegrees * sign + 360) % 360 }
Java
/** * Computes rotation required to transform the camera sensor output orientation to the * device's current orientation in degrees. * * @param characteristics The CameraCharacteristics to query for the sensor orientation. * @param surfaceRotationDegrees The current device orientation as a Surface constant. * @return Relative rotation of the camera sensor output. */ public int computeRelativeRotation( CameraCharacteristics characteristics, int surfaceRotationDegrees ){ Integer sensorOrientationDegrees = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); // Reverse device orientation for back-facing cameras. int sign = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT ? 1 : -1; // Calculate desired orientation relative to camera orientation to make // the image upright relative to the device orientation. return (sensorOrientationDegrees - surfaceRotationDegrees * sign + 360) % 360; }
ウィンドウ指標
画面サイズは、カメラのビューファインダーの寸法の決定には使用しないでください。カメラアプリは、モバイル デバイスではマルチウィンドウ モード、ChromeOS ではフリーフレート モードで、画面の一部で実行される可能性があります。
WindowManager#getCurrentWindowMetrics()
(API レベル 30 で追加)は、画面のサイズではなく、アプリ ウィンドウのサイズを返します。Jetpack WindowManager ライブラリのメソッド WindowMetricsCalculator#computeCurrentWindowMetrics()
と WindowInfoTracker#currentWindowMetrics()
も、API レベル 14 との下位互換性を備えた同様のサポートを提供します。
180 度回転
デバイスを 180 度回転(たとえば、自然な向きから自然な向きへ)しても、onConfigurationChanged()
コールバックはトリガーされません。その結果、カメラのプレビューが逆向きになることがあります。
180 度の回転を検出するには、DisplayListener
を実装し、onDisplayChanged()
コールバックで Display#getRotation()
を呼び出してデバイスの回転を確認します。
専用リソース
Android 10 より前のバージョンでは、マルチウィンドウ環境で最初に表示されるアクティビティのみが RESUMED
状態になっていました。これは、どのアクティビティが再開されるかをシステムが示していないため、ユーザーを混乱させていました。
Android 10(API レベル 29)では、表示されるすべてのアクティビティが RESUMED
状態である、複数のアプリの再開が導入されました。たとえば、透明なアクティビティがアクティビティの上に配置されている場合や、ピクチャー イン ピクチャー モードなどでアクティビティをフォーカスできない場合(ピクチャー イン ピクチャーのサポートを参照)、表示されるアクティビティが PAUSED
状態になることもあります。
カメラ、マイク、API レベル 29 以降の専用リソースまたはシングルトン リソースを使用するアプリは、複数のアプリの再開をサポートする必要があります。たとえば、再開された 3 つのアクティビティがカメラを使用する場合、1 つのアクティビティだけがこの専用リソースにアクセスできます。各アクティビティは、優先度の高いアクティビティによるカメラへのプリエンプティブ アクセスを認識するため、onDisconnected()
コールバックを実装する必要があります。
詳細については、複数のアプリの再開をご覧ください。
参考情報
- Camera2 の例については、GitHub の Camera2Basic アプリをご覧ください。
- CameraX のプレビューのユースケースについては、CameraX のプレビューを実装するをご覧ください。
- CameraX カメラのプレビューの実装例については、GitHub の CameraXBasic リポジトリをご覧ください。
- ChromeOS でのカメラ プレビューについては、カメラの向きをご覧ください。
- 折りたたみ式デバイスの開発については、折りたたみ式デバイスについてをご覧ください。