Skip to content

Commit 5d546dc

Browse files
authored
Merge pull request #542 from code-payments/feat/invert-drag-zoom-beta
feat(camera): ease-in drag-to-zoom; move to GestureController
2 parents 1667fa6 + 9086d70 commit 5d546dc

2 files changed

Lines changed: 206 additions & 164 deletions

File tree

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package com.getcode.view.main.camera
2+
3+
import android.content.Context
4+
import android.os.Handler
5+
import android.os.Looper
6+
import android.view.GestureDetector
7+
import android.view.MotionEvent
8+
import android.view.ScaleGestureDetector
9+
import androidx.camera.core.CameraControl
10+
import androidx.camera.core.CameraInfo
11+
import androidx.camera.core.FocusMeteringAction
12+
import androidx.camera.core.MeteringPoint
13+
import androidx.compose.ui.geometry.Offset
14+
import java.util.concurrent.TimeUnit
15+
import kotlin.math.pow
16+
17+
internal class CameraGestureController(
18+
context: Context,
19+
invertedDragEnabled: Boolean,
20+
private val gesturesEnabled: Boolean,
21+
private val cameraControl: CameraControl,
22+
private val cameraInfo: CameraInfo,
23+
onTap: (Offset) -> MeteringPoint,
24+
) {
25+
private val handler = Handler(Looper.getMainLooper())
26+
private var shouldIgnoreScroll = false
27+
private var resetIgnore: Runnable? = null
28+
private var initialZoomLevel = 0f
29+
private var accumulatedDelta = 0f
30+
31+
// Pinch-to-zoom gesture detector
32+
private val scaleGestureDetector = ScaleGestureDetector(
33+
context,
34+
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
35+
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
36+
shouldIgnoreScroll = true
37+
resetIgnore?.let { handler.removeCallbacks(it) }
38+
return true
39+
}
40+
41+
override fun onScale(detector: ScaleGestureDetector): Boolean {
42+
val currentZoomRatio = cameraInfo.zoomState.value?.zoomRatio ?: 1f
43+
val delta = detector.scaleFactor
44+
val newZoomRatio = currentZoomRatio * delta
45+
46+
// Clamp the new zoom ratio between the minimum and maximum zoom ratio
47+
val clampedZoomRatio = newZoomRatio.coerceIn(
48+
cameraInfo.zoomState.value?.minZoomRatio ?: 1f,
49+
cameraInfo.zoomState.value?.maxZoomRatio ?: currentZoomRatio
50+
)
51+
52+
// Apply the zoom to the camera control
53+
cameraControl.setZoomRatio(clampedZoomRatio)
54+
return true
55+
}
56+
57+
override fun onScaleEnd(detector: ScaleGestureDetector) {
58+
initialZoomLevel = cameraInfo.zoomState.value?.zoomRatio ?: 1f
59+
resetIgnore = Runnable { shouldIgnoreScroll = false }
60+
resetIgnore?.let { handler.postDelayed(it, 500) }
61+
}
62+
})
63+
64+
// Gesture detector for tap and drag-to-zoom
65+
private val gestureDetector = GestureDetector(
66+
context,
67+
object : GestureDetector.OnGestureListener {
68+
override fun onDown(e: MotionEvent): Boolean {
69+
initialZoomLevel = cameraInfo.zoomState.value?.zoomRatio ?: 1f
70+
accumulatedDelta = 0f
71+
return true
72+
}
73+
74+
override fun onSingleTapUp(event: MotionEvent): Boolean {
75+
val point = onTap(Offset(event.x, event.y))
76+
val action = FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF)
77+
.setAutoCancelDuration(5, TimeUnit.SECONDS)
78+
.build()
79+
80+
cameraControl.startFocusAndMetering(action)
81+
return true
82+
}
83+
84+
override fun onScroll(
85+
e1: MotionEvent?,
86+
e2: MotionEvent,
87+
distanceX: Float,
88+
distanceY: Float
89+
): Boolean {
90+
if (!shouldIgnoreScroll) {
91+
accumulatedDelta = if (invertedDragEnabled) {
92+
accumulatedDelta + distanceY * 0.5f
93+
} else {
94+
accumulatedDelta - distanceY * 0.5f
95+
}
96+
97+
val zoomDelta = ease(
98+
value = accumulatedDelta,
99+
fromRange = 0f..250f,
100+
toRange = 0f..10f,
101+
easeIn = true,
102+
easeOut = false
103+
)
104+
105+
val maxZoom = cameraInfo.zoomState.value?.maxZoomRatio ?: 1f
106+
val minZoom = cameraInfo.zoomState.value?.minZoomRatio ?: 1f
107+
108+
val newZoom = (initialZoomLevel + zoomDelta).coerceIn(minZoom, maxZoom)
109+
cameraControl.setZoomRatio(newZoom)
110+
}
111+
return true
112+
}
113+
114+
override fun onShowPress(e: MotionEvent) {}
115+
override fun onLongPress(e: MotionEvent) {}
116+
override fun onFling(
117+
e1: MotionEvent?,
118+
e2: MotionEvent,
119+
velocityX: Float,
120+
velocityY: Float
121+
): Boolean {
122+
return false
123+
}
124+
}
125+
)
126+
127+
fun onTouchEvent(event: MotionEvent) {
128+
if (gesturesEnabled) {
129+
scaleGestureDetector.onTouchEvent(event)
130+
gestureDetector.onTouchEvent(event)
131+
132+
if (event.action == MotionEvent.ACTION_UP) {
133+
animateZoomReset(cameraInfo, cameraControl)
134+
initialZoomLevel = cameraInfo.zoomState.value?.zoomRatio ?: 1f
135+
}
136+
}
137+
}
138+
139+
private fun animateZoomReset(cameraInfo: CameraInfo?, cameraControl: CameraControl?) {
140+
val durationMs = 300L
141+
val frameInterval = 16L
142+
val maxSteps = durationMs / frameInterval
143+
val currentZoomLevel = cameraInfo?.zoomState?.value?.linearZoom ?: 0f
144+
145+
val decrement = currentZoomLevel / maxSteps
146+
147+
var currentStep = 0L
148+
handler.post(object : Runnable {
149+
override fun run() {
150+
if (currentStep < maxSteps) {
151+
val newZoomLevel = currentZoomLevel - (decrement * currentStep)
152+
cameraControl?.setLinearZoom(newZoomLevel.coerceIn(0f, 1f))
153+
currentStep++
154+
handler.postDelayed(this, frameInterval)
155+
} else {
156+
cameraControl?.setLinearZoom(0f)
157+
}
158+
}
159+
})
160+
}
161+
162+
private fun ease(
163+
value: Float,
164+
fromRange: ClosedFloatingPointRange<Float>,
165+
toRange: ClosedFloatingPointRange<Float>,
166+
easeIn: Boolean,
167+
easeOut: Boolean
168+
): Float {
169+
val normalizedValue = (value - fromRange.start) / (fromRange.endInclusive - fromRange.start)
170+
171+
val easedValue: Float = if (easeIn && easeOut) {
172+
if (normalizedValue < 0.5f) {
173+
4 * normalizedValue * normalizedValue * normalizedValue
174+
} else {
175+
1 - (-2 * normalizedValue + 2).toDouble().pow(3.0).toFloat() / 2
176+
}
177+
} else if (easeIn) {
178+
normalizedValue * normalizedValue * normalizedValue
179+
} else if (easeOut) {
180+
1 - (1 - normalizedValue).toDouble().pow(3.0).toFloat()
181+
} else {
182+
normalizedValue
183+
}
184+
185+
return easedValue * (toRange.endInclusive - toRange.start) + toRange.start
186+
}
187+
}

0 commit comments

Comments
 (0)