Skip to content

Camera¶

Haze can apply Blur and Glass to a live camera preview when the preview pixels are drawn into the same Compose graphics layer that contains hazeSource.

This example intentionally uses HazeInput.Sources: camera and video integrations need explicit captured-source semantics, and a platform surface can sit outside the current window's earlier Compose pixels. HazeInput.Backdrop cannot cross that surface or window boundary.

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(),
      ),
  )
}

The important part is how CameraPreview renders. A platform interop composable such as AndroidView does not, by itself, guarantee that its pixels can be captured.

CameraX on Android¶

CameraX's PreviewView uses ImplementationMode.PERFORMANCE by default. When possible, that mode renders through a SurfaceView. A SurfaceView owns a separate Android surface, so its pixels are not present when Haze captures the Compose source layer.

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 is outside Haze's captured Compose layer.

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