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