Skip to content

Commit 81f1c9c

Browse files
committed
feat: add drag-to-zoom and auto zoom out
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent ed019d9 commit 81f1c9c

9 files changed

Lines changed: 115 additions & 74 deletions

File tree

api/src/main/java/com/getcode/model/Feature.kt

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,8 @@ data class BalanceCurrencyFeature(
4343
override val available: Boolean = true, // always available
4444
): Feature
4545

46-
data class CameraAFFeature(
47-
override val enabled: Boolean = BetaOptions.Defaults.cameraAFEnabled,
48-
override val available: Boolean = true, // always available
49-
): Feature
50-
51-
data class CameraZoomFeature(
52-
override val enabled: Boolean = BetaOptions.Defaults.cameraPinchZoomEnabled,
46+
data class CameraGesturesFeature(
47+
override val enabled: Boolean = BetaOptions.Defaults.cameraGesturesEnabled,
5348
override val available: Boolean = true, // always available
5449
): Feature
5550

api/src/main/java/com/getcode/model/PrefBool.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ sealed class PrefsBool(val value: String) {
6161
data object SHARE_TWEET_TO_TIP : PrefsBool("share_tweet_to_tip"), BetaFlag, Immutable
6262
data object TIP_CARD_ON_HOMESCREEN: PrefsBool("tip_card_on_home_screen"), BetaFlag, Immutable
6363
data object TIP_CARD_FLIPPABLE: PrefsBool("tipcard_flippable"), BetaFlag
64-
data object CAMERA_AF_ENABLED: PrefsBool("camera_af_enabled"), BetaFlag
65-
data object CAMERA_PINCH_ZOOM: PrefsBool("camera_pinch_zoom_enabled"), BetaFlag
64+
data object CAMERA_GESTURES_ENABLED: PrefsBool("camera_gestures_enabled"), BetaFlag
6665
}
6766

6867
val APP_SETTINGS: List<AppSetting> = listOf(PrefsBool.CAMERA_START_BY_DEFAULT, PrefsBool.REQUIRE_BIOMETRICS)

api/src/main/java/com/getcode/network/repository/BetaFlagsRepository.kt

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ data class BetaOptions(
2121
val kadoWebViewEnabled: Boolean,
2222
val shareTweetToTip: Boolean,
2323
val tipCardOnHomeScreen: Boolean,
24-
val cameraAFEnabled: Boolean,
25-
val cameraPinchZoomEnabled: Boolean,
24+
val cameraGesturesEnabled: Boolean,
2625
val canFlipTipCard: Boolean,
2726
) {
2827
companion object {
@@ -43,8 +42,7 @@ data class BetaOptions(
4342
kadoWebViewEnabled = false,
4443
shareTweetToTip = true,
4544
tipCardOnHomeScreen = true,
46-
cameraAFEnabled = true,
47-
cameraPinchZoomEnabled = true,
45+
cameraGesturesEnabled = true,
4846
canFlipTipCard = false
4947
)
5048
}
@@ -83,8 +81,7 @@ class BetaFlagsRepository @Inject constructor(
8381
observeBetaFlag(PrefsBool.KADO_WEBVIEW_ENABLED, default = defaults.kadoWebViewEnabled),
8482
observeBetaFlag(PrefsBool.SHARE_TWEET_TO_TIP, default = defaults.shareTweetToTip),
8583
observeBetaFlag(PrefsBool.TIP_CARD_ON_HOMESCREEN, defaults.tipCardOnHomeScreen),
86-
observeBetaFlag(PrefsBool.CAMERA_AF_ENABLED, defaults.cameraAFEnabled),
87-
observeBetaFlag(PrefsBool.CAMERA_PINCH_ZOOM, defaults.cameraPinchZoomEnabled),
84+
observeBetaFlag(PrefsBool.CAMERA_GESTURES_ENABLED, defaults.cameraGesturesEnabled),
8885
observeBetaFlag(PrefsBool.TIP_CARD_FLIPPABLE, defaults.canFlipTipCard)
8986
) {
9087
BetaOptions(
@@ -103,9 +100,8 @@ class BetaFlagsRepository @Inject constructor(
103100
kadoWebViewEnabled = it[12],
104101
shareTweetToTip = it[13],
105102
tipCardOnHomeScreen = it[14],
106-
cameraAFEnabled = it[15],
107-
cameraPinchZoomEnabled = it[16],
108-
canFlipTipCard = it[17]
103+
cameraGesturesEnabled = it[15],
104+
canFlipTipCard = it[16],
109105
)
110106
}
111107
}

api/src/main/java/com/getcode/network/repository/FeatureRepository.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package com.getcode.network.repository
22

33
import com.getcode.model.BalanceCurrencyFeature
44
import com.getcode.model.BuyModuleFeature
5-
import com.getcode.model.CameraAFFeature
5+
import com.getcode.model.CameraGesturesFeature
66
import com.getcode.model.PrefsBool
77
import com.getcode.model.RequestKinFeature
88
import com.getcode.model.TipCardFeature
@@ -32,8 +32,7 @@ class FeatureRepository @Inject constructor(
3232
val conversations = betaFlags.observe().map { ConversationsFeature(it.conversationsEnabled) }
3333
val conversationsCash = betaFlags.observe().map { ConversationCashFeature(it.conversationCashEnabled) }
3434

35-
val cameraAutoFocus = betaFlags.observe().map { CameraAFFeature(it.cameraAFEnabled) }
36-
val cameraPinchZoom = betaFlags.observe().map { CameraAFFeature(it.cameraPinchZoomEnabled) }
35+
val cameraGestures = betaFlags.observe().map { CameraGesturesFeature(it.cameraGesturesEnabled) }
3736

3837
val requestKin = betaFlags.observe().map { RequestKinFeature(it.giveRequestsEnabled) }
3938

app/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,10 @@ fun BetaFlagsScreen(
5555
state.shareTweetToTip,
5656
),
5757
BetaFeature(
58-
PrefsBool.CAMERA_AF_ENABLED,
59-
R.string.beta_camera_af,
60-
stringResource(id = R.string.beta_camera_af_description),
61-
state.cameraAFEnabled,
62-
),
63-
BetaFeature(
64-
PrefsBool.CAMERA_PINCH_ZOOM,
65-
R.string.beta_camera_pinch_zoom,
66-
stringResource(id = R.string.beta_camera_pinch_zoom_description),
67-
state.cameraPinchZoomEnabled,
58+
PrefsBool.CAMERA_GESTURES_ENABLED,
59+
R.string.beta_camera_gestures,
60+
stringResource(id = R.string.beta_camera_gestures_description),
61+
state.cameraGesturesEnabled,
6862
),
6963
BetaFeature(
7064
PrefsBool.TIP_CARD_FLIPPABLE,

app/src/main/java/com/getcode/view/main/camera/CodeScanner.kt

Lines changed: 94 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.getcode.view.main.camera
22

33
import android.content.Context
4+
import android.os.Handler
5+
import android.os.Looper
46
import android.view.GestureDetector
57
import android.view.MotionEvent
68
import android.view.ScaleGestureDetector
@@ -57,8 +59,7 @@ import java.util.concurrent.TimeUnit
5759
@Composable
5860
fun CodeScanner(
5961
scanningEnabled: Boolean,
60-
cameraAFEnabled: Boolean,
61-
cameraPinchZoomEnabled: Boolean,
62+
cameraGesturesEnabled: Boolean,
6263
onPreviewStateChanged: (Boolean) -> Unit,
6364
onCodeScanned: (ScannableKikCode) -> Unit
6465
) {
@@ -92,15 +93,15 @@ fun CodeScanner(
9293

9394
val cameraExecutor = remember { Executors.newSingleThreadExecutor() }
9495

96+
var camera by remember { mutableStateOf<Camera?>(null) }
97+
var autoFocusPoint by remember { mutableStateOf(Offset.Unspecified) }
98+
9599
val kikCodeAnalyzer = remember(scanner, onCodeScanned) {
96100
KikCodeAnalyzer(scanner, onCodeScanned)
97101
}
98102

99103
val biometricsState = LocalBiometricsState.current
100104

101-
var camera by remember { mutableStateOf<Camera?>(null) }
102-
var autoFocusPoint by remember { mutableStateOf(Offset.Unspecified) }
103-
104105
val scope = rememberCoroutineScope()
105106
LaunchedEffect(scanner, biometricsState.isAwaitingAuthentication, Biometrics.promptActive) {
106107
val active = Biometrics.promptActive || biometricsState.isAwaitingAuthentication
@@ -143,7 +144,7 @@ fun CodeScanner(
143144
}
144145
}
145146

146-
LaunchedEffect(camera, cameraAFEnabled, cameraPinchZoomEnabled) {
147+
LaunchedEffect(camera, cameraGesturesEnabled) {
147148
camera?.let {
148149
val cameraControl = it.cameraControl
149150
val cameraInfo = it.cameraInfo
@@ -152,8 +153,7 @@ fun CodeScanner(
152153
previewView,
153154
cameraControl,
154155
cameraInfo,
155-
cameraAFEnabled,
156-
cameraPinchZoomEnabled
156+
cameraGesturesEnabled,
157157
) { point ->
158158
autoFocusPoint = point
159159
}
@@ -219,13 +219,20 @@ private fun setupInteractionControls(
219219
previewView: PreviewView,
220220
cameraControl: CameraControl,
221221
cameraInfo: CameraInfo,
222-
autoFocusEnabled: Boolean,
223-
pinchZoomEnabled: Boolean,
222+
cameraGesturesEnabled: Boolean,
224223
onTap: (Offset) -> Unit,
225224
) {
225+
var isPinchZooming = false
226+
227+
// Pinch-to-zoom gesture detector
226228
val scaleGestureDetector = ScaleGestureDetector(
227229
previewView.context,
228230
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
231+
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
232+
isPinchZooming = true
233+
return true
234+
}
235+
229236
override fun onScale(detector: ScaleGestureDetector): Boolean {
230237
val currentZoomRatio = cameraInfo.zoomState.value?.zoomRatio ?: 1f
231238
val delta = detector.scaleFactor
@@ -241,38 +248,102 @@ private fun setupInteractionControls(
241248
cameraControl.setZoomRatio(clampedZoomRatio)
242249
return true
243250
}
251+
252+
override fun onScaleEnd(detector: ScaleGestureDetector) {
253+
isPinchZooming = false
254+
}
244255
})
245256

246-
// Create a gesture detector to detect tap gestures
247-
val gestureDetector =
248-
GestureDetector(previewView.context, object : GestureDetector.SimpleOnGestureListener() {
257+
// Gesture detector for tap and drag-to-zoom
258+
val gestureDetector = GestureDetector(
259+
previewView.context,
260+
object : GestureDetector.OnGestureListener {
261+
private var initialZoomLevel = 0f
262+
private var accumulatedDelta = 0f
263+
264+
override fun onDown(e: MotionEvent): Boolean {
265+
initialZoomLevel = cameraInfo.zoomState.value?.zoomRatio ?: 1f
266+
accumulatedDelta = 0f
267+
return true
268+
}
269+
249270
override fun onSingleTapUp(event: MotionEvent): Boolean {
250-
// Get the tap location
251271
val point = previewView.meteringPointFactory.createPoint(event.x, event.y)
252272
onTap(Offset(event.x, event.y))
253273

254-
// Prepare focus action
255274
val action = FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF)
256-
.setAutoCancelDuration(
257-
5,
258-
TimeUnit.SECONDS
259-
) // Optional: Auto-cancel after 5 seconds
275+
.setAutoCancelDuration(5, TimeUnit.SECONDS)
260276
.build()
261277

262-
// Trigger focus and metering at the tapped location
263278
cameraControl.startFocusAndMetering(action)
279+
return true
280+
}
281+
282+
override fun onScroll(
283+
e1: MotionEvent?,
284+
e2: MotionEvent,
285+
distanceX: Float,
286+
distanceY: Float
287+
): Boolean {
288+
if (!isPinchZooming) {
289+
accumulatedDelta += distanceY
290+
291+
val deltaZoom = accumulatedDelta / 1000f
292+
293+
val maxZoom = cameraInfo.zoomState.value?.maxZoomRatio ?: 1f
294+
val minZoom = cameraInfo.zoomState.value?.minZoomRatio ?: 1f
264295

296+
val newZoom = (initialZoomLevel + deltaZoom).coerceIn(minZoom, maxZoom)
297+
cameraControl.setZoomRatio(newZoom)
298+
}
265299
return true
266300
}
301+
302+
override fun onShowPress(e: MotionEvent) {}
303+
override fun onLongPress(e: MotionEvent) {}
304+
override fun onFling(
305+
e1: MotionEvent?,
306+
e2: MotionEvent,
307+
velocityX: Float,
308+
velocityY: Float
309+
): Boolean {
310+
return false
311+
}
267312
})
268313

269314
previewView.setOnTouchListener { _, event ->
270-
if (pinchZoomEnabled) {
315+
if (cameraGesturesEnabled) {
271316
scaleGestureDetector.onTouchEvent(event)
272-
}
273-
if (autoFocusEnabled) {
274317
gestureDetector.onTouchEvent(event)
318+
319+
if (event.action == MotionEvent.ACTION_UP) {
320+
animateZoomReset(cameraInfo, cameraControl)
321+
}
275322
}
276323
true
277324
}
325+
}
326+
327+
private fun animateZoomReset(cameraInfo: CameraInfo, cameraControl: CameraControl) {
328+
val handler = Handler(Looper.getMainLooper())
329+
val durationMs = 300L
330+
val frameInterval = 16L
331+
val maxSteps = durationMs / frameInterval
332+
val currentZoomLevel = cameraInfo.zoomState.value?.linearZoom ?: 0f
333+
334+
val decrement = currentZoomLevel / maxSteps
335+
336+
var currentStep = 0L
337+
handler.post(object : Runnable {
338+
override fun run() {
339+
if (currentStep < maxSteps) {
340+
val newZoomLevel = currentZoomLevel - (decrement * currentStep)
341+
cameraControl.setLinearZoom(newZoomLevel.coerceIn(0f, 1f))
342+
currentStep++
343+
handler.postDelayed(this, frameInterval)
344+
} else {
345+
cameraControl.setLinearZoom(0f)
346+
}
347+
}
348+
})
278349
}

app/src/main/java/com/getcode/view/main/home/HomeScan.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,7 @@ private fun HomeScan(
226226
scannerView = {
227227
CodeScanner(
228228
scanningEnabled = previewing,
229-
cameraAFEnabled = dataState.cameraAutoFocus.enabled,
230-
cameraPinchZoomEnabled = dataState.cameraPinchZoom.enabled,
229+
cameraGesturesEnabled = dataState.cameraGestures.enabled,
231230
onPreviewStateChanged = { previewing = it },
232231
onCodeScanned = {
233232
if (previewing) {

app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ import com.getcode.manager.ModalManager
2424
import com.getcode.manager.SessionManager
2525
import com.getcode.manager.TopBarManager
2626
import com.getcode.model.BuyModuleFeature
27-
import com.getcode.model.CameraAFFeature
28-
import com.getcode.model.CameraZoomFeature
27+
import com.getcode.model.CameraGesturesFeature
2928
import com.getcode.model.CodePayload
3029
import com.getcode.model.Currency
3130
import com.getcode.model.Domain
@@ -160,8 +159,7 @@ data class HomeUiModel(
160159
val notificationUnreadCount: Int = 0,
161160
val buyModule: Feature = BuyModuleFeature(),
162161
val requestKin: Feature = RequestKinFeature(),
163-
val cameraAutoFocus: Feature = CameraAFFeature(),
164-
val cameraPinchZoom: Feature = CameraZoomFeature(),
162+
val cameraGestures: Feature = CameraGesturesFeature(),
165163
val flippableTipCard: Feature = FlippableTipCardFeature(),
166164
val actions: List<HomeAction> = listOf(HomeAction.GIVE_KIN, HomeAction.TIP_CARD, HomeAction.BALANCE),
167165
val tipCardConnected: Boolean = false,
@@ -240,19 +238,11 @@ class HomeViewModel @Inject constructor(
240238
}
241239
}.launchIn(viewModelScope)
242240

243-
features.cameraAutoFocus
241+
features.cameraGestures
244242
.distinctUntilChanged()
245243
.onEach { module ->
246244
uiFlow.update {
247-
it.copy(cameraAutoFocus = module)
248-
}
249-
}.launchIn(viewModelScope)
250-
251-
features.cameraPinchZoom
252-
.distinctUntilChanged()
253-
.onEach { module ->
254-
uiFlow.update {
255-
it.copy(cameraPinchZoom = module)
245+
it.copy(cameraGestures = module)
256246
}
257247
}.launchIn(viewModelScope)
258248

app/src/main/res/values/strings-universal.xml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@
2626
<string translatable="false" name="beta_balance_currency">Currency Selection in Balance</string>
2727
<string translatable="false" name="beta_kado_webview">Buy Kin Internally</string>
2828
<string translatable="false" name="beta_share_tweet_tip">Share Tweets to Tip</string>
29-
<string translatable="false" name="beta_camera_af">Camera Auto Focus</string>
30-
<string translatable="false" name="beta_camera_pinch_zoom">Camera Pinch to Zoom</string>
29+
<string translatable="false" name="beta_camera_gestures">Camera Gestures</string>
3130
<string translatable="false" name="beta_tipcard_can_flip">Tap Tip Card to See Back</string>
3231
<string translatable="false" name="beta_display_errors">Show Errors</string>
3332
<string name="beta_bucket_debugger_description" translatable="false">If enabled, you\'ll gain the ability to tap the balance on the Balance screen to inspect individual bucket balances.</string>
@@ -44,8 +43,7 @@
4443
<string name="beta_conversations_cash_description" translatable="false">If enabled, you\'ll gain the ability to send cash in conversations.</string>
4544
<string name="beta_kado_webview_description" translatable="false">If enabled, the Buy Kin flow will open in an internal WebView.</string>
4645
<string name="beta_share_tweet_tip_description" translatable="false">If enabled, you\'ll gain the ability to share tweets directly from Twitter to Code to tip the author.</string>
47-
<string name="beta_camera_af_description" translatable="false">If enabled, you\'ll gain the ability to manually trigger auto focus by tapping the camera.</string>
48-
<string name="beta_camera_pinch_zoom_description" translatable="false">If enabled, you\'ll gain the ability to pinch to zoom in on the camera.</string>
46+
<string name="beta_camera_gestures_description" translatable="false">If enabled, you\'ll gain the ability to pinch-to-zoom, drag-to-zoom, and tap to auto focus on the camera screen.</string>
4947
<string name="beta_tipcard_can_flip_description" translatable="false">If enabled, you\'ll gain the ability to tap your own Tip Card to see the back.</string>
5048
<string name="subtitle_remoteSendText" translatable="false">%1$s %2$s</string>
5149
<string name="beta_resetTooltips" translatable="false">Reset Tooltips</string>

0 commit comments

Comments
 (0)