Skip to content

Commit 4517c62

Browse files
committed
feat(scanner): add zoom ratio label during pinch-to-zoom
Show a pill badge with the current zoom level (e.g. 1.0x) below the top bar while a pinch gesture is active. Fades in/out with the gesture. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 9c8e4ac commit 4517c62

5 files changed

Lines changed: 55 additions & 3 deletions

File tree

apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/Scanner.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.compose.runtime.DisposableEffect
66
import androidx.compose.runtime.LaunchedEffect
77
import androidx.compose.runtime.collectAsState
88
import androidx.compose.runtime.getValue
9+
import androidx.compose.runtime.mutableFloatStateOf
910
import androidx.compose.runtime.mutableStateOf
1011
import androidx.compose.runtime.remember
1112
import androidx.compose.runtime.setValue
@@ -59,6 +60,9 @@ internal fun Scanner() {
5960

6061
val vibrator = LocalVibrator.current
6162

63+
var isPinching by remember { mutableStateOf(false) }
64+
var zoomRatio by remember { mutableFloatStateOf(1f) }
65+
6266
LaunchedEffect(biometricsState, previewing) {
6367
if (previewing == true) {
6468
focusManager.clearFocus()
@@ -74,6 +78,8 @@ internal fun Scanner() {
7478
@SuppressLint("LocalContextGetResourceValueCall")
7579
BillContainer(
7680
isPaused = isPaused,
81+
isPinching = isPinching,
82+
zoomRatio = zoomRatio,
7783
onAction = {
7884
when (it) {
7985
ScannerDecorItem.Give -> {
@@ -95,6 +101,10 @@ internal fun Scanner() {
95101
CodeScanner(
96102
scanningEnabled = previewing == true,
97103
cameraGesturesEnabled = true,
104+
onPinchStateChanged = { pinching, zoom ->
105+
isPinching = pinching
106+
zoomRatio = zoom
107+
},
98108
onPreviewStateChanged = {
99109
cameraAvailable = true
100110
previewing = it

apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/BillContainerView.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ import kotlinx.coroutines.delay
6767
internal fun BillContainer(
6868
modifier: Modifier = Modifier,
6969
isPaused: Boolean,
70+
isPinching: Boolean = false,
71+
zoomRatio: Float = 1f,
7072
scannerView: @Composable () -> Unit,
7173
onAction: (ScannerDecorItem) -> Unit
7274
) {
@@ -199,6 +201,8 @@ internal fun BillContainer(
199201
state = updatedState,
200202
billState = updatedBillState,
201203
isPaused = isPaused,
204+
isPinching = isPinching,
205+
zoomRatio = zoomRatio,
202206
onAction = onAction
203207
)
204208
}

apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/ui/components/DecorView.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Column
1313
import androidx.compose.foundation.layout.Row
1414
import androidx.compose.foundation.layout.WindowInsets
1515
import androidx.compose.foundation.layout.fillMaxSize
16+
import androidx.compose.foundation.layout.fillMaxWidth
1617
import androidx.compose.foundation.layout.navigationBars
1718
import androidx.compose.foundation.layout.padding
1819
import androidx.compose.foundation.layout.size
@@ -21,6 +22,7 @@ import androidx.compose.foundation.layout.width
2122
import androidx.compose.foundation.layout.windowInsetsPadding
2223
import androidx.compose.foundation.layout.wrapContentSize
2324
import androidx.compose.foundation.shape.CircleShape
25+
import androidx.compose.foundation.shape.RoundedCornerShape
2426
import androidx.compose.material.Icon
2527
import androidx.compose.material.Text
2628
import androidx.compose.runtime.Composable
@@ -35,7 +37,9 @@ import androidx.compose.ui.graphics.Color
3537
import androidx.compose.ui.platform.testTag
3638
import androidx.compose.ui.res.painterResource
3739
import androidx.compose.ui.res.stringResource
40+
import androidx.compose.ui.text.font.FontWeight
3841
import androidx.compose.ui.unit.dp
42+
import androidx.compose.ui.unit.sp
3943
import androidx.lifecycle.compose.collectAsStateWithLifecycle
4044
import com.flipcash.app.bill.customization.LocalBillPlaygroundController
4145
import com.flipcash.app.core.bill.BillState
@@ -44,6 +48,7 @@ import com.flipcash.app.session.SessionState
4448
import com.flipcash.features.scanner.R
4549
import com.getcode.theme.CodeTheme
4650
import com.getcode.theme.xxl
51+
import com.getcode.ui.components.Pill
4752
import com.getcode.ui.core.rememberedClickable
4853
import com.getcode.ui.core.unboundedClickable
4954
import com.getcode.utils.network.LocalNetworkObserver
@@ -53,6 +58,8 @@ internal fun DecorView(
5358
state: SessionState,
5459
billState: BillState,
5560
isPaused: Boolean,
61+
isPinching: Boolean = false,
62+
zoomRatio: Float = 1f,
5663
modifier: Modifier = Modifier,
5764
onAction: (ScannerDecorItem) -> Unit,
5865
) {
@@ -111,6 +118,29 @@ internal fun DecorView(
111118
)
112119
}
113120

121+
AnimatedVisibility(
122+
modifier = Modifier
123+
.align(Alignment.TopCenter)
124+
.statusBarsPadding()
125+
.padding(top = CodeTheme.dimens.grid.x9),
126+
visible = isPinching,
127+
enter = fadeIn(tween(150)),
128+
exit = fadeOut(tween(250)),
129+
) {
130+
Box(
131+
modifier = Modifier.fillMaxSize(),
132+
contentAlignment = Alignment.TopCenter,
133+
) {
134+
Pill(
135+
text = "${"%.1f".format(zoomRatio)}x",
136+
textStyle = CodeTheme.typography.textSmall.copy(
137+
fontWeight = FontWeight.Bold
138+
),
139+
shape = CodeTheme.shapes.xxl,
140+
)
141+
}
142+
}
143+
114144
Column(modifier = Modifier.align(Alignment.BottomCenter)) {
115145
val networkState by LocalNetworkObserver.current.state.collectAsState()
116146

ui/scanner/src/main/kotlin/com/getcode/ui/scanner/CodeScanner.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,24 @@ import androidx.camera.core.Preview
1111
import androidx.camera.lifecycle.ProcessCameraProvider
1212
import androidx.camera.view.PreviewView
1313
import androidx.compose.animation.AnimatedVisibility
14-
import androidx.compose.runtime.DisposableEffect
1514
import androidx.compose.animation.core.tween
1615
import androidx.compose.animation.fadeIn
1716
import androidx.compose.animation.fadeOut
1817
import androidx.compose.foundation.background
1918
import androidx.compose.foundation.layout.Box
2019
import androidx.compose.foundation.layout.fillMaxSize
2120
import androidx.compose.runtime.Composable
21+
import androidx.compose.runtime.DisposableEffect
2222
import androidx.compose.runtime.LaunchedEffect
2323
import androidx.compose.runtime.getValue
24+
import androidx.compose.runtime.mutableFloatStateOf
2425
import androidx.compose.runtime.mutableStateOf
2526
import androidx.compose.runtime.remember
2627
import androidx.compose.runtime.rememberCoroutineScope
2728
import androidx.compose.runtime.setValue
2829
import androidx.compose.ui.Modifier
2930
import androidx.compose.ui.geometry.Offset
3031
import androidx.compose.ui.platform.LocalContext
31-
import androidx.compose.ui.platform.testTag
3232
import androidx.compose.ui.viewinterop.AndroidView
3333
import androidx.lifecycle.Lifecycle
3434
import androidx.lifecycle.LifecycleOwner
@@ -60,6 +60,7 @@ fun CodeScanner(
6060
scanningEnabled: Boolean,
6161
cameraGesturesEnabled: Boolean,
6262
modifier: Modifier = Modifier,
63+
onPinchStateChanged: (Boolean, Float) -> Unit,
6364
onPreviewStateChanged: (Boolean) -> Unit,
6465
onCodeScanned: (CodeScanResult) -> Unit,
6566
onError: (Throwable) -> Unit = { },
@@ -97,6 +98,8 @@ fun CodeScanner(
9798
var camera by remember { mutableStateOf<Camera?>(null) }
9899
var autoFocusPoint by remember { mutableStateOf(Offset.Unspecified) }
99100
var gestureController by remember { mutableStateOf<CameraGestureController?>(null) }
101+
var isPinching by remember { mutableStateOf(false) }
102+
var zoomRatio by remember { mutableFloatStateOf(1f) }
100103

101104
val codeAnalyzer = rememberMultiCodeAnalyzer(
102105
onCodeScanned = onCodeScanned,
@@ -170,6 +173,7 @@ fun CodeScanner(
170173
cameraControl = it.cameraControl,
171174
cameraInfo = it.cameraInfo,
172175
gesturesEnabled = cameraGesturesEnabled,
176+
onPinchStateChanged = onPinchStateChanged,
173177
) { touchedAt ->
174178
autoFocusPoint = touchedAt
175179
previewView.meteringPointFactory.createPoint(touchedAt.x, touchedAt.y)
@@ -279,4 +283,4 @@ suspend fun bindWithRetry(
279283
throw NoCamerasAvailableException()
280284
}
281285

282-
class NoCamerasAvailableException: Throwable()
286+
class NoCamerasAvailableException : Throwable()

ui/scanner/src/main/kotlin/com/getcode/ui/scanner/internal/CameraGestureController.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ internal class CameraGestureController(
1919
private val gesturesEnabled: Boolean,
2020
private val cameraControl: CameraControl,
2121
private val cameraInfo: CameraInfo,
22+
private val onPinchStateChanged: (isPinching: Boolean, zoomRatio: Float) -> Unit = { _, _ -> },
2223
onTap: (Offset) -> MeteringPoint,
2324
) {
2425
private val handler = Handler(Looper.getMainLooper())
@@ -52,6 +53,7 @@ internal class CameraGestureController(
5253
gestureZoomFactor = currentZoom
5354
appliedZoom = currentZoom
5455
cumulativeScale = 1f
56+
onPinchStateChanged(true, currentZoom)
5557
return true
5658
}
5759

@@ -65,6 +67,7 @@ internal class CameraGestureController(
6567
// Lerp toward target to smooth lens-switch transitions
6668
appliedZoom += (targetZoom - appliedZoom) * 0.4f
6769
cameraControl.setZoomRatio(appliedZoom)
70+
onPinchStateChanged(true, appliedZoom)
6871
return true
6972
}
7073

@@ -122,6 +125,7 @@ internal class CameraGestureController(
122125

123126
if (event.action == MotionEvent.ACTION_UP) {
124127
if (isPinching) {
128+
onPinchStateChanged(false, currentZoom)
125129
animateZoomReset(cameraInfo, cameraControl)
126130
initialZoomRatio = currentZoom
127131
isPinching = false

0 commit comments

Comments
 (0)