How can I use a CameraView with Jetpack Compose?
Asked Answered
N

4

19

Currently there's no equivalent to CameraView (and PreviewView) in Compose. Is it possible to wrap it and display it in a compose layout?

Northwards answered 14/5, 2020 at 10:50 Comment(4)
I haven't tried my self, but there is a sample for webView: android.googlesource.com/platform/frameworks/support/+/… android.googlesource.com/platform/frameworks/support/+/…Chenopod
May you specify this a bit?Fancied
@pentexnyx I added an answer with more details.Chenopod
@HabibKazemi great one (upvoted) - but I was talking to icefex (i didn't mention him though, my bad)Fancied
C
10

At the moment there isn't any official Composable function for CameraX so we have to inflate the legacy android view inside compose.

To achieve that we can use AndroidView composable function, it accepts two parameters

  • @param resId The id of the layout resource to be inflated.
  • @param postInflationCallback The callback to be invoked after the layout is inflated.

and to access the lifecycle and context we use the ambients

val lifecycleOwner = LifecycleOwnerAmbient.current
val context = ContextAmbient.current

As we have everything we need let's do it:

Create a layout camera_host.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.camera.view.PreviewView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/previewView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

and inflate it using AndroidView Composable function.

@Composable
fun SimpleCameraPreview() {
    val lifecycleOwner = LifecycleOwnerAmbient.current
    val context = ContextAmbient.current
    val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
    AndroidView(resId = R.layout.camera_host) { inflatedLayout ->
       //You can call
      // findViewById<>() and etc ... on inflatedLayout
      // here PreviewView is the root of my layout so I just cast it to
      // the PreviewView and no findViewById is required

        cameraProviderFuture.addListener(Runnable {
            val cameraProvider = cameraProviderFuture.get()
            bindPreview(
                 lifecycleOwner,
                 inflatedLayout as PreviewView /*the inflated layout*/,
                 cameraProvider)
        }, ContextCompat.getMainExecutor(context))

    }
}

fun bindPreview(
    lifecycleOwner: LifecycleOwner,
    previewView: PreviewView,
    cameraProvider: ProcessCameraProvider
) {
    var preview: Preview = Preview.Builder().build()

    var cameraSelector: CameraSelector = CameraSelector.Builder()
        .requireLensFacing(CameraSelector.LENS_FACING_BACK)
        .build()

    preview.setSurfaceProvider(previewView.createSurfaceProvider())

    var camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview)
}
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SimpleCameraPreview()
        }
    }
}
Chenopod answered 12/6, 2020 at 5:20 Comment(0)
M
14

There is still no CameraX composable. You need to use AndroidView to create one.

Updated example for Compose 1.0.0-beta02:

@Composable
fun CameraPreview(
    modifier: Modifier = Modifier,
    cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA,
    scaleType: PreviewView.ScaleType = PreviewView.ScaleType.FILL_CENTER,
) {
    val lifecycleOwner = LocalLifecycleOwner.current
    AndroidView(
        modifier = modifier,
        factory = { context ->
            val previewView = PreviewView(context).apply {
                this.scaleType = scaleType
                layoutParams = ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT
                )
                // Preview is incorrectly scaled in Compose on some devices without this
                implementationMode = PreviewView.ImplementationMode.COMPATIBLE
            }

            val cameraProviderFuture = ProcessCameraProvider.getInstance(context)

            cameraProviderFuture.addListener({
                val cameraProvider = cameraProviderFuture.get()

                // Preview
                val preview = Preview.Builder()
                    .build()
                    .also {
                        it.setSurfaceProvider(previewView.surfaceProvider)
                    }

                try {
                    // Must unbind the use-cases before rebinding them.
                    cameraProvider.unbindAll()

                    cameraProvider.bindToLifecycle(
                        lifecycleOwner, cameraSelector, preview
                    )
                } catch (exc: Exception) {
                    Log.e(TAG, "Use case binding failed", exc)
                }
            }, ContextCompat.getMainExecutor(context))

            previewView
        })
}
Martelli answered 23/3, 2021 at 13:11 Comment(3)
This is along the lines of what I was hoping to find. One question: ProcessCameraProvider appears to be undefined. I've included androidx.camera:camera-lifecycle:1.1.0-alpha3 based on the reference docs here. What library and version are you using for ProcessCameraProvider? Thanks!Michel
My dependencies are: implementation "androidx.camera:camera-camera2:1.0.0-rc04" implementation "androidx.camera:camera-lifecycle:1.0.0-rc04" implementation "androidx.camera:camera-view:1.0.0-alpha23" in addition to plenty of other androidx dependencies, but I think those should do it. I would imagine that 1.1.0 would work as well, but I haven't tried it.Martelli
Thanks. I just got it working with 1.1.0-alpha03 from Maven and was on my way back to report. What a time to be alive! :) mvnrepository.com/artifact/androidx.camera/camera-lifecycleMichel
C
10

At the moment there isn't any official Composable function for CameraX so we have to inflate the legacy android view inside compose.

To achieve that we can use AndroidView composable function, it accepts two parameters

  • @param resId The id of the layout resource to be inflated.
  • @param postInflationCallback The callback to be invoked after the layout is inflated.

and to access the lifecycle and context we use the ambients

val lifecycleOwner = LifecycleOwnerAmbient.current
val context = ContextAmbient.current

As we have everything we need let's do it:

Create a layout camera_host.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.camera.view.PreviewView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/previewView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

and inflate it using AndroidView Composable function.

@Composable
fun SimpleCameraPreview() {
    val lifecycleOwner = LifecycleOwnerAmbient.current
    val context = ContextAmbient.current
    val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
    AndroidView(resId = R.layout.camera_host) { inflatedLayout ->
       //You can call
      // findViewById<>() and etc ... on inflatedLayout
      // here PreviewView is the root of my layout so I just cast it to
      // the PreviewView and no findViewById is required

        cameraProviderFuture.addListener(Runnable {
            val cameraProvider = cameraProviderFuture.get()
            bindPreview(
                 lifecycleOwner,
                 inflatedLayout as PreviewView /*the inflated layout*/,
                 cameraProvider)
        }, ContextCompat.getMainExecutor(context))

    }
}

fun bindPreview(
    lifecycleOwner: LifecycleOwner,
    previewView: PreviewView,
    cameraProvider: ProcessCameraProvider
) {
    var preview: Preview = Preview.Builder().build()

    var cameraSelector: CameraSelector = CameraSelector.Builder()
        .requireLensFacing(CameraSelector.LENS_FACING_BACK)
        .build()

    preview.setSurfaceProvider(previewView.createSurfaceProvider())

    var camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview)
}
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SimpleCameraPreview()
        }
    }
}
Chenopod answered 12/6, 2020 at 5:20 Comment(0)
W
6

This is my snippet (based on Sean's answer), which also handles torch state and resource disposition and adds a focus on tap logic. Dependencies:

implementation 'androidx.camera:camera-camera2:1.1.0-alpha11'
implementation 'androidx.camera:camera-view:1.0.0-alpha31'
implementation 'androidx.camera:camera-lifecycle:1.1.0-alpha11'

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.0-RC'
@Composable
fun CameraPreview(
    modifier: Modifier = Modifier,
    cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA,
    implementationMode: PreviewView.ImplementationMode = PreviewView.ImplementationMode.COMPATIBLE,
    scaleType: PreviewView.ScaleType = PreviewView.ScaleType.FILL_CENTER,
    imageAnalysis: ImageAnalysis? = null,
    imageCapture: ImageCapture? = null,
    preview: Preview = remember { Preview.Builder().build() },
    enableTorch: Boolean = false,
    focusOnTap: Boolean = false
) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    val cameraProvider by produceState<ProcessCameraProvider?>(initialValue = null) {
        value = ProcessCameraProvider.getInstance(context).await()
    }

    // TODO: add cameraSelector
    val camera = remember(cameraProvider) {
        cameraProvider?.let {
            it.unbindAll()
            it.bindToLifecycle(
                lifecycleOwner,
                cameraSelector,
                *listOfNotNull(imageAnalysis, imageCapture, preview).toTypedArray()
            )
        }
    }

    LaunchedEffect(camera, enableTorch) {
        camera?.let {
            if (it.cameraInfo.hasFlashUnit()) {
                it.cameraControl.enableTorch(enableTorch).await()
            }
        }
    }

    DisposableEffect(Unit) {
        onDispose {
            cameraProvider?.unbindAll()
        }
    }

    AndroidView(
        modifier = modifier.pointerInput(camera, focusOnTap) {
            if (!focusOnTap) return@pointerInput

            detectTapGestures {
                val meteringPointFactory = SurfaceOrientedMeteringPointFactory(
                    size.width.toFloat(),
                    size.height.toFloat()
                )

                val meteringAction = FocusMeteringAction.Builder(
                    meteringPointFactory.createPoint(it.x, it.y),
                    FocusMeteringAction.FLAG_AF
                ).disableAutoCancel().build()

                camera?.cameraControl?.startFocusAndMetering(meteringAction)
            }
        },
        factory = { _ ->
            PreviewView(context).also {
                it.scaleType = scaleType
                it.implementationMode = implementationMode
                preview.setSurfaceProvider(it.surfaceProvider)
            }
        }
    )
}
Westernmost answered 10/12, 2021 at 10:4 Comment(1)
thanks this is really helpful while enabling torch without recomposition!Oar
S
1

I've created a library to use CameraX in Jetpack Compose. It might be useful until an official library comes out.

https://github.com/skgmn/CameraXX

In your build.gradle, (requires GitHub personal access token)

implementation "com.github.skgmn:cameraxx-composable:0.3.0"

Composable method signature

CameraPreview(
    modifier: Modifier = Modifier,
    cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA,
    preview: Preview?,
    imageCapture: ImageCapture? = null,
    imageAnalysis: ImageAnalysis? = null
)

You can omit preview parameter to use default Preview instance.

An example

class MainViewModel : ViewModel() {
    val imageCapture = ImageCapture.Builder().build()
}

@Composable
fun Main() {
    val viewModel: MainViewModel = viewModel()
    val imageCapture by remember { viewModel.imageCapture }

    CameraPreview(Modifier.fillMaxSize(), imageCapture)
}
Stemma answered 23/8, 2021 at 12:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.