Skip to content

Commit 12c389a

Browse files
committed
QR haptics
1 parent 9565d49 commit 12c389a

2 files changed

Lines changed: 111 additions & 77 deletions

File tree

app/src/main/java/com/sameerasw/airsync/presentation/ui/activities/QRScannerActivity.kt

Lines changed: 96 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,19 @@ import androidx.compose.material3.*
2222
import androidx.compose.runtime.*
2323
import androidx.compose.ui.Alignment
2424
import androidx.compose.ui.Modifier
25+
import androidx.compose.ui.draw.scale
2526
import androidx.compose.ui.graphics.Color
27+
import androidx.compose.ui.platform.LocalHapticFeedback
2628
import androidx.compose.ui.text.style.TextAlign
2729
import androidx.compose.ui.unit.dp
2830
import androidx.compose.ui.viewinterop.AndroidView
29-
import androidx.compose.foundation.Canvas
3031
import androidx.core.content.ContextCompat
32+
import androidx.core.view.WindowCompat
3133
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
3234
import com.google.mlkit.vision.barcode.BarcodeScanning
3335
import com.google.mlkit.vision.common.InputImage
3436
import com.sameerasw.airsync.ui.theme.AirSyncTheme
37+
import com.sameerasw.airsync.utils.HapticUtil
3538
import java.util.concurrent.Executors
3639

3740
class QRScannerActivity : ComponentActivity() {
@@ -73,102 +76,116 @@ class QRScannerActivity : ComponentActivity() {
7376
onClosed = {
7477
setResult(RESULT_CANCELED)
7578
finish()
76-
}
79+
},
80+
activity = this
7781
)
7882
}
7983
}
8084
}
8185
}
8286

87+
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
8388
@SuppressLint("UnsafeOptInUsageError")
8489
@Composable
8590
private fun QRScannerScreen(
8691
onQrScanned: (String) -> Unit,
87-
onClosed: () -> Unit
92+
onClosed: () -> Unit,
93+
activity: ComponentActivity? = null
8894
) {
8995
var scanned by remember { mutableStateOf(false) }
9096
var lastScannedCode by remember { mutableStateOf("") }
9197
var scanMessage by remember { mutableStateOf("") }
98+
var isActivelyScanning by remember { mutableStateOf(false) }
99+
var loadingHapticsJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) }
100+
val haptics = LocalHapticFeedback.current
92101

93-
Scaffold(
94-
modifier = Modifier.fillMaxSize(),
95-
topBar = {
96-
@OptIn(ExperimentalMaterial3Api::class)
97-
TopAppBar(
98-
title = { Text("Scan QR Code") },
99-
navigationIcon = {
100-
IconButton(onClick = onClosed) {
101-
Icon(
102-
Icons.AutoMirrored.Filled.ArrowBack,
103-
contentDescription = "Back"
104-
)
105-
}
106-
}
107-
)
102+
// Make camera draw behind system bars
103+
LaunchedEffect(Unit) {
104+
if (activity != null) {
105+
WindowCompat.setDecorFitsSystemWindows(activity.window, false)
108106
}
109-
) { innerPadding ->
107+
}
108+
109+
// Continuous subtle haptic feedback while actively scanning
110+
LaunchedEffect(isActivelyScanning) {
111+
if (isActivelyScanning) {
112+
loadingHapticsJob = HapticUtil.startLoadingHaptics(haptics)
113+
} else {
114+
loadingHapticsJob?.cancel()
115+
loadingHapticsJob = null
116+
}
117+
}
118+
119+
Box(
120+
modifier = Modifier
121+
.fillMaxSize()
122+
.background(Color.Black)
123+
) {
124+
QrCodeScannerView(
125+
modifier = Modifier.fillMaxSize(),
126+
onQrScanned = { qrData ->
127+
if (!scanned && qrData != lastScannedCode) {
128+
lastScannedCode = qrData
129+
scanned = true
130+
scanMessage = "QR Code detected!"
131+
Log.d("QrScanner", "QR detected in screen: $qrData")
132+
133+
// Trigger 3-step haptic feedback on successful scan
134+
HapticUtil.performSuccess(haptics)
135+
136+
isActivelyScanning = false
137+
loadingHapticsJob?.cancel()
138+
// Small delay to ensure UI update before finishing
139+
onQrScanned(qrData)
140+
}
141+
},
142+
onActiveScanningChanged = { isScanning ->
143+
isActivelyScanning = isScanning
144+
}
145+
)
146+
147+
// Scanning indicator
110148
Box(
111149
modifier = Modifier
112-
.fillMaxSize()
113-
.padding(innerPadding)
114-
.background(Color.Black)
150+
.align(Alignment.Center),
151+
contentAlignment = Alignment.Center
115152
) {
116-
QrCodeScannerView(
117-
modifier = Modifier.fillMaxSize(),
118-
onQrScanned = { qrData ->
119-
if (!scanned && qrData != lastScannedCode) {
120-
lastScannedCode = qrData
121-
scanned = true
122-
scanMessage = "QR Code detected!"
123-
Log.d("QrScanner", "QR detected in screen: $qrData")
124-
// Small delay to ensure UI update before finishing
125-
onQrScanned(qrData)
126-
}
127-
}
128-
)
153+
LoadingIndicator(modifier = Modifier.scale(2f))
154+
}
129155

130-
// Scanning frame overlay
131-
Canvas(
132-
modifier = Modifier
133-
.size(300.dp)
134-
.align(Alignment.Center)
135-
) {
136-
drawRect(
137-
color = Color(0x99FFFFFF),
138-
topLeft = androidx.compose.ui.geometry.Offset(
139-
(size.width - 200.dp.toPx()) / 2,
140-
(size.height - 200.dp.toPx()) / 2
141-
),
142-
size = androidx.compose.ui.geometry.Size(200.dp.toPx(), 200.dp.toPx()),
143-
style = androidx.compose.ui.graphics.drawscope.Stroke(width = 3f)
144-
)
145-
}
156+
// Back button overlay on top-left
157+
IconButton(
158+
onClick = onClosed,
159+
modifier = Modifier
160+
.align(Alignment.TopStart)
161+
.padding(16.dp)
162+
.systemBarsPadding()
163+
.background(MaterialTheme.colorScheme.surface, shape = MaterialTheme.shapes.large)
164+
) {
165+
Icon(
166+
Icons.AutoMirrored.Filled.ArrowBack,
167+
contentDescription = "Back",
168+
tint = MaterialTheme.colorScheme.onSurface,
169+
)
170+
}
146171

147-
// Status messages
148-
Column(
149-
modifier = Modifier
150-
.align(Alignment.BottomCenter)
151-
.padding(16.dp)
152-
.fillMaxWidth(),
153-
horizontalAlignment = Alignment.CenterHorizontally,
154-
verticalArrangement = Arrangement.spacedBy(8.dp)
155-
) {
156-
if (scanMessage.isNotEmpty()) {
157-
Text(
158-
scanMessage,
159-
color = Color.Green,
160-
textAlign = TextAlign.Center,
161-
modifier = Modifier.padding(bottom = 8.dp),
162-
style = MaterialTheme.typography.titleMedium
163-
)
164-
}
165-
Text(
166-
"Position QR code in frame\nKeep phone steady",
167-
color = Color.White,
168-
textAlign = TextAlign.Center,
169-
style = MaterialTheme.typography.bodyMedium
170-
)
171-
}
172+
// Status messages with background
173+
Column(
174+
modifier = Modifier
175+
.align(Alignment.BottomCenter)
176+
.padding(16.dp)
177+
.systemBarsPadding()
178+
.background(MaterialTheme.colorScheme.surface, shape = MaterialTheme.shapes.large)
179+
.padding(16.dp),
180+
horizontalAlignment = Alignment.CenterHorizontally,
181+
verticalArrangement = Arrangement.spacedBy(8.dp)
182+
) {
183+
Text(
184+
"Scan the QR code to connect",
185+
color = MaterialTheme.colorScheme.onSurface,
186+
textAlign = TextAlign.Center,
187+
style = MaterialTheme.typography.bodyMedium
188+
)
172189
}
173190
}
174191
}
@@ -177,7 +194,8 @@ private fun QRScannerScreen(
177194
@Composable
178195
fun QrCodeScannerView(
179196
modifier: Modifier = Modifier,
180-
onQrScanned: (String) -> Unit
197+
onQrScanned: (String) -> Unit,
198+
onActiveScanningChanged: (Boolean) -> Unit = {}
181199
) {
182200
var lastScanned by remember { mutableStateOf("") }
183201

@@ -219,6 +237,7 @@ fun QrCodeScannerView(
219237
processQrImage(scanner, imageProxy) { result ->
220238
if (result != lastScanned && result.isNotEmpty()) {
221239
lastScanned = result
240+
onActiveScanningChanged(true)
222241
Log.d("QrScanner", "QR scanned, calling callback with: $result")
223242
onQrScanned(result)
224243
}

app/src/main/java/com/sameerasw/airsync/utils/HapticUtil.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,21 @@ object HapticUtil {
4747
} catch (_: Exception) {}
4848
}
4949

50+
/**
51+
* Perform a success haptic - 3 quick taps for successful operations
52+
*/
53+
fun performSuccess(haptics: HapticFeedback?) {
54+
try {
55+
CoroutineScope(Dispatchers.Main).launch {
56+
haptics?.performHapticFeedback(HapticFeedbackType.LongPress)
57+
delay(100)
58+
haptics?.performHapticFeedback(HapticFeedbackType.LongPress)
59+
delay(50)
60+
haptics?.performHapticFeedback(HapticFeedbackType.LongPress)
61+
}
62+
} catch (_: Exception) {}
63+
}
64+
5065
/**
5166
* Start repeating haptic ticks for loading states
5267
* Returns a Job that can be cancelled to stop the haptics

0 commit comments

Comments
 (0)