Skip to content

Commit

Permalink
feat: add exposure compensation for camera preview
Browse files Browse the repository at this point in the history
  • Loading branch information
ujizin committed Aug 15, 2023
1 parent 884a784 commit 8c07fc6
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -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<Int>

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
)
}
}
7 changes: 6 additions & 1 deletion camposer/src/main/java/com/ujizin/camposer/CameraPreview.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -93,6 +95,7 @@ public fun CameraPreview(
cameraState = cameraState,
camSelector = camSelector,
captureMode = captureMode,
exposureCompensation = exposureCompensation,
imageCaptureMode = imageCaptureMode,
imageCaptureTargetSize = imageCaptureTargetSize,
flashMode = flashMode,
Expand Down Expand Up @@ -129,6 +132,7 @@ internal fun CameraPreviewImpl(
zoomRatio: Float,
implementationMode: ImplementationMode,
imageAnalyzer: ImageAnalyzer?,
exposureCompensation: Int,
isImageAnalysisEnabled: Boolean,
isFocusOnTapEnabled: Boolean,
isPinchToZoomEnabled: Boolean,
Expand Down Expand Up @@ -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,
)
}

Expand Down
48 changes: 46 additions & 2 deletions camposer/src/main/java/com/ujizin/camposer/state/CameraState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* */
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -489,6 +529,7 @@ public class CameraState internal constructor(context: Context) {
private fun resetCamera() {
hasFlashUnit = controller.cameraInfo?.hasFlashUnit() ?: false
startZoom()
startExposure()
}

private fun Set<Int>.sumOr(initial: Int = 0): Int = fold(initial) { acc, current ->
Expand All @@ -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
Expand All @@ -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
}
}

Expand Down

0 comments on commit 8c07fc6

Please sign in to comment.