* [#1413] [#1460] [#1461] QR code scanning from gallery picker Closes #1413 Closes #1460 Closes #1461 * [#1413] [#1460] [#1461] Code cleanup * [#1413] [#1460] [#1461] Camera scan frame anchored to a static view Closes #1413 Closes #1460 Closes #1461 * [#1413] [#1460] [#1461] Code cleanup Closes #1413 Closes #1460 Closes #1461 * Changelog update - Unrelated change: this commit also removes a log from Navigation that was introduced in some of the previous changes and does not describe the actual execution state --------- Co-authored-by: Milan Cerovsky <milan.cerovsky@leeaf.life> Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
parent
987595eaa2
commit
31649ff718
|
@ -13,6 +13,7 @@ directly impact users rather than highlighting other key architectural updates.*
|
||||||
- New bubble message style for the Send and Transaction history item text components
|
- New bubble message style for the Send and Transaction history item text components
|
||||||
- Display all messages within the transaction history record when it is expanded
|
- Display all messages within the transaction history record when it is expanded
|
||||||
- The Dark mode is now officially supported by the entire app UI
|
- The Dark mode is now officially supported by the entire app UI
|
||||||
|
- The Scan screen now allows users to pick and scan a QR code of an address from a photo saved in the device library
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- The Not Enough Free Space screen UI has been slightly refactored to align with the latest design guidelines
|
- The Not Enough Free Space screen UI has been slightly refactored to align with the latest design guidelines
|
||||||
|
|
|
@ -53,6 +53,7 @@ class ScanViewTestSetup(
|
||||||
snackbarHostState = SnackbarHostState(),
|
snackbarHostState = SnackbarHostState(),
|
||||||
onBack = {},
|
onBack = {},
|
||||||
onScanned = {},
|
onScanned = {},
|
||||||
|
onScanError = {},
|
||||||
onOpenSettings = {
|
onOpenSettings = {
|
||||||
onOpenSettingsCount.incrementAndGet()
|
onOpenSettingsCount.incrementAndGet()
|
||||||
},
|
},
|
||||||
|
|
|
@ -35,6 +35,7 @@ class ScanViewBasicTestSetup(
|
||||||
onBackCount.incrementAndGet()
|
onBackCount.incrementAndGet()
|
||||||
},
|
},
|
||||||
onScanned = {},
|
onScanned = {},
|
||||||
|
onScanError = {},
|
||||||
onOpenSettings = {},
|
onOpenSettings = {},
|
||||||
onScanStateChanged = {
|
onScanStateChanged = {
|
||||||
scanState.set(it)
|
scanState.set(it)
|
||||||
|
|
|
@ -318,7 +318,6 @@ private fun MainActivity.NavigationHome(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else if (ConfigurationEntries.IS_APP_UPDATE_CHECK_ENABLED.getValue(RemoteConfig.current)) {
|
} else if (ConfigurationEntries.IS_APP_UPDATE_CHECK_ENABLED.getValue(RemoteConfig.current)) {
|
||||||
Twig.info { "App update available" }
|
|
||||||
WrapCheckForUpdate()
|
WrapCheckForUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,9 @@ fun WrapScan(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onScanError = {
|
||||||
|
addressValidationResult = AddressType.Invalid()
|
||||||
|
},
|
||||||
onOpenSettings = {
|
onOpenSettings = {
|
||||||
runCatching {
|
runCatching {
|
||||||
context.startActivity(SettingsUtil.newSettingsIntent(context.packageName))
|
context.startActivity(SettingsUtil.newSettingsIntent(context.packageName))
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.scan.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
|
import co.electriccoin.zcash.spackle.Twig
|
||||||
|
import com.google.zxing.BarcodeFormat
|
||||||
|
import com.google.zxing.BinaryBitmap
|
||||||
|
import com.google.zxing.DecodeHintType
|
||||||
|
import com.google.zxing.MultiFormatReader
|
||||||
|
import com.google.zxing.RGBLuminanceSource
|
||||||
|
import com.google.zxing.common.HybridBinarizer
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class ImageUriToQrCodeConverter {
|
||||||
|
suspend operator fun invoke(
|
||||||
|
context: Context,
|
||||||
|
uri: Uri
|
||||||
|
): String? =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
uri.toBitmap(context)
|
||||||
|
.toBinaryBitmap()
|
||||||
|
.toQRCode()
|
||||||
|
}.onFailure {
|
||||||
|
Twig.error(it) { "Failed to convert Uri to QR code" }
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Uri.toBitmap(context: Context): Bitmap =
|
||||||
|
context.contentResolver.openInputStream(this)
|
||||||
|
.use {
|
||||||
|
BitmapFactory.decodeStream(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Bitmap.toBinaryBitmap(): BinaryBitmap {
|
||||||
|
val width = this.width
|
||||||
|
val height = this.height
|
||||||
|
val pixels = IntArray(width * height)
|
||||||
|
this.getPixels(pixels, 0, width, 0, 0, width, height)
|
||||||
|
this.recycle()
|
||||||
|
val source = RGBLuminanceSource(width, height, pixels)
|
||||||
|
return BinaryBitmap(HybridBinarizer(source))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BinaryBitmap.toQRCode(): String =
|
||||||
|
MultiFormatReader()
|
||||||
|
.apply {
|
||||||
|
setHints(mapOf(DecodeHintType.POSSIBLE_FORMATS to arrayListOf(BarcodeFormat.QR_CODE)))
|
||||||
|
}
|
||||||
|
.decode(this@toQRCode).text
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ package co.electriccoin.zcash.ui.screen.scan.view
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.camera.core.CameraControl
|
import androidx.camera.core.CameraControl
|
||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
import androidx.camera.core.ImageAnalysis
|
import androidx.camera.core.ImageAnalysis
|
||||||
|
@ -15,6 +17,7 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
@ -29,9 +32,13 @@ import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
@ -44,6 +51,7 @@ import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
@ -67,9 +75,11 @@ import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
||||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanState
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanState
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.util.ImageUriToQrCodeConverter
|
||||||
import co.electriccoin.zcash.ui.screen.scan.util.QrCodeAnalyzer
|
import co.electriccoin.zcash.ui.screen.scan.util.QrCodeAnalyzer
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.PermissionState
|
import com.google.accompanist.permissions.PermissionState
|
||||||
|
import com.google.accompanist.permissions.PermissionStatus
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.google.accompanist.permissions.shouldShowRationale
|
import com.google.accompanist.permissions.shouldShowRationale
|
||||||
|
@ -78,6 +88,7 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.guava.await
|
import kotlinx.coroutines.guava.await
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
@ -89,10 +100,11 @@ private fun ScanPreview() {
|
||||||
snackbarHostState = SnackbarHostState(),
|
snackbarHostState = SnackbarHostState(),
|
||||||
onBack = {},
|
onBack = {},
|
||||||
onScanned = {},
|
onScanned = {},
|
||||||
|
onScanError = {},
|
||||||
onOpenSettings = {},
|
onOpenSettings = {},
|
||||||
onScanStateChanged = {},
|
onScanStateChanged = {},
|
||||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||||
addressValidationResult = AddressType.Transparent,
|
addressValidationResult = AddressType.Invalid(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,6 +119,7 @@ private fun ScanDarkPreview() {
|
||||||
snackbarHostState = SnackbarHostState(),
|
snackbarHostState = SnackbarHostState(),
|
||||||
onBack = {},
|
onBack = {},
|
||||||
onScanned = {},
|
onScanned = {},
|
||||||
|
onScanError = {},
|
||||||
onOpenSettings = {},
|
onOpenSettings = {},
|
||||||
onScanStateChanged = {},
|
onScanStateChanged = {},
|
||||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||||
|
@ -118,30 +131,48 @@ private fun ScanDarkPreview() {
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@Suppress("LongParameterList", "UnusedMaterial3ScaffoldPaddingParameter")
|
@Suppress("LongParameterList", "UnusedMaterial3ScaffoldPaddingParameter", "LongMethod")
|
||||||
fun Scan(
|
fun Scan(
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onScanned: (String) -> Unit,
|
onScanned: (String) -> Unit,
|
||||||
|
onScanError: () -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
onScanStateChanged: (ScanState) -> Unit,
|
onScanStateChanged: (ScanState) -> Unit,
|
||||||
topAppBarSubTitleState: TopAppBarSubTitleState,
|
topAppBarSubTitleState: TopAppBarSubTitleState,
|
||||||
addressValidationResult: AddressType?
|
addressValidationResult: AddressType?
|
||||||
) {
|
) {
|
||||||
val permissionState =
|
val permissionState =
|
||||||
rememberPermissionState(
|
if (LocalInspectionMode.current) {
|
||||||
Manifest.permission.CAMERA
|
remember {
|
||||||
)
|
object : PermissionState {
|
||||||
|
override val permission = Manifest.permission.CAMERA
|
||||||
|
override val status = PermissionStatus.Granted
|
||||||
|
|
||||||
|
override fun launchPermissionRequest() = Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rememberPermissionState(
|
||||||
|
Manifest.permission.CAMERA
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val (scanState, setScanState) =
|
val (scanState, setScanState) =
|
||||||
rememberSaveable {
|
if (LocalInspectionMode.current) {
|
||||||
mutableStateOf(
|
remember {
|
||||||
if (permissionState.status.isGranted) {
|
mutableStateOf(ScanState.Scanning)
|
||||||
ScanState.Scanning
|
}
|
||||||
} else {
|
} else {
|
||||||
ScanState.Permission
|
rememberSaveable {
|
||||||
}
|
mutableStateOf(
|
||||||
)
|
if (permissionState.status.isGranted) {
|
||||||
|
ScanState.Scanning
|
||||||
|
} else {
|
||||||
|
ScanState.Permission
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
@ -151,6 +182,7 @@ fun Scan(
|
||||||
ScanMainContent(
|
ScanMainContent(
|
||||||
addressValidationResult = addressValidationResult,
|
addressValidationResult = addressValidationResult,
|
||||||
onScanned = onScanned,
|
onScanned = onScanned,
|
||||||
|
onScanError = onScanError,
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onScanStateChanged = onScanStateChanged,
|
onScanStateChanged = onScanStateChanged,
|
||||||
|
@ -282,11 +314,12 @@ data class FramePosition(
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Suppress("LongMethod", "LongParameterList")
|
@Suppress("LongMethod", "LongParameterList", "CyclomaticComplexMethod", "MagicNumber")
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScanMainContent(
|
private fun ScanMainContent(
|
||||||
addressValidationResult: AddressType?,
|
addressValidationResult: AddressType?,
|
||||||
onScanned: (String) -> Unit,
|
onScanned: (String) -> Unit,
|
||||||
|
onScanError: () -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onScanStateChanged: (ScanState) -> Unit,
|
onScanStateChanged: (ScanState) -> Unit,
|
||||||
|
@ -317,9 +350,13 @@ private fun ScanMainContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the best frame size for the current device screen
|
// Calculate the best frame size for the current device screen
|
||||||
val framePossibleSize = remember { mutableStateOf(IntSize.Zero) }
|
var framePossibleSize by remember { mutableStateOf(IntSize.Zero) }
|
||||||
|
|
||||||
val frameActualSize = (framePossibleSize.value.width * FRAME_SIZE_RATIO).roundToInt()
|
val frameActualSize by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
(framePossibleSize.width * FRAME_SIZE_RATIO).roundToInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
@ -327,34 +364,60 @@ private fun ScanMainContent(
|
||||||
|
|
||||||
val framePosition =
|
val framePosition =
|
||||||
FramePosition(
|
FramePosition(
|
||||||
left = (framePossibleSize.value.width - frameActualSize) / 2f,
|
left = (framePossibleSize.width - frameActualSize) / 2f,
|
||||||
top = (framePossibleSize.value.height - frameActualSize) / 2f,
|
top = (framePossibleSize.height - frameActualSize) / 2f,
|
||||||
right = (framePossibleSize.value.width - frameActualSize) / 2f + frameActualSize,
|
right = (framePossibleSize.width - frameActualSize) / 2f + frameActualSize,
|
||||||
bottom = (framePossibleSize.value.height - frameActualSize) / 2f + frameActualSize,
|
bottom = (framePossibleSize.height - frameActualSize) / 2f + frameActualSize,
|
||||||
screenHeight = with(density) { configuration.screenHeightDp.dp.roundToPx() },
|
screenHeight = with(density) { configuration.screenHeightDp.dp.roundToPx() },
|
||||||
screenWidth = with(density) { configuration.screenWidthDp.dp.roundToPx() }
|
screenWidth = with(density) { configuration.screenWidthDp.dp.roundToPx() }
|
||||||
)
|
)
|
||||||
|
|
||||||
val (isTorchOn, setIsTorchOn) = rememberSaveable { mutableStateOf(false) }
|
val (isTorchOn, setIsTorchOn) = rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val convertImageUriToQrCode by remember { mutableStateOf(ImageUriToQrCodeConverter()) }
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val galleryLauncher =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.GetContent(),
|
||||||
|
onResult = { uri ->
|
||||||
|
uri?.let {
|
||||||
|
scope.launch {
|
||||||
|
val qrCode = convertImageUriToQrCode(context = context, uri = uri)
|
||||||
|
if (qrCode == null) {
|
||||||
|
onScanError()
|
||||||
|
} else {
|
||||||
|
onScanned(qrCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
ConstraintLayout(modifier = modifier) {
|
ConstraintLayout(modifier = modifier) {
|
||||||
val (frame, bottomItems) = createRefs()
|
val (frame, bottomItems, bottomAnchor) = createRefs()
|
||||||
|
|
||||||
when (scanState) {
|
when (scanState) {
|
||||||
ScanState.Permission -> {
|
ScanState.Permission -> {
|
||||||
// Keep initial ui state
|
// Keep initial ui state
|
||||||
onScanStateChanged(ScanState.Permission)
|
onScanStateChanged(ScanState.Permission)
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanState.Scanning -> {
|
ScanState.Scanning -> {
|
||||||
onScanStateChanged(ScanState.Scanning)
|
onScanStateChanged(ScanState.Scanning)
|
||||||
|
|
||||||
ScanCameraView(
|
if (!LocalInspectionMode.current) {
|
||||||
framePosition = framePosition,
|
ScanCameraView(
|
||||||
isTorchOn = isTorchOn,
|
framePosition = framePosition,
|
||||||
onScanned = onScanned,
|
isTorchOn = isTorchOn,
|
||||||
permissionState = permissionState,
|
onScanned = onScanned,
|
||||||
setScanState = setScanState,
|
permissionState = permissionState,
|
||||||
)
|
setScanState = setScanState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
clipRect(
|
clipRect(
|
||||||
|
@ -368,7 +431,23 @@ private fun ScanMainContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Image(
|
ImageButton(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_gallery),
|
||||||
|
contentDescription = stringResource(id = R.string.gallery_content_description),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.offset(
|
||||||
|
x =
|
||||||
|
with(density) {
|
||||||
|
framePosition.left.toDp() - ZcashTheme.dimens.spacingMid
|
||||||
|
},
|
||||||
|
y = with(density) { framePosition.bottom.toDp() }
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
galleryLauncher.launch("image/*")
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageButton(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (isTorchOn) {
|
if (isTorchOn) {
|
||||||
ImageVector.vectorResource(R.drawable.ic_torch_off)
|
ImageVector.vectorResource(R.drawable.ic_torch_off)
|
||||||
|
@ -388,16 +467,12 @@ private fun ScanMainContent(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
y = with(density) { framePosition.bottom.toDp() }
|
y = with(density) { framePosition.bottom.toDp() }
|
||||||
)
|
),
|
||||||
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
|
) {
|
||||||
.clickable { setIsTorchOn(!isTorchOn) }
|
setIsTorchOn(!isTorchOn)
|
||||||
.padding(ZcashTheme.dimens.spacingDefault)
|
}
|
||||||
.size(
|
|
||||||
width = ZcashTheme.dimens.cameraTorchButton,
|
|
||||||
height = ZcashTheme.dimens.cameraTorchButton
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanState.Failed -> {
|
ScanState.Failed -> {
|
||||||
onScanStateChanged(ScanState.Failed)
|
onScanStateChanged(ScanState.Failed)
|
||||||
}
|
}
|
||||||
|
@ -408,14 +483,14 @@ private fun ScanMainContent(
|
||||||
Modifier
|
Modifier
|
||||||
.constrainAs(frame) {
|
.constrainAs(frame) {
|
||||||
top.linkTo(parent.top)
|
top.linkTo(parent.top)
|
||||||
bottom.linkTo(bottomItems.top)
|
bottom.linkTo(bottomAnchor.top)
|
||||||
start.linkTo(parent.start)
|
start.linkTo(parent.start)
|
||||||
end.linkTo(parent.end)
|
end.linkTo(parent.end)
|
||||||
width = Dimension.fillToConstraints
|
width = Dimension.fillToConstraints
|
||||||
height = Dimension.fillToConstraints
|
height = Dimension.fillToConstraints
|
||||||
}
|
}
|
||||||
.onSizeChanged { coordinates ->
|
.onSizeChanged { coordinates ->
|
||||||
framePossibleSize.value = coordinates
|
framePossibleSize = coordinates
|
||||||
},
|
},
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
@ -425,6 +500,15 @@ private fun ScanMainContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight(.28f)
|
||||||
|
.constrainAs(bottomAnchor) {
|
||||||
|
bottom.linkTo(parent.bottom)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
@ -447,6 +531,28 @@ private fun ScanMainContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ImageButton(
|
||||||
|
imageVector: ImageVector,
|
||||||
|
contentDescription: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
imageVector = imageVector,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
|
||||||
|
.clickable { onClick() }
|
||||||
|
.padding(ZcashTheme.dimens.spacingDefault)
|
||||||
|
.size(
|
||||||
|
width = ZcashTheme.dimens.cameraTorchButton,
|
||||||
|
height = ZcashTheme.dimens.cameraTorchButton
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScanFrame(
|
fun ScanFrame(
|
||||||
frameSize: Int,
|
frameSize: Int,
|
||||||
|
@ -457,18 +563,15 @@ fun ScanFrame(
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
modifier
|
modifier
|
||||||
.then(
|
.size(with(LocalDensity.current) { frameSize.toDp() })
|
||||||
Modifier
|
.background(
|
||||||
.size(with(LocalDensity.current) { frameSize.toDp() })
|
if (isScanning) {
|
||||||
.background(
|
Color.Transparent
|
||||||
if (isScanning) {
|
} else {
|
||||||
Color.Transparent
|
ZcashTheme.colors.cameraDisabledFrameColor
|
||||||
} else {
|
}
|
||||||
ZcashTheme.colors.cameraDisabledFrameColor
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.testTag(ScanTag.QR_FRAME)
|
|
||||||
)
|
)
|
||||||
|
.testTag(ScanTag.QR_FRAME)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = ImageVector.vectorResource(R.drawable.ic_scan_corner),
|
imageVector = ImageVector.vectorResource(R.drawable.ic_scan_corner),
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="26dp"
|
||||||
|
android:height="20dp"
|
||||||
|
android:viewportWidth="26"
|
||||||
|
android:viewportHeight="20">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0.839,0.474h25v18.611h-25z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M3.617,0.474C2.089,0.474 0.839,1.724 0.839,3.252V16.308C0.839,17.835 2.089,19.085 3.617,19.085H23.034C24.562,19.085 25.812,17.835 25.812,16.308V15.002C25.812,15.002 25.812,14.974 25.812,14.946V3.252C25.812,1.724 24.562,0.474 23.034,0.474H3.617ZM3.617,1.946H23.034C23.784,1.946 24.339,2.53 24.339,3.252V13.419L18.867,8.974C18.589,8.752 18.201,8.752 17.923,8.974L14.812,11.752L9.839,6.613C9.562,6.335 9.089,6.307 8.812,6.613L2.312,12.78V3.252C2.312,2.502 2.895,1.946 3.617,1.946ZM14.506,3.946C13.367,3.974 12.45,4.919 12.45,6.057C12.45,7.196 13.395,8.169 14.562,8.169C15.728,8.169 16.673,7.224 16.673,6.057C16.673,4.891 15.728,3.946 14.562,3.946C14.562,3.946 14.534,3.946 14.506,3.946ZM14.506,5.419C14.506,5.419 14.506,5.419 14.534,5.419C14.895,5.419 15.173,5.696 15.173,6.057C15.173,6.419 14.895,6.696 14.534,6.696C14.173,6.696 13.895,6.419 13.895,6.057C13.895,5.696 14.145,5.446 14.478,5.419H14.506ZM9.256,8.196L15.173,14.28C15.45,14.557 15.923,14.585 16.201,14.28C16.478,14.002 16.506,13.53 16.201,13.252L15.812,12.835L18.423,10.502L24.339,15.307V16.308C24.339,17.058 23.756,17.613 23.034,17.613H3.617C2.867,17.613 2.312,17.03 2.312,16.308V14.807L9.284,8.196H9.256Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
|
@ -15,4 +15,5 @@
|
||||||
<string name="scan_address_validation_failed">This QR code is not a valid Zcash Address.</string>
|
<string name="scan_address_validation_failed">This QR code is not a valid Zcash Address.</string>
|
||||||
|
|
||||||
<string name="scan_torch_content_description">Camera torch toggle</string>
|
<string name="scan_torch_content_description">Camera torch toggle</string>
|
||||||
|
<string name="gallery_content_description">Gallery</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue