Switch username scanner to use CameraScreen.

This commit is contained in:
Greyson Parrelli
2026-05-04 14:14:45 -04:00
parent 23698dbc28
commit ab090236a1
6 changed files with 186 additions and 257 deletions
@@ -7,7 +7,6 @@ import android.app.Activity
import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.compose.animation.AnimatedVisibility
@@ -49,8 +48,7 @@ import androidx.core.app.ShareCompat
import androidx.core.app.TaskStackBuilder
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@@ -58,8 +56,10 @@ import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.coroutines.CoroutineScope
import org.signal.camera.CameraScreenEvents
import org.signal.camera.CameraScreenState
import org.signal.camera.CameraScreenViewModel
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.ComposeFragment
import org.signal.core.ui.compose.DayNightPreviews
@@ -69,7 +69,6 @@ import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.Snackbars
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.ui.permissions.Permissions
import org.signal.core.util.concurrent.LifecycleDisposable
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
@@ -85,7 +84,6 @@ import java.util.UUID
class UsernameLinkSettingsFragment : ComposeFragment() {
private val viewModel: UsernameLinkSettingsViewModel by viewModels()
private val disposables: LifecycleDisposable = LifecycleDisposable()
private lateinit var galleryLauncher: ActivityResultLauncher<Unit>
@@ -115,21 +113,29 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
val linkCopiedEvent: UUID? by viewModel.linkCopiedEvent
val helpText = stringResource(id = R.string.UsernameLinkSettings_scan_this_qr_code)
val cameraViewModel: CameraScreenViewModel = viewModel { CameraScreenViewModel() }
val cameraState by cameraViewModel.state
val cameraPermissionState: PermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) {
viewModel.onTabSelected(ActiveTab.Scan)
}
LaunchedEffect(cameraViewModel) {
cameraViewModel.qrCodeDetected.collect { data ->
viewModel.onQrCodeScanned(data)
}
}
MainScreen(
state = state,
navController = navController,
lifecycleOwner = viewLifecycleOwner,
disposables = disposables.disposables,
cameraState = cameraState,
cameraEmitter = cameraViewModel::onEvent,
cameraPermissionState = cameraPermissionState,
onCodeTabSelected = { viewModel.onTabSelected(ActiveTab.Code) },
onScanTabSelected = { viewModel.onTabSelected(ActiveTab.Scan) },
onUsernameLinkResetResultHandled = { viewModel.onUsernameLinkResetResultHandled() },
onShareBadge = { shareQrBadge(requireActivity(), viewModel.generateQrCodeImage(helpText)) },
onQrCodeScanned = { data -> viewModel.onQrCodeScanned(data) },
onQrResultHandled = { viewModel.onQrResultHandled() },
onOpenCameraClicked = { askCameraPermissions() },
onOpenGalleryClicked = { galleryLauncher.launch(Unit) },
@@ -143,10 +149,6 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
disposables.bindTo(viewLifecycleOwner)
}
override fun onResume() {
super.onResume()
viewModel.onResume()
@@ -167,14 +169,13 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
private fun MainScreen(
state: UsernameLinkSettingsState,
navController: NavController? = null,
lifecycleOwner: LifecycleOwner = previewLifecycleOwner,
disposables: CompositeDisposable = CompositeDisposable(),
cameraState: CameraScreenState = CameraScreenState(),
cameraEmitter: (CameraScreenEvents) -> Unit = {},
cameraPermissionState: PermissionState = previewPermissionState(),
onCodeTabSelected: () -> Unit = {},
onScanTabSelected: () -> Unit = {},
onUsernameLinkResetResultHandled: () -> Unit = {},
onShareBadge: () -> Unit = {},
onQrCodeScanned: (String) -> Unit = {},
onQrResultHandled: () -> Unit = {},
onOpenCameraClicked: () -> Unit = {},
onOpenGalleryClicked: () -> Unit = {},
@@ -238,10 +239,9 @@ private fun MainScreen(
exit = slideOutHorizontally(targetOffsetX = { fullWidth -> fullWidth })
) {
UsernameQrScanScreen(
lifecycleOwner = lifecycleOwner,
disposables = disposables,
qrScanResult = state.qrScanResult,
onQrCodeScanned = onQrCodeScanned,
cameraState = cameraState,
cameraEmitter = cameraEmitter,
onQrResultHandled = onQrResultHandled,
onOpenCameraClicked = onOpenCameraClicked,
onOpenGalleryClicked = onOpenGalleryClicked,
@@ -394,11 +394,6 @@ private fun previewPermissionState(): PermissionState {
}
}
private val previewLifecycleOwner: LifecycleOwner = object : LifecycleOwner {
override val lifecycle: Lifecycle
get() = throw UnsupportedOperationException("Only for tests")
}
private fun shareQrBadge(activity: Activity, badge: Bitmap?) {
if (badge == null) {
return
@@ -1,45 +1,50 @@
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.LifecycleOwner
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.signal.camera.CameraCaptureMode
import org.signal.camera.CameraScreen
import org.signal.camera.CameraScreenEvents
import org.signal.camera.CameraScreenState
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.qr.QrScannerView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig
import org.thoughtcrime.securesms.qr.QrScanScreens
import org.thoughtcrime.securesms.qr.QrCrosshair
import org.thoughtcrime.securesms.recipients.Recipient
import java.util.concurrent.TimeUnit
/**
* A screen that allows you to scan a QR code to start a chat.
*/
@Composable
fun UsernameQrScanScreen(
lifecycleOwner: LifecycleOwner,
disposables: CompositeDisposable,
qrScanResult: QrScanResult?,
onQrCodeScanned: (String) -> Unit,
cameraState: CameraScreenState,
cameraEmitter: (CameraScreenEvents) -> Unit,
onQrResultHandled: () -> Unit,
onOpenCameraClicked: () -> Unit,
onOpenGalleryClicked: () -> Unit,
@@ -88,26 +93,49 @@ fun UsernameQrScanScreen(
modifier = Modifier
.fillMaxWidth()
.weight(1f, true)
.background(Color.Black)
) {
QrScanScreens.QrScanScreen(
factory = { context ->
val view = QrScannerView(context)
disposables += view.qrData.throttleFirst(3000, TimeUnit.MILLISECONDS).subscribe { data ->
onQrCodeScanned(data)
if (hasCameraPermission) {
CameraScreen(
state = cameraState,
emitter = cameraEmitter,
enableQrScanning = true,
captureMode = CameraCaptureMode.ImageOnly,
roundCorners = false,
fillViewport = true,
modifier = Modifier.fillMaxSize()
) {
QrCrosshair(modifier = Modifier.fillMaxSize())
}
} else {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.align(Alignment.Center)
.padding(48.dp)
) {
Text(
text = stringResource(R.string.CameraXFragment_to_scan_qr_code_allow_camera),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
color = Color.White
)
Buttons.MediumTonal(
colors = ButtonDefaults.filledTonalButtonColors(),
onClick = onOpenCameraClicked
) {
Text(stringResource(R.string.CameraXFragment_allow_access))
}
view
},
update = { view ->
view.start(lifecycleOwner = lifecycleOwner, forceLegacy = CameraXRemoteConfig.isBlocklisted())
},
hasPermission = hasCameraPermission,
onRequestPermissions = onOpenCameraClicked,
qrHeaderLabelString = ""
)
}
}
FloatingActionButton(
shape = CircleShape,
containerColor = SignalTheme.colors.colorSurface1,
modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 24.dp),
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 24.dp),
onClick = onOpenGalleryClicked
) {
Image(
@@ -143,3 +171,37 @@ private fun QrScanResultDialog(title: String? = null, message: String, onDismiss
onDismiss = onDismiss
)
}
@DayNightPreviews
@Composable
private fun UsernameQrScanScreenPreview() {
Previews.Preview {
UsernameQrScanScreen(
qrScanResult = null,
cameraState = CameraScreenState(),
cameraEmitter = {},
onQrResultHandled = {},
onOpenCameraClicked = {},
onOpenGalleryClicked = {},
onRecipientFound = {},
hasCameraPermission = true
)
}
}
@DayNightPreviews
@Composable
private fun UsernameQrScanScreenNoPermissionPreview() {
Previews.Preview {
UsernameQrScanScreen(
qrScanResult = null,
cameraState = CameraScreenState(),
cameraEmitter = {},
onQrResultHandled = {},
onOpenCameraClicked = {},
onOpenGalleryClicked = {},
onRecipientFound = {},
hasCameraPermission = false
)
}
}
@@ -24,22 +24,24 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.permissions.rememberPermissionState
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.signal.camera.CameraScreenEvents
import org.signal.camera.CameraScreenState
import org.signal.camera.CameraScreenViewModel
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.ui.permissions.Permissions
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.getParcelableExtraCompat
import org.signal.core.util.permissions.PermissionCompat
import org.thoughtcrime.securesms.R
@@ -57,7 +59,6 @@ class UsernameQrScannerActivity : AppCompatActivity() {
}
private val viewModel: UsernameQrScannerViewModel by viewModels()
private val disposables = LifecycleDisposable()
@SuppressLint("MissingSuperCall")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
@@ -66,7 +67,6 @@ class UsernameQrScannerActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
disposables.bindTo(this)
val galleryLauncher = registerForActivityResult(QrImageSelectionActivity.Contract()) { uri ->
if (uri != null) {
@@ -86,14 +86,22 @@ class UsernameQrScannerActivity : AppCompatActivity() {
val cameraPermissionState: PermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
val state by viewModel.state
val cameraViewModel: CameraScreenViewModel = viewModel { CameraScreenViewModel() }
val cameraState by cameraViewModel.state
LaunchedEffect(cameraViewModel) {
cameraViewModel.qrCodeDetected.collect { url ->
viewModel.onQrScanned(url)
}
}
SignalTheme {
Content(
lifecycleOwner = this,
diposables = disposables.disposables,
state = state,
cameraState = cameraState,
cameraEmitter = cameraViewModel::onEvent,
galleryPermissionsState = galleryPermissionState,
cameraPermissionState = cameraPermissionState,
onQrScanned = { url -> viewModel.onQrScanned(url) },
onQrResultHandled = {
finish()
},
@@ -143,12 +151,11 @@ class UsernameQrScannerActivity : AppCompatActivity() {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Content(
lifecycleOwner: LifecycleOwner,
diposables: CompositeDisposable,
state: UsernameQrScannerViewModel.ScannerState,
cameraState: CameraScreenState,
cameraEmitter: (CameraScreenEvents) -> Unit,
galleryPermissionsState: MultiplePermissionsState,
cameraPermissionState: PermissionState,
onQrScanned: (String) -> Unit,
onQrResultHandled: () -> Unit,
onOpenCameraClicked: () -> Unit,
onOpenGalleryClicked: () -> Unit,
@@ -173,10 +180,9 @@ fun Content(
}
) { contentPadding ->
UsernameQrScanScreen(
lifecycleOwner = lifecycleOwner,
disposables = diposables,
qrScanResult = state.qrScanResult,
onQrCodeScanned = onQrScanned,
cameraState = cameraState,
cameraEmitter = cameraEmitter,
onQrResultHandled = onQrResultHandled,
onOpenCameraClicked = onOpenCameraClicked,
onOpenGalleryClicked = onOpenGalleryClicked,
@@ -16,16 +16,9 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@@ -41,6 +34,7 @@ import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.linkdevice.LinkDeviceRepository.LinkDeviceResult
import org.thoughtcrime.securesms.qr.QrCrosshair
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
@@ -115,8 +109,6 @@ fun LinkDeviceQrScanScreen(
.background(Color.Black)
) {
if (hasPermission) {
val crosshairPath = remember { Path() }
CameraScreen(
state = cameraState,
emitter = cameraEmitter,
@@ -126,14 +118,7 @@ fun LinkDeviceQrScanScreen(
fillViewport = true,
modifier = Modifier.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxSize()
.drawWithContent {
drawContent()
drawQrCrosshair(crosshairPath)
}
)
QrCrosshair(modifier = Modifier.fillMaxSize())
Text(
text = stringResource(R.string.AddLinkDeviceFragment__scan_the_qr_code),
@@ -173,43 +158,6 @@ fun LinkDeviceQrScanScreen(
}
}
private fun DrawScope.drawQrCrosshair(path: Path) {
val crosshairWidth: Float = size.minDimension * 0.6f
val crosshairLineLength = crosshairWidth * 0.125f
val topLeft = center - Offset(crosshairWidth / 2, crosshairWidth / 2)
val topRight = center + Offset(crosshairWidth / 2, -crosshairWidth / 2)
val bottomRight = center + Offset(crosshairWidth / 2, crosshairWidth / 2)
val bottomLeft = center + Offset(-crosshairWidth / 2, crosshairWidth / 2)
path.reset()
drawPath(
path = path.apply {
moveTo(topLeft.x, topLeft.y + crosshairLineLength)
lineTo(topLeft.x, topLeft.y)
lineTo(topLeft.x + crosshairLineLength, topLeft.y)
moveTo(topRight.x - crosshairLineLength, topRight.y)
lineTo(topRight.x, topRight.y)
lineTo(topRight.x, topRight.y + crosshairLineLength)
moveTo(bottomRight.x, bottomRight.y - crosshairLineLength)
lineTo(bottomRight.x, bottomRight.y)
lineTo(bottomRight.x - crosshairLineLength, bottomRight.y)
moveTo(bottomLeft.x + crosshairLineLength, bottomLeft.y)
lineTo(bottomLeft.x, bottomLeft.y)
lineTo(bottomLeft.x, bottomLeft.y - crosshairLineLength)
},
color = Color.White,
style = Stroke(
width = 3.dp.toPx(),
pathEffect = PathEffect.cornerPathEffect(10.dp.toPx())
)
)
}
private fun makeToast(context: Context, messageId: Int, onLinkDeviceFailure: () -> Unit) {
Toast.makeText(context, messageId, Toast.LENGTH_LONG).show()
onLinkDeviceFailure()
@@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.qr
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp
/**
* Four-corner crosshair overlay used to frame the QR scan target in camera viewfinders.
* The crosshair is sized to 60% of the smaller dimension of its layout.
*/
@Composable
fun QrCrosshair(modifier: Modifier = Modifier) {
val path = remember { Path() }
Canvas(modifier = modifier) {
val crosshairWidth = size.minDimension * 0.6f
val crosshairLineLength = crosshairWidth * 0.125f
val topLeft = center - Offset(crosshairWidth / 2, crosshairWidth / 2)
val topRight = center + Offset(crosshairWidth / 2, -crosshairWidth / 2)
val bottomRight = center + Offset(crosshairWidth / 2, crosshairWidth / 2)
val bottomLeft = center + Offset(-crosshairWidth / 2, crosshairWidth / 2)
path.reset()
drawPath(
path = path.apply {
moveTo(topLeft.x, topLeft.y + crosshairLineLength)
lineTo(topLeft.x, topLeft.y)
lineTo(topLeft.x + crosshairLineLength, topLeft.y)
moveTo(topRight.x - crosshairLineLength, topRight.y)
lineTo(topRight.x, topRight.y)
lineTo(topRight.x, topRight.y + crosshairLineLength)
moveTo(bottomRight.x, bottomRight.y - crosshairLineLength)
lineTo(bottomRight.x, bottomRight.y)
lineTo(bottomRight.x - crosshairLineLength, bottomRight.y)
moveTo(bottomLeft.x + crosshairLineLength, bottomLeft.y)
lineTo(bottomLeft.x, bottomLeft.y)
lineTo(bottomLeft.x, bottomLeft.y - crosshairLineLength)
},
color = Color.White,
style = Stroke(
width = 3.dp.toPx(),
pathEffect = PathEffect.cornerPathEffect(10.dp.toPx())
)
)
}
}
@@ -1,140 +0,0 @@
package org.thoughtcrime.securesms.qr
import android.content.Context
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.viewinterop.NoOpUpdate
import org.signal.core.ui.compose.Buttons
import org.signal.qr.QrScannerView
import org.thoughtcrime.securesms.R
object QrScanScreens {
/**
* Full-screen qr scanning screen with permission-asking UI
*/
@Composable
fun QrScanScreen(
factory: (Context) -> QrScannerView,
update: (QrScannerView) -> Unit = NoOpUpdate,
hasPermission: Boolean,
onRequestPermissions: () -> Unit = {},
qrHeaderLabelString: String
) {
val path = remember { Path() }
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f, true)
) {
AndroidView(
factory = factory,
update = update,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.drawWithContent {
drawContent()
if (hasPermission) {
drawQrCrosshair(path)
}
}
)
if (!hasPermission) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.align(Alignment.Center).padding(48.dp)
) {
Text(
text = stringResource(R.string.CameraXFragment_to_scan_qr_code_allow_camera),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
color = Color.White
)
Buttons.MediumTonal(
colors = ButtonDefaults.filledTonalButtonColors(),
onClick = onRequestPermissions
) {
Text(stringResource(R.string.CameraXFragment_allow_access))
}
}
} else {
Text(
text = qrHeaderLabelString,
style = MaterialTheme.typography.bodyMedium,
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 24.dp).fillMaxWidth()
)
}
}
}
}
private fun DrawScope.drawQrCrosshair(path: Path) {
val crosshairWidth: Float = size.minDimension * 0.6f
val crosshairLineLength = crosshairWidth * 0.125f
val topLeft = center - Offset(crosshairWidth / 2, crosshairWidth / 2)
val topRight = center + Offset(crosshairWidth / 2, -crosshairWidth / 2)
val bottomRight = center + Offset(crosshairWidth / 2, crosshairWidth / 2)
val bottomLeft = center + Offset(-crosshairWidth / 2, crosshairWidth / 2)
path.reset()
drawPath(
path = path.apply {
moveTo(topLeft.x, topLeft.y + crosshairLineLength)
lineTo(topLeft.x, topLeft.y)
lineTo(topLeft.x + crosshairLineLength, topLeft.y)
moveTo(topRight.x - crosshairLineLength, topRight.y)
lineTo(topRight.x, topRight.y)
lineTo(topRight.x, topRight.y + crosshairLineLength)
moveTo(bottomRight.x, bottomRight.y - crosshairLineLength)
lineTo(bottomRight.x, bottomRight.y)
lineTo(bottomRight.x - crosshairLineLength, bottomRight.y)
moveTo(bottomLeft.x + crosshairLineLength, bottomLeft.y)
lineTo(bottomLeft.x, bottomLeft.y)
lineTo(bottomLeft.x, bottomLeft.y - crosshairLineLength)
},
color = Color.White,
style = Stroke(
width = 3.dp.toPx(),
pathEffect = PathEffect.cornerPathEffect(10.dp.toPx())
)
)
}
}