New feature: Tap-to-focus (#100)
This commit is contained in:
@@ -21,6 +21,7 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.camera.core.CameraControl
|
import androidx.camera.core.CameraControl
|
||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
|
import androidx.camera.core.FocusMeteringAction
|
||||||
import androidx.camera.core.ImageAnalysis
|
import androidx.camera.core.ImageAnalysis
|
||||||
import androidx.camera.core.ImageCapture
|
import androidx.camera.core.ImageCapture
|
||||||
import androidx.camera.core.ImageCaptureException
|
import androidx.camera.core.ImageCaptureException
|
||||||
@@ -69,6 +70,7 @@ import org.fairscan.imageprocessing.Quad
|
|||||||
import org.fairscan.imageprocessing.scaledTo
|
import org.fairscan.imageprocessing.scaledTo
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CameraPreview(
|
fun CameraPreview(
|
||||||
@@ -283,6 +285,7 @@ class CameraCaptureController {
|
|||||||
var cameraControl: CameraControl? = null
|
var cameraControl: CameraControl? = null
|
||||||
var imageCapture: ImageCapture? = null
|
var imageCapture: ImageCapture? = null
|
||||||
private val executor = Executors.newSingleThreadExecutor()
|
private val executor = Executors.newSingleThreadExecutor()
|
||||||
|
var previewView: PreviewView? = null
|
||||||
|
|
||||||
fun shutdown() {
|
fun shutdown() {
|
||||||
executor.shutdown()
|
executor.shutdown()
|
||||||
@@ -302,6 +305,20 @@ class CameraCaptureController {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun tapToFocus(tapOffset: Offset) {
|
||||||
|
val view = previewView ?: return
|
||||||
|
val control = cameraControl ?: return
|
||||||
|
|
||||||
|
val factory = view.meteringPointFactory
|
||||||
|
val point = factory.createPoint(tapOffset.x, tapOffset.y)
|
||||||
|
|
||||||
|
val action = FocusMeteringAction.Builder(point)
|
||||||
|
.setAutoCancelDuration(5, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
control.startFocusAndMetering(action)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface CameraBindState {
|
sealed interface CameraBindState {
|
||||||
|
|||||||
@@ -24,11 +24,13 @@ import androidx.camera.view.PreviewView
|
|||||||
import androidx.compose.animation.core.animateFloat
|
import androidx.compose.animation.core.animateFloat
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.core.updateTransition
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.LocalIndication
|
import androidx.compose.foundation.LocalIndication
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -66,10 +68,13 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.boundsInWindow
|
import androidx.compose.ui.layout.boundsInWindow
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
@@ -156,7 +161,10 @@ fun CameraScreen(
|
|||||||
CameraPreview(
|
CameraPreview(
|
||||||
onImageAnalyzed = onImageAnalyzed,
|
onImageAnalyzed = onImageAnalyzed,
|
||||||
captureController = captureController,
|
captureController = captureController,
|
||||||
onPreviewViewReady = { view -> previewView = view },
|
onPreviewViewReady = { view ->
|
||||||
|
previewView = view
|
||||||
|
captureController.previewView = view
|
||||||
|
},
|
||||||
cameraPermission = cameraPermission,
|
cameraPermission = cameraPermission,
|
||||||
onError = { message, throwable -> cameraViewModel.logError(message, throwable) }
|
onError = { message, throwable -> cameraViewModel.logError(message, throwable) }
|
||||||
)
|
)
|
||||||
@@ -192,7 +200,8 @@ fun CameraScreen(
|
|||||||
isTorchEnabled = !isTorchEnabled
|
isTorchEnabled = !isTorchEnabled
|
||||||
captureController.cameraControl?.enableTorch(isTorchEnabled) },
|
captureController.cameraControl?.enableTorch(isTorchEnabled) },
|
||||||
thumbnailCoords = thumbnailCoords,
|
thumbnailCoords = thumbnailCoords,
|
||||||
navigation = navigation
|
navigation = navigation,
|
||||||
|
captureController
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +216,16 @@ private fun CameraScreenScaffold(
|
|||||||
onTorchSwitched: () -> Unit,
|
onTorchSwitched: () -> Unit,
|
||||||
thumbnailCoords: MutableState<Offset>,
|
thumbnailCoords: MutableState<Offset>,
|
||||||
navigation: Navigation,
|
navigation: Navigation,
|
||||||
|
captureController: CameraCaptureController,
|
||||||
) {
|
) {
|
||||||
|
var focusPoint by remember { mutableStateOf<Offset?>(null) }
|
||||||
|
LaunchedEffect(focusPoint) {
|
||||||
|
if (focusPoint != null) {
|
||||||
|
delay(1000)
|
||||||
|
focusPoint = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var tapCount by remember { mutableLongStateOf(0) }
|
var tapCount by remember { mutableLongStateOf(0) }
|
||||||
var lastTapTime by remember { mutableLongStateOf(0L) }
|
var lastTapTime by remember { mutableLongStateOf(0L) }
|
||||||
val tapThreshold = 500L
|
val tapThreshold = 500L
|
||||||
@@ -235,9 +253,16 @@ private fun CameraScreenScaffold(
|
|||||||
CameraPreviewBox(
|
CameraPreviewBox(
|
||||||
cameraPreview,
|
cameraPreview,
|
||||||
cameraUiState,
|
cameraUiState,
|
||||||
|
focusPoint,
|
||||||
onCapture,
|
onCapture,
|
||||||
onTorchSwitched,
|
onTorchSwitched,
|
||||||
modifier.clickable(onClick = onPageCountClick)
|
modifier.pointerInput(Unit) {
|
||||||
|
detectTapGestures { offset ->
|
||||||
|
focusPoint = offset
|
||||||
|
captureController.tapToFocus(offset)
|
||||||
|
onPageCountClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (cameraUiState.captureState is CaptureState.CapturePreview) {
|
if (cameraUiState.captureState is CaptureState.CapturePreview) {
|
||||||
@@ -251,6 +276,7 @@ private fun CameraScreenScaffold(
|
|||||||
private fun CameraPreviewBox(
|
private fun CameraPreviewBox(
|
||||||
cameraPreview: @Composable (() -> Unit),
|
cameraPreview: @Composable (() -> Unit),
|
||||||
cameraUiState: CameraUiState,
|
cameraUiState: CameraUiState,
|
||||||
|
focusPoint: Offset?,
|
||||||
onCapture: () -> Unit,
|
onCapture: () -> Unit,
|
||||||
onTorchSwitched: () -> Unit,
|
onTorchSwitched: () -> Unit,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
@@ -266,6 +292,7 @@ private fun CameraPreviewBox(
|
|||||||
if (cameraUiState.isDebugMode) {
|
if (cameraUiState.isDebugMode) {
|
||||||
MessageBox(cameraUiState.liveAnalysisState.inferenceTime)
|
MessageBox(cameraUiState.liveAnalysisState.inferenceTime)
|
||||||
}
|
}
|
||||||
|
FocusOverlay(focusPoint)
|
||||||
CaptureButton(
|
CaptureButton(
|
||||||
onClick = onCapture,
|
onClick = onCapture,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -427,6 +454,23 @@ private fun CameraPreviewWithOverlay(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FocusOverlay(focusPoint: Offset?) {
|
||||||
|
if (focusPoint == null) return
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
|
val size = 80f
|
||||||
|
drawRect(
|
||||||
|
color = Color.White,
|
||||||
|
topLeft = Offset(
|
||||||
|
focusPoint.x - size / 2,
|
||||||
|
focusPoint.y - size / 2
|
||||||
|
),
|
||||||
|
size = Size(size, size),
|
||||||
|
style = Stroke(width = 3f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageBox(inferenceTime: Long) {
|
fun MessageBox(inferenceTime: Long) {
|
||||||
Text(
|
Text(
|
||||||
@@ -519,7 +563,8 @@ private fun ScreenPreview(captureState: CaptureState, rotationDegrees: Float = 0
|
|||||||
onDebugModeSwitched = {},
|
onDebugModeSwitched = {},
|
||||||
onTorchSwitched = {},
|
onTorchSwitched = {},
|
||||||
thumbnailCoords = thumbnailCoords,
|
thumbnailCoords = thumbnailCoords,
|
||||||
navigation = dummyNavigation()
|
navigation = dummyNavigation(),
|
||||||
|
captureController = CameraCaptureController()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user