Android Java FragmentでCameraXライブラリ使ってみた

Google I/O 2019で紹介されたCameraXをJavaでFragment上で試してみたので簡単にご紹介。


##準備

公式の[チュートリアル](https://codelabs.developers.google.com/codelabs/camerax-getting-started/#0)をみながら進めていくと楽チンなのでこちらを見ながら進めていきます。

プロジェクト作成や、dependenciesの追加などはチュートリアル通りに進めるだけなので本記事では割愛。



##プレビュー用のビューを追加
Fragmentで表示するlayout.xmlにTextureViewを追加します。

```xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFFFFF">

    <TextureView
        android:id="@+id/view_finder"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <RelativeLayout
        android:id="@+id/camera_header"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#456700"
        android:padding="15dp">

        <ImageButton
            android:id="@+id/camera_back"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/common_backbtn"
            android:layout_alignParentLeft="true"/>

    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/camera_bottom_control"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:background="#456700"
        android:padding="20dp">

        <Button
            android:id="@+id/capture_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="撮影する"
            />
        
    </RelativeLayout>

</RelativeLayout>
```

見た目はこんな感じ↓
<img width="271" alt="スクリーンショット 2019-12-12 15.20.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218649/4ead82b3-f7a9-3345-3467-3caf3bdc655f.png">

TextureViewは全面に配置してその上に画面要素を重ねて表示しています。

##カメラを起動

プレビューの設定を行ってフラグメントのライフサイクルとバインドするだけです。

CameraX.bindToLifecycleで作成したPreviewConfigをバインドするだけなので簡単ですね。

```java
public void startCamera() {
	PreviewConfig.Builder builder = new PreviewConfig.Builder();

	builder.setTargetResolution(new Size(App.width, App.height));
	PreviewConfig previewConfig = builder.build();

	preview = new Preview(previewConfig);
	preview.setOnPreviewOutputUpdateListener(new Preview.OnPreviewOutputUpdateListener() {
		@Override
		public void onUpdated(@NonNull Preview.PreviewOutput output) {
			ViewGroup viewGroup = (ViewGroup) viewFinder.getParent();
			viewGroup.removeView(viewFinder);
			viewGroup.addView(viewFinder, 0);

			viewFinder.setSurfaceTexture(output.getSurfaceTexture());
			updateTransform();
		}
	});
	CameraX.bindToLifecycle(this, preview);
}
    
public void updateTransform() {
	Matrix matrix = new Matrix();

	float centerX = viewFinder.getWidth() / 2f;
	float centerY = viewFinder.getHeight() / 2f;

	float rotationDegrees;
	switch (viewFinder.getDisplay().getRotation()) {
		case Surface.ROTATION_0:
			rotationDegrees = 0f;
			break;
		case Surface.ROTATION_90:
			rotationDegrees = 90f;
			break;
		case Surface.ROTATION_180:
			rotationDegrees = 180f;
			break;
		case Surface.ROTATION_270:
			rotationDegrees = 270f;
			break;
		default:
			return;
	}

	matrix.postRotate(-rotationDegrees, centerX, centerY);
	viewFinder.setTransform(matrix);
}
```
基本的にはこれだけ書いて、カメラのパーミッション取得後にstartCameraを呼び出せばプレビューが表示されます。

##カメラのキャプチャ
カメラのプレビューをするだけじゃ何にも使えないんでcapture_buttonが押されたらその瞬間の画像をキャプチャしてみましょう。

上記startCameraメソッドのバインド前に下記コードを追加します。

```java
ImageCaptureConfig.Builder imageBuilder = new ImageCaptureConfig.Builder();
imageBuilder.setTargetResolution(new Size(App.width, App.height));
imageBuilder.setCaptureMode(ImageCapture.CaptureMode.MAX_QUALITY);
imageBuilder.setFlashMode(FlashMode.ON);
ImageCaptureConfig imageCaptureConfig = imageBuilder.build();
imageCapture = new ImageCapture(imageCaptureConfig);
```
画像キャプチャの設定をしています。
ここではFlashをONにしてクオリティも最大にしています。
ImageCaptureConfigを作成したら、バインドメソッドにImageCaptureConfigも追加しましょう。

```java
CameraX.bindToLifecycle(this, preview, imageCapture);
```

これで準備は完了です。

imageCapture.takePicture()を呼べばキャプチャができるのでボタンにイベントを設定しましょう。

```java
btnCamera.setOnClickListener((View v) -> {

	imageCapture.takePicture(CameraFragment.this, new ImageCapture.OnImageCapturedListener() {
			@Override
			public void onCaptureSuccess(ImageProxy imageProxy, int rotationDegrees) {

			}

			@Override
			public void onError(
				@NonNull ImageCapture.ImageCaptureError imageCaptureError, @NonNull String message,
								@Nullable Throwable cause) {

			}
		});
	}
});
	
```
	
takePictureの引数にはExecutorを求められるのでここではFragmentにExecutorをimplementsして写真撮影後の処理をUiThreadで処理を実行できるようにしておきます。

```java

public class CameraFragment implements Executor {

    @Override
    public void execute(Runnable command) {
		if(getActivity() != null){
			getActivity().runOnUiThread(command);
		}
	}
}
```

撮影ボタンを押して撮影に成功するとonCaptureSuccess(ImageProxy imageProxy, int rotationDegrees)が呼ばれてimageProxyから画像が取得できました！

##感想
いやあ簡単ですね...

Camera2でゴリゴリ書いてたのはなんだったのか。

カメラの実装地味に大変なのでこういうの待ってたの一言です。

##ハマりポイント
さて、非常に簡単そうに記載してきましたが、いくつかハマりポイントがあったのでご紹介していきます。

###①シャッターオンが鳴らない！

シャッター音が鳴らないんですよ。はい。

キャプチャ設定でなんか設定できるかなと探したんですが、どこにもないんですよね...

何としても鳴らしたい人は無理やりこう。

```java
MediaActionSound sound = new MediaActionSound();
sound.play(MediaActionSound.SHUTTER_CLICK);
```

###②ImageProxyどうやってBitmapに変換すんの？

はい、これです。

```java
public Bitmap imageProxyToBitmap(ImageProxy image, int rotationDegrees) {
	ImageProxy.PlaneProxy planeProxy = image.getPlanes()[0];
	ByteBuffer buffer = planeProxy.getBuffer();
	byte[] bytes = new byte[buffer.remaining()];
	buffer.get(bytes);

	Matrix mat = new Matrix();
	mat.postRotate(rotationDegrees);

	if (rotationDegrees != 0) {
		return rotate(BitmapFactory.decodeByteArray(bytes, 0, bytes.length), rotationDegrees);
	} else {
		return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
	}
}

public Bitmap rotate(Bitmap in, int angle) {
		Matrix mat = new Matrix();
		mat.postRotate(angle);
		return Bitmap.createBitmap(in, 0, 0, in.getWidth(), in.getHeight(), mat, true);
}
```
	
###③TextureViewの配置の仕方

Q.レイアウトファイルでTextureViewを全面においてるのなんで？プレビュー領域に合わせておいちゃダメなの？

A.あえて全面においてます。

PreviewサイズとTextureViewのサイズ縦横比が一致しないとプレビューが歪んで表示される問題や、画面サイズに対して小さいプレビュー領域の画面を作成した場合、TextureViewをプレビュー領域に合わせて作っていると撮影したものがそのTextureView縮小されて表示される為プレビューが非常に見えづらくなってしまいます。

TextureViewを全面においておけば上記問題は全て解決され、普段使っている全面のカメラと同じ感覚で撮影できるので違和感なく撮影できちゃいます。

ユーザから見えてる領域だけの画像が欲しければ、撮影後に切り抜けばOKです。

##終わりに

CameraXはまだアルファ版なのでリリース版ではインターフェースが変わる可能性があるので注意が必要です。

またここでは紹介していませんが、デバイスに固有のベンダー効果（ピンぼけ、HDR、夜景）なんかも簡単に使用できるAPIが用意されているようなのでそちらも今後試してみたいと思います。