Preserve user zoom level when starting video recording.

Remove the unconditional zoom reset to 1x at the start of video
recording so that any pinch-to-zoom the user applied before recording
is maintained.
This commit is contained in:
Greyson Parrelli
2026-02-18 18:39:18 +00:00
committed by Cody Henthorne
parent 177ef8a555
commit 4ed0056d2a
6 changed files with 53 additions and 23 deletions

View File

@@ -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();

View File

@@ -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

View File

@@ -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

View File

@@ -79,6 +79,7 @@ class CameraScreenViewModel : ViewModel() {
private var brightnessBeforeFlash: Float = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
private var brightnessWindow: WeakReference<Window>? = null
private var orientationListener: OrientationEventListener? = null
private var recordingStartZoomRatio: Float = 1f
private val _qrCodeDetected = MutableSharedFlow<String>(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)

View File

@@ -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)
}
}

View File

@@ -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.