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