cameraxをComposeでいい感じに取り扱いたい人の戦闘ログ
モチベーション
- camerax使いにくすぎる & Composeで書きたいのでうまいことラップしたい
- camerax触ったことある人もない人も学習コストが低くなるように己の思想を極力入れない
結論
色々用意することで以下のような感じでシンプルにcameraxを使えそう。
var imageCapture by remember { mutableStateOf<ImageCapture?>(null) }
val scope = rememberCoroutineScope()
CameraPreview(
onBind = {
val preview = previewUseCase()
val analysis = imageAnalysisUseCase(executor) {}
imageCapture = imageCaptureUseCase()
cameraProvider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
analysis,
imageCapture,
)
},
)
// ボタンなどのクリック時
scope.launch {
val bitmap = imageCapture.takePicture().toBitmap()
}
用意するコード
@Composable
fun CameraPreview(
onBind: OnBindScope.() -> Unit,
onInitPreviewView: PreviewView.() -> Unit = {},
) {
var bindingState by remember {
mutableStateOf<BindingState>(BindingState.Initial)
}
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
AndroidView(
factory = {
PreviewView(it)
.apply {
scaleType = PreviewView.ScaleType.FILL_CENTER
layoutParams =
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
}.apply(onInitPreviewView)
},
update = { previewView ->
bindingState =
when (bindingState) {
is BindingState.Initial ->
try {
val cameraProvider =
ProcessCameraProvider
.getInstance(
context,
).get()
cameraProvider.unbindAll()
OnBindScope(
cameraProvider = cameraProvider,
previewView = previewView,
context = context,
lifecycleOwner = lifecycleOwner,
).onBind()
BindingState.Success
} catch (e: Exception) {
BindingState.Failed
}
is BindingState.Finish -> bindingState
}
},
)
}
private sealed interface BindingState {
data object Initial : BindingState
interface Finish : BindingState
data object Failed : Finish
data object Success : Finish
}
data class OnBindScope(
val cameraProvider: ProcessCameraProvider,
val previewView: PreviewView,
val context: Context,
val lifecycleOwner: LifecycleOwner,
)
// UseCase helpers
fun OnBindScope.previewUseCase(builder: Preview.Builder.() -> Preview.Builder = { this }) =
previewUseCase(
builder = builder,
previewView = previewView,
)
fun previewUseCase(
builder: Preview.Builder.() -> Preview.Builder = { this },
previewView: PreviewView,
) = Preview.Builder()
.builder()
.build()
.apply {
setSurfaceProvider(previewView.surfaceProvider)
}
fun imageAnalysisUseCase(
executor: Executor,
builder: ImageAnalysis.Builder.() -> ImageAnalysis.Builder = { this },
analyzer: ImageAnalysis.Analyzer,
) = ImageAnalysis.Builder()
.builder()
.build()
.apply {
setAnalyzer(executor, analyzer)
}
fun imageCaptureUseCase(builder: ImageCapture.Builder.() -> ImageCapture.Builder = { this }) =
ImageCapture.Builder()
.builder()
.build()
suspend fun ImageCapture.takePicture(executor: Executor): ImageProxy =
suspendCancellableCoroutine { continuation ->
takePicture(
executor,
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
super.onCaptureSuccess(image)
continuation.resume(image)
}
override fun onError(exception: ImageCaptureException) {
super.onError(exception)
continuation.cancel(exception)
}
},
)
}
suspend fun ImageCapture.takePicture(
outputFileOptions: ImageCapture.OutputFileOptions,
executor: Executor,
): ImageCapture.OutputFileResults =
suspendCancellableCoroutine { continuation ->
takePicture(
outputFileOptions,
executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
continuation.resume(outputFileResults)
}
override fun onError(exception: ImageCaptureException) {
continuation.cancel(exception)
}
},
)
}
良さげなのを教えてくれた人ありけり
Qualityとかまでは頭が回らないけど参考にしながら一旦自分なりにラップしてみるの巻。
メインコンセプト
fatにならないように意識しながら組んでみる。
ポイントは初期化処理を onBind
コールバックで持ってくることで、呼び出し側で指定できるようにしたところ。
同じCameraPreviewの呼び出しでも画面によって使用するUseCaseなどは違うのでそれらを都度 onBind
で指定させることで画面ごとに違うんやでができるようになる。
@Composable
fun CameraPreview(
onBind: OnBindScope.() -> Unit,
) {
var bindingState by remember {
mutableStateOf<BindingState>(BindingState.Initial)
}
// ...
AndroidView(
factory = {
PreviewView(it)
.apply {
// TODO 基本的な設定
}
},
update = { previewView ->
bindingState =
when (bindingState) {
is BindingState.Initial ->
try {
val cameraProvider =
ProcessCameraProvider
.getInstance(
context,
).get()
cameraProvider.unbindAll()
onBind()
BindingState.Success
} catch (e: Exception) {
BindingState.Failed
}
is BindingState.Finish -> bindingState
}
},
)
}
@Composable
fun CameraPreviewSample() {
CameraPreview(
onBind = {
// UseCaseはしやすいようにヘルパーを用意(後述)
val preview = previewUseCase() // onBindコールバック内のpreviewUseCase() はいい感じにsetSurfaceしてくれる
val analysis = imageAnalysisUseCase(executor) {} // コールバックが書きやすいでしょ?
val imageCapture = imageCaptureUseCase(executor)
// other UseCases ...
cameraProvider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
analysis,
imageCapture,
)
},
)
// TODO ボタンクリック時などにimageCapture.takePictureとかする
}
ダッサいコールバックをなんとかする
ImageCapture.takePictureが ダッサいコールバックになってるのでどうにかしたい。
imageCapture.takePicture(
executor,
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
super.onCaptureSuccess(image)
continuation.resume(image)
}
override fun onError(exception: ImageCaptureException) {
super.onError(exception)
continuation.cancel(exception)
}
},
)
flow { } とかでいけそうかと思ったら意外とそうもいかず(emitがsuspendだったりするので)。
ChatGPTに聞いたら suspendCancellableCoroutine
が良さげっぽいとのこと。
suspend fun ImageCapture.takePicture(executor: Executor): ImageProxy =
suspendCancellableCoroutine { continuation ->
takePicture(
executor,
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
super.onCaptureSuccess(image)
continuation.resume(image)
}
override fun onError(exception: ImageCaptureException) {
super.onError(exception)
continuation.cancel(exception)
}
},
)
}
これで呼び出し側からは以下のように呼べる👍
someScope.launch {
val result :ImageProxy = imageCapture.takePicture(executor)
}
ファイルに保存するバージョンはこんな感じ
suspend fun ImageCapture.takePicture(
outputFileOptions: ImageCapture.OutputFileOptions,
executor: Executor,
): ImageCapture.OutputFileResults =
suspendCancellableCoroutine { continuation ->
takePicture(
outputFileOptions,
executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
continuation.resume(outputFileResults)
}
override fun onError(exception: ImageCaptureException) {
continuation.cancel(exception)
}
},
)
}
「コールバック→coroutineで置き換えは suspendCancellableCoroutine
が便利」←これをしっかり覚えておこう
suspendCoroutineなるものもあるらしい。
両者の違いを調べてみたいゾゾゾ。
UseCase()
のラップ
ことの発端はPreview UseCaseの setSurfaceProvider
呼び忘れがち問題。これを解決するために悩んだ結果、 onBind内で previewUseCase()
経由で取得すれば setSurfaceProvider
呼んでおいてあげるで な実装になった。
↑賛否両論はありそう。setSurfaceProvider知ってる他の人が見たらびっくりしかねない挙動ではある。(ただ万が一複数回setSurfaceProviderしても問題ない認識)
そのために CameraPreviewの onBind
コールバックを以下のように変更して、
@Composable
fun CameraPreview(
+ onBind: OnBindScope.() -> Unit,
) {
...
update = { previewView ->
// 初期化時
cameraProvider.unbindAll()
- onBind()
+ OnBindScope(
+ previewView = previewView,
+ ).onBind()
...
}
...
}
+ data class OnBindScope(
+ val cameraProvider: ProcessCameraProvider,
+ val previewView: PreviewView,
+ val context: Context,
+ val lifecycleOwner: LifecycleOwner,
+ )
こんな拡張関数を生やす
fun OnBindScope.previewUseCase() =
Preview.Builder()
.build()
.apply {
setSurfaceProvider(previewView.surfaceProvider)
}
するとOnBindScope内(=onBind内
)でpreviewUseCaseを呼ぶと previewViewでsetSurfaceProvider(previewView.surfaceProvider)
されたPreviewが取得できる😄
CameraPreview(
onBind = {
val preview = previewUseCase()
cameraProvider.bindToLifecycle(... , preview)
...
previewUseCaseみたいな関数を UseCase helper
と名付けておく。
UseCase のBuilderでカスタムできるようにもしておく
実際には Preview.Builder()
を使って何かしら設定をしたいことがありそう(setCameraSelector()
とか)。
なのでオプショナル引数builderを好き勝手いじれるようにしておく。
fun OnBindScope.previewUseCase(
+ builder: Preview.Builder.() -> Preview.Builder = { this },
) =
Preview.Builder()
+ .builder()
.build()
.apply {
setSurfaceProvider(previewView.surfaceProvider)
}
他のUseCaseのUseCase helper
previewだけ previewUseCase()
呼ぶ方式だと他のUseCaseと乖離しちゃいそうなので、他のUseCaseのUseCase helper
も用意してみる。(こっちは単に実装が短くなったね程度にしか恩恵はない)
fun imageAnalysisUseCase(
executor: Executor,
builder: ImageAnalysis.Builder.() -> ImageAnalysis.Builder = { this },
analyzer: ImageAnalysis.Analyzer,
) = ImageAnalysis.Builder()
.builder()
.build()
.apply {
setAnalyzer(executor, analyzer)
}
fun imageCaptureUseCase(builder: ImageCapture.Builder.() -> ImageCapture.Builder = { this }) =
ImageCapture.Builder()
.builder()
.build()
これで完成じゃ。
勉強会登壇した