@ComposablefunCameraPreview(modifier:Modifier=Modifier){valcontext=LocalContext.currentvallifecycleOwner=LocalLifecycleOwner.current// Writer: MutableStateFlow we can update from CameraX callbacksvalsurfaceRequests=remember{MutableStateFlow<SurfaceRequest?>(null)}// Reader: Compose state derived from the flowvalsurfaceRequestbysurfaceRequests.collectAsState(initial=null)// Bind CameraX use cases onceLaunchedEffect(Unit){valprovider=ProcessCameraProvider.awaitInstance(context)valpreview=Preview.Builder().build().apply{// When CameraX needs a surface, publish it to ComposesetSurfaceProvider{request->surfaceRequests.value=request}}provider.unbindAll()provider.bindToLifecycle(lifecycleOwner,CameraSelector.DEFAULT_BACK_CAMERA,preview)}// The actual Compose viewfindersurfaceRequest?.let{request->CameraXViewfinder(surfaceRequest=request,modifier=modifier.fillMaxSize())}}
@ComposablefunPreviewWithLensSwitch(modifier:Modifier=Modifier){valcontext=LocalContext.currentvallifecycleOwner=LocalLifecycleOwner.currentvalsurfaceRequests=remember{MutableStateFlow<SurfaceRequest?>(null)}valsurfaceRequestbysurfaceRequests.collectAsState(initial=null)// remember current lensvaruseFrontbyrememberSaveable{mutableStateOf(false)}valselector=if(useFront)CameraSelector.DEFAULT_FRONT_CAMERAelseCameraSelector.DEFAULT_BACK_CAMERA// bind when camera selector changes (front/back camera)LaunchedEffect(selector){valprovider=ProcessCameraProvider.awaitInstance(context)valpreview=Preview.Builder().build().apply{setSurfaceProvider{req->surfaceRequests.value=req}}provider.unbindAll()provider.bindToLifecycle(lifecycleOwner,selector,preview)}Box(Modifier.fillMaxSize()){surfaceRequest?.let{req->CameraXViewfinder(surfaceRequest=req,modifier=Modifier.fillMaxSize())}FloatingActionButton(onClick={useFront=!useFront},modifier=Modifier.align(Alignment.BottomEnd).padding(16.dp)){Icon(Icons.Rounded.Cameraswitch,contentDescription="Switch camera")}}}
@ComposablefunInteractiveCameraPreview(modifier:Modifier=Modifier,onFocusTap:(success:Boolean)->Unit={}){valcontext=LocalContext.currentvallifecycleOwner=LocalLifecycleOwner.currentvarcamerabyremember{mutableStateOf<Camera?>(null)}valsurfaceRequests=remember{MutableStateFlow<SurfaceRequest?>(null)}valsurfaceRequestbysurfaceRequests.collectAsState(initial=null)// Bind camera onceLaunchedEffect(Unit){valprovider=ProcessCameraProvider.awaitInstance(context)valpreview=Preview.Builder().build().apply{setSurfaceProvider{req->surfaceRequests.value=req}}camera=provider.bindToLifecycle(lifecycleOwner,CameraSelector.DEFAULT_BACK_CAMERA,preview)}// Coordinate transformer: Compose UI → Camera surfacevalcoordinateTransformer=remember{MutableCoordinateTransformer()}surfaceRequest?.let{request->CameraXViewfinder(surfaceRequest=request,coordinateTransformer=coordinateTransformer,modifier=modifier.fillMaxSize().pointerInput(camera){// Tap-to-focusdetectTapGestures{offset->valcam=camera?:return@detectTapGestures// Transform Compose coordinates to camera surfacevalsurfacePoint=with(coordinateTransformer){offset.transform()}valmeteringFactory=SurfaceOrientedMeteringPointFactory(request.resolution.width.toFloat(),request.resolution.height.toFloat())valfocusPoint=meteringFactory.createPoint(surfacePoint.x,surfacePoint.y)valaction=FocusMeteringAction.Builder(focusPoint,FocusMeteringAction.FLAG_AForFocusMeteringAction.FLAG_AE).setAutoCancelDuration(3,TimeUnit.SECONDS).build()cam.cameraControl.startFocusAndMetering(action).addListener({onFocusTap(true)},ContextCompat.getMainExecutor(context))}}.pointerInput(camera){// Pinch-to-zoomdetectTransformGestures{_,_,zoom,_->valcam=camera?:return@detectTransformGesturesvalzoomState=cam.cameraInfo.zoomState.value?:return@detectTransformGesturesvalnewRatio=(zoomState.zoomRatio*zoom).coerceIn(zoomState.minZoomRatio,zoomState.maxZoomRatio)cam.cameraControl.setZoomRatio(newRatio)}})}}
@ComposablefunCameraScreen(){valcontext=LocalContext.currentvallifecycleOwner=LocalLifecycleOwner.currentvarcamerabyremember{mutableStateOf<Camera?>(null)}varimageCapturebyremember{mutableStateOf<ImageCapture?>(null)}varvideoCapturebyremember{mutableStateOf<VideoCapture<Recorder>?>(null)}varactiveRecordingbyremember{mutableStateOf<Recording?>(null)}valsurfaceRequests=remember{MutableStateFlow<SurfaceRequest?>(null)}valsurfaceRequestbysurfaceRequests.collectAsState(initial=null)// Bind all use casesLaunchedEffect(Unit){valprovider=ProcessCameraProvider.awaitInstance(context)valpreview=Preview.Builder().build().apply{setSurfaceProvider{req->surfaceRequests.value=req}}imageCapture=ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY).build()valrecorder=Recorder.Builder().setQualitySelector(QualitySelector.from(Quality.FHD)).build()videoCapture=VideoCapture.withOutput(recorder)camera=provider.bindToLifecycle(lifecycleOwner,CameraSelector.DEFAULT_BACK_CAMERA,preview,imageCapture!!,videoCapture!!)}Box(modifier=Modifier.fillMaxSize()){// Camera previewsurfaceRequest?.let{request->CameraXViewfinder(surfaceRequest=request,modifier=Modifier.fillMaxSize())}// Compose UI controlsRow(modifier=Modifier.align(Alignment.BottomCenter).padding(bottom=32.dp)){// Capture photo buttonIconButton(onClick={capturePhoto(context,imageCapture)}){Icon(Icons.Default.PhotoCamera,"Take Photo")}Spacer(modifier=Modifier.width(32.dp))// Video record toggle (mic requested only when needed)PermissionGate(permission=Permission.RECORD_AUDIO,// Optional: custom UI if permission is not yet grantedcontentNonGranted={missing,humanReadable,requestPermissions->// Minimal, inline UX: re-request directlyButton(onClick={requestPermissions(missing)}){Text("Grant $humanReadable")}}){IconButton(onClick={activeRecording=toggleRecording(context,videoCapture,activeRecording)}){Icon(if(activeRecording==null)Icons.Default.RadioButtonUncheckedelseIcons.Default.Stop,"Record Video")}}}}}privatefuncapturePhoto(context:Context,imageCapture:ImageCapture?){valcapture=imageCapture?:returnvalname="IMG_${System.currentTimeMillis()}.jpg"valcontentValues=ContentValues().apply{put(MediaStore.Images.Media.DISPLAY_NAME,name)put(MediaStore.Images.Media.MIME_TYPE,"image/jpeg")// On Android 10+ you could also set RELATIVE_PATH = "DCIM/CameraX"}valoutputOptions=ImageCapture.OutputFileOptions.Builder(context.contentResolver,MediaStore.Images.Media.EXTERNAL_CONTENT_URI,contentValues).build()capture.takePicture(outputOptions,ContextCompat.getMainExecutor(context),object: ImageCapture.OnImageSavedCallback{overridefunonImageSaved(output:ImageCapture.OutputFileResults){// Success: output.savedUri}overridefunonError(exception:ImageCaptureException){// Handle error}})}privatefuntoggleRecording(context:Context,videoCapture:VideoCapture<Recorder>?,currentRecording:Recording?):Recording?{valcapture=videoCapture?:returnnull// Stop if already recordingif(currentRecording!=null){currentRecording.stop()returnnull}// Start new recordingvalname="VID_${System.currentTimeMillis()}.mp4"valcontentValues=ContentValues().apply{put(MediaStore.Video.Media.DISPLAY_NAME,name)// On Android 10+ you could also set RELATIVE_PATH = "DCIM/CameraX"}valoutputOptions=MediaStoreOutputOptions.Builder(context.contentResolver,MediaStore.Video.Media.EXTERNAL_CONTENT_URI).setContentValues(contentValues).build()returncapture.output.prepareRecording(context,outputOptions).withAudioEnabled()// mic permission is ensured by PermissionGate above.start(ContextCompat.getMainExecutor(context)){event->// Handle recording events (e.g., finalize, error)}}
valcameraxVersion="1.5.1"dependencies{implementation("androidx.camera:camera-core:$cameraxVersion")implementation("androidx.camera:camera-camera2:$cameraxVersion")implementation("androidx.camera:camera-lifecycle:$cameraxVersion")implementation("androidx.camera:camera-video:$cameraxVersion")// The new Compose-native viewfinderimplementation("androidx.camera:camera-compose:$cameraxVersion")}
importandroidx.camera.viewfinder.core.ImplementationModeCameraXViewfinder(surfaceRequest=request,implementationMode=ImplementationMode.EXTERNAL// or ImplementationMode.EMBEDDED)