diff --git a/camposer/src/androidTest/java/com/ujizin/camposer/ExposureCompensationTest.kt b/camposer/src/androidTest/java/com/ujizin/camposer/ExposureCompensationTest.kt new file mode 100644 index 0000000..1d185d6 --- /dev/null +++ b/camposer/src/androidTest/java/com/ujizin/camposer/ExposureCompensationTest.kt @@ -0,0 +1,70 @@ +package com.ujizin.camposer + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import junit.framework.TestCase.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +internal class ExposureCompensationTest: CameraTest() { + + private lateinit var exposureCompensation: MutableState + + private val currentExposure: Int? + get() = cameraState.controller.cameraInfo?.exposureState?.exposureCompensationIndex + + @Test + fun test_minExposureCompensation() = with(composeTestRule) { + initCameraWithExposure(0) + + exposureCompensation.value = cameraState.minExposure + + runOnIdle { + assertEquals(cameraState.minExposure, currentExposure) + assertEquals(exposureCompensation.value, currentExposure) + } + } + + @Test + fun test_maxExposureCompensation() = with(composeTestRule) { + initCameraWithExposure(0) + + exposureCompensation.value = cameraState.maxExposure + + runOnIdle { + assertEquals(cameraState.maxExposure, currentExposure) + assertEquals(exposureCompensation.value, currentExposure) + } + } + + + @Test + fun test_invalidExposureCompensation() = with(composeTestRule) { + initCameraWithExposure(0) + + exposureCompensation.value = Int.MAX_VALUE + + runOnIdle { + assertNotEquals(cameraState.maxExposure, currentExposure) + assertNotEquals(exposureCompensation.value, currentExposure) + assertEquals(cameraState.initialExposure, currentExposure) + } + } + + private fun ComposeContentTestRule.initCameraWithExposure( + exposure: Int, + ) = initCameraState { state -> + exposureCompensation = remember { mutableStateOf(exposure) } + CameraPreview( + cameraState = state, + exposureCompensation = exposureCompensation.value + ) + } +} \ No newline at end of file diff --git a/camposer/src/main/java/com/ujizin/camposer/CameraPreview.kt b/camposer/src/main/java/com/ujizin/camposer/CameraPreview.kt index 81715eb..8681379 100644 --- a/camposer/src/main/java/com/ujizin/camposer/CameraPreview.kt +++ b/camposer/src/main/java/com/ujizin/camposer/CameraPreview.kt @@ -44,6 +44,7 @@ import androidx.camera.core.CameraSelector as CameraXSelector * @param flashMode flash mode to be added, default is off * @param scaleType scale type to be added, default is fill center * @param enableTorch enable torch from camera, default is false. + * @param exposureCompensation camera exposure compensation to be added * @param zoomRatio zoom ratio to be added, default is 1.0 * @param imageAnalyzer image analyzer from camera, see [ImageAnalyzer] * @param implementationMode implementation mode to be added, default is performance @@ -71,6 +72,7 @@ public fun CameraPreview( flashMode: FlashMode = cameraState.flashMode, scaleType: ScaleType = cameraState.scaleType, enableTorch: Boolean = cameraState.enableTorch, + exposureCompensation: Int = cameraState.initialExposure, zoomRatio: Float = 1F, imageAnalyzer: ImageAnalyzer? = null, implementationMode: ImplementationMode = cameraState.implementationMode, @@ -93,6 +95,7 @@ public fun CameraPreview( cameraState = cameraState, camSelector = camSelector, captureMode = captureMode, + exposureCompensation = exposureCompensation, imageCaptureMode = imageCaptureMode, imageCaptureTargetSize = imageCaptureTargetSize, flashMode = flashMode, @@ -129,6 +132,7 @@ internal fun CameraPreviewImpl( zoomRatio: Float, implementationMode: ImplementationMode, imageAnalyzer: ImageAnalyzer?, + exposureCompensation: Int, isImageAnalysisEnabled: Boolean, isFocusOnTapEnabled: Boolean, isPinchToZoomEnabled: Boolean, @@ -198,7 +202,8 @@ internal fun CameraPreviewImpl( enableTorch = enableTorch, zoomRatio = zoomRatio, imageCaptureMode = imageCaptureMode, - meteringPoint = meteringPointFactory.createPoint(x, y) + meteringPoint = meteringPointFactory.createPoint(x, y), + exposureCompensation = exposureCompensation, ) } diff --git a/camposer/src/main/java/com/ujizin/camposer/state/CameraState.kt b/camposer/src/main/java/com/ujizin/camposer/state/CameraState.kt index 2cc67c8..a105814 100644 --- a/camposer/src/main/java/com/ujizin/camposer/state/CameraState.kt +++ b/camposer/src/main/java/com/ujizin/camposer/state/CameraState.kt @@ -75,6 +75,37 @@ public class CameraState internal constructor(context: Context) { ) internal set + + /** + * Get range compensation range from camera. + * */ + private val exposureCompensationRange + get() = controller.cameraInfo?.exposureState?.exposureCompensationRange + + /** + * Get min exposure from camera. + * */ + public var minExposure: Int by mutableStateOf( + exposureCompensationRange?.lower ?: INITIAL_EXPOSURE_VALUE + ) + internal set + + /** + * Get max exposure from camera. + * */ + public var maxExposure: Int by mutableStateOf( + exposureCompensationRange?.upper ?: INITIAL_EXPOSURE_VALUE + ) + internal set + + public val initialExposure: Int = INITIAL_EXPOSURE_VALUE + get() = controller.cameraInfo?.exposureState?.exposureCompensationIndex ?: field + + /** + * Check if compensation exposure is supported. + * */ + public val isExposureSupported: Boolean by derivedStateOf { maxExposure != INITIAL_EXPOSURE_VALUE } + /** * Check if camera is streaming or not. * */ @@ -282,11 +313,16 @@ public class CameraState internal constructor(context: Context) { init { controller.initializationFuture.addListener({ - startZoom() + resetCamera() isInitialized = true }, mainExecutor) } + private fun startExposure() { + minExposure = exposureCompensationRange?.lower ?: INITIAL_EXPOSURE_VALUE + maxExposure = exposureCompensationRange?.upper ?: INITIAL_EXPOSURE_VALUE + } + /** * Take a picture with the camera. * @@ -352,6 +388,10 @@ public class CameraState internal constructor(context: Context) { controller.setZoomRatio(zoomRatio.coerceIn(minZoom, maxZoom)) } + private fun setExposureCompensation(exposureCompensation: Int) { + controller.cameraControl?.setExposureCompensationIndex(exposureCompensation) + } + /** * Start recording camera. * @@ -489,6 +529,7 @@ public class CameraState internal constructor(context: Context) { private fun resetCamera() { hasFlashUnit = controller.cameraInfo?.hasFlashUnit() ?: false startZoom() + startExposure() } private fun Set.sumOr(initial: Int = 0): Int = fold(initial) { acc, current -> @@ -511,7 +552,8 @@ public class CameraState internal constructor(context: Context) { zoomRatio: Float, imageCaptureMode: ImageCaptureMode, enableTorch: Boolean, - meteringPoint: MeteringPoint + meteringPoint: MeteringPoint, + exposureCompensation: Int ) { this.camSelector = camSelector this.captureMode = captureMode @@ -525,12 +567,14 @@ public class CameraState internal constructor(context: Context) { this.enableTorch = enableTorch this.isFocusOnTapSupported = meteringPoint.isFocusMeteringSupported this.imageCaptureMode = imageCaptureMode + setExposureCompensation(exposureCompensation) setZoomRatio(zoomRatio) } private companion object { private val TAG = this::class.java.name private const val INITIAL_ZOOM_VALUE = 1F + private const val INITIAL_EXPOSURE_VALUE = 0 } }