diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraButtonView.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraButtonView.java index 80d3ecfbc9..2cce344c7d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraButtonView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraButtonView.java @@ -257,14 +257,20 @@ public class CameraButtonView extends View { startAnimation(shrinkAnimation); } case MotionEvent.ACTION_MOVE: - if (isRecordingVideo && eventIsNotInsideDeadzone(event)) { + if (isRecordingVideo) { + float maxRange = getHeight() * DRAG_DISTANCE_MULTIPLIER; - float maxRange = getHeight() * DRAG_DISTANCE_MULTIPLIER; - float deltaY = Math.abs(event.getY() - deadzoneRect.top); - float increment = Math.min(1f, deltaY / maxRange); - - notifyZoomPercent(ZOOM_INTERPOLATOR.getInterpolation(increment)); - invalidate(); + if (eventIsAboveDeadzone(event)) { + float deltaY = Math.abs(event.getY() - deadzoneRect.top); + float increment = Math.min(1f, deltaY / maxRange); + notifyZoomPercent(ZOOM_INTERPOLATOR.getInterpolation(increment)); + invalidate(); + } else if (eventIsBelowDeadzone(event)) { + float deltaY = Math.abs(event.getY() - deadzoneRect.bottom); + float increment = Math.min(1f, deltaY / maxRange); + notifyZoomPercent(-ZOOM_INTERPOLATOR.getInterpolation(increment)); + invalidate(); + } } break; case MotionEvent.ACTION_CANCEL: @@ -279,10 +285,14 @@ public class CameraButtonView extends View { return super.onTouchEvent(event); } - private boolean eventIsNotInsideDeadzone(MotionEvent event) { + private boolean eventIsAboveDeadzone(MotionEvent event) { return Math.round(event.getY()) < deadzoneRect.top; } + private boolean eventIsBelowDeadzone(MotionEvent event) { + return Math.round(event.getY()) > deadzoneRect.bottom; + } + private void notifyVideoCaptureStarted() { if (!isRecordingVideo && videoCaptureListener != null) { videoCaptureListener.onVideoCaptureStarted(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java index 145b01a33d..c2ee8d2263 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java @@ -238,8 +238,15 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener @Override public void onZoomIncremented(float increment) { ZoomState zoomState = Objects.requireNonNull(cameraController.getZoomState().getValue()); - float range = zoomState.getMaxZoomRatio() - getDefaultVideoZoomRatio(); - cameraController.setZoomRatio((range * increment) + getDefaultVideoZoomRatio()); + float base = getDefaultVideoZoomRatio(); + + if (increment >= 0f) { + float range = zoomState.getMaxZoomRatio() - base; + cameraController.setZoomRatio(base + range * increment); + } else { + float range = base - zoomState.getMinZoomRatio(); + cameraController.setZoomRatio(base + range * increment); + } } @Override diff --git a/feature/camera/src/main/java/org/signal/camera/CameraScreenEvents.kt b/feature/camera/src/main/java/org/signal/camera/CameraScreenEvents.kt index 9d5dd8aed6..5615f35540 100644 --- a/feature/camera/src/main/java/org/signal/camera/CameraScreenEvents.kt +++ b/feature/camera/src/main/java/org/signal/camera/CameraScreenEvents.kt @@ -29,8 +29,8 @@ sealed interface CameraScreenEvents { /** Zoom that happens when you pinch your fingers. */ data class PinchZoom(val zoomFactor: Float) : CameraScreenEvents - /** Zoom that happens when you move your finger up and down during recording. */ - data class LinearZoom(@param:FloatRange(from = 0.0, to = 1.0) val linearZoom: Float) : CameraScreenEvents + /** Zoom that happens when you move your finger up and down during recording. Positive values zoom in, negative values zoom out. */ + data class LinearZoom(@param:FloatRange(from = -1.0, to = 1.0) val linearZoom: Float) : CameraScreenEvents /** Switches between available cameras (i.e. front and rear cameras). */ data class SwitchCamera(val context: Context) : CameraScreenEvents diff --git a/feature/camera/src/main/java/org/signal/camera/CameraScreenViewModel.kt b/feature/camera/src/main/java/org/signal/camera/CameraScreenViewModel.kt index 54351a24a2..c19c72d80b 100644 --- a/feature/camera/src/main/java/org/signal/camera/CameraScreenViewModel.kt +++ b/feature/camera/src/main/java/org/signal/camera/CameraScreenViewModel.kt @@ -79,6 +79,7 @@ class CameraScreenViewModel : ViewModel() { private var brightnessBeforeFlash: Float = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE private var brightnessWindow: WeakReference? = null private var orientationListener: OrientationEventListener? = null + private var recordingStartZoomRatio: Float = 1f private val _qrCodeDetected = MutableSharedFlow(extraBufferCapacity = 1) @@ -229,6 +230,8 @@ class CameraScreenViewModel : ViewModel() { ) { val capture = videoCapture ?: return + recordingStartZoomRatio = _state.value.zoomRatio + val enableTorch = _state.value.flashMode == FlashMode.On && _state.value.lensFacing == CameraSelector.LENS_FACING_BACK @@ -236,9 +239,6 @@ class CameraScreenViewModel : ViewModel() { camera?.cameraControl?.enableTorch(true) } - camera?.cameraControl?.setZoomRatio(1f) - _state.value = _state.value.copy(zoomRatio = 1f) - // Prepare recording based on configuration val pendingRecording = when (output) { is VideoOutput.FileOutput -> { @@ -494,14 +494,21 @@ class CameraScreenViewModel : ViewModel() { ) { val currentCamera = camera ?: return - // Clamp linear zoom to valid range - val clampedLinearZoom = linearZoom.coerceIn(0f, 1f) + // Clamp linear zoom to valid range (-1 to 1) + val clampedLinearZoom = linearZoom.coerceIn(-1f, 1f) - // Map 0.0-1.0 to the range from 1x to maxZoomRatio. - // We use 1x as the base instead of minZoomRatio because minZoomRatio may be less than 1x on devices with an ultrawide lens (e.g. 0.5x). - val baseZoom = 1f + // Use the zoom ratio from when recording started as the base, so that the + // drag gesture is relative to the user's current zoom level rather than jumping. + // Positive values (0 to 1) zoom in from base toward maxZoomRatio. + // Negative values (-1 to 0) zoom out from base toward minZoomRatio. + val baseZoom = recordingStartZoomRatio + val minZoom = currentCamera.cameraInfo.zoomState.value?.minZoomRatio ?: 1f val maxZoom = currentCamera.cameraInfo.zoomState.value?.maxZoomRatio ?: 1f - val newZoomRatio = baseZoom + (maxZoom - baseZoom) * clampedLinearZoom + val newZoomRatio = if (clampedLinearZoom >= 0f) { + baseZoom + (maxZoom - baseZoom) * clampedLinearZoom + } else { + baseZoom + (baseZoom - minZoom) * clampedLinearZoom + } currentCamera.cameraControl.setZoomRatio(newZoomRatio) diff --git a/feature/camera/src/main/java/org/signal/camera/hud/CaptureButton.kt b/feature/camera/src/main/java/org/signal/camera/hud/CaptureButton.kt index 2390f728ad..254067c055 100644 --- a/feature/camera/src/main/java/org/signal/camera/hud/CaptureButton.kt +++ b/feature/camera/src/main/java/org/signal/camera/hud/CaptureButton.kt @@ -202,13 +202,19 @@ fun CaptureButton( // Handle zoom during recording if (longPressTriggered) { + val deadzoneBottom = size.height * (1f - DEADZONE_REDUCTION_PERCENT / 2f) val isAboveDeadzone = currentPointer.position.y < deadzoneTop + val isBelowDeadzone = currentPointer.position.y > deadzoneBottom if (isAboveDeadzone) { val deltaY = (deadzoneTop - currentPointer.position.y).coerceAtLeast(0f) val zoomPercent = (deltaY / maxRange).coerceIn(0f, 1f) - // Apply decelerate interpolation like CameraButtonView val interpolatedZoom = decelerateInterpolation(zoomPercent) onZoomChange(interpolatedZoom) + } else if (isBelowDeadzone) { + val deltaY = (currentPointer.position.y - deadzoneBottom).coerceAtLeast(0f) + val zoomPercent = (deltaY / maxRange).coerceIn(0f, 1f) + val interpolatedZoom = decelerateInterpolation(zoomPercent) + onZoomChange(-interpolatedZoom) } } diff --git a/feature/camera/src/main/java/org/signal/camera/hud/StandardCameraHudEvents.kt b/feature/camera/src/main/java/org/signal/camera/hud/StandardCameraHudEvents.kt index 35a81887b3..5a4c83e727 100644 --- a/feature/camera/src/main/java/org/signal/camera/hud/StandardCameraHudEvents.kt +++ b/feature/camera/src/main/java/org/signal/camera/hud/StandardCameraHudEvents.kt @@ -16,7 +16,7 @@ sealed interface StandardCameraHudEvents { data object SwitchCamera : StandardCameraHudEvents - data class SetZoomLevel(@param:FloatRange(from = 0.0, to = 1.0) val zoomLevel: Float) : StandardCameraHudEvents + data class SetZoomLevel(@param:FloatRange(from = -1.0, to = 1.0) val zoomLevel: Float) : StandardCameraHudEvents /** * Emitted when the gallery button is clicked.