Skip to content

CameraΒΆ

Haze can apply Blur and Glass to a live camera preview when the preview draws into Compose, so hazeSource can capture it.

This example uses HazeInput.Sources, which captures exactly the content marked with hazeSource. HazeInput.Backdrop reads only pixels drawn earlier in the same window, so it cannot include a preview on a separate surface or window.

val hazeState = rememberHazeState()

Box {
  CameraPreview(
    modifier = Modifier
      .fillMaxSize()
      .hazeSource(hazeState),
  )

  Box(
    modifier = Modifier
      .align(Alignment.Center)
      .size(200.dp)
      .hazeBlur(
        input = HazeInput.Sources(hazeState),
        style = HazeMaterials.ultraThin(),
      ),
  )
}

What matters is how CameraPreview renders. Wrapping a platform view in AndroidView does not by itself make its pixels capturable.

CameraX on AndroidΒΆ

CameraX's PreviewView uses ImplementationMode.PERFORMANCE by default, which renders through a SurfaceView when possible. A SurfaceView draws on its own surface, so Haze cannot capture it.

Use ImplementationMode.COMPATIBLE, which makes PreviewView use a TextureView:

val cameraController = remember(context) {
  LifecycleCameraController(context).apply {
    cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
  }
}

DisposableEffect(cameraController, lifecycleOwner) {
  cameraController.bindToLifecycle(lifecycleOwner)
  onDispose { cameraController.unbind() }
}

AndroidView(
  factory = { previewContext ->
    PreviewView(previewContext).apply {
      implementationMode = PreviewView.ImplementationMode.COMPATIBLE
      scaleType = PreviewView.ScaleType.FILL_CENTER
      controller = cameraController
    }
  },
  modifier = Modifier
    .fillMaxSize()
    .hazeSource(hazeState),
)

Set implementationMode before attaching the controller. See the complete CameraX sample.

KameraΒΆ

Kamera provides shared camera state for Android and Desktop. The state and Haze layout can live in a source set shared by those targets:

val cameraState by rememberCameraKState(
  config = CameraConfiguration(
    cameraLens = CameraLens.BACK,
    aspectRatio = AspectRatio.RATIO_16_9,
  ),
)

when (val state = cameraState) {
  CameraKState.Initializing -> LoadingCamera()
  is CameraKState.Ready -> KameraPreview(
    controller = state.controller,
    modifier = Modifier
      .fillMaxSize()
      .hazeSource(hazeState),
  )
  is CameraKState.Error -> CameraError(state.message)
}

Kamera's Android preview also creates a default PreviewView, so use a small Android adapter that selects the compatible implementation and binds Kamera's controller:

@Composable
actual fun KameraPreview(
  controller: CameraController,
  modifier: Modifier,
) {
  AndroidView(
    factory = { context ->
      PreviewView(context).also { previewView ->
        previewView.implementationMode = PreviewView.ImplementationMode.COMPATIBLE
        previewView.scaleType = PreviewView.ScaleType.FILL_CENTER
        controller.bindCamera(previewView)
      }
    },
    modifier = modifier,
  )
}

On Desktop, Kamera's standard CameraPreviewView renders frames as a Compose Image, so it can be used directly. The complete sample keeps the permission and camera-state handling alongside the shared Haze layout:

Platform supportΒΆ

Integration Haze support Requirement
CameraX on Android Yes Use PreviewView.ImplementationMode.COMPATIBLE.
Kamera on Android Yes Bind the controller to a compatible PreviewView.
Kamera on Desktop Yes Use Kamera's Compose-rendered preview.
Kamera on iOS No Its preview uses UIKitViewController, which Haze cannot capture.

The same constraint applies to video players and other platform views. Choose a rendering path that draws into Compose, such as ExoPlayer's TextureView mode, before marking it with hazeSource.

For iOS, see UIKit interop on iOS.