From 85e61772a111e3aa8ef15a956adbe4160def8d9e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Jan 2024 15:42:00 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../appbar/AppBarWithBackButtonAndIcon.kt | 82 ++++------- .../AppBarWithBackButtonAndIconContent.kt | 131 ++++++++++++++++++ .../src/main/res/drawable/ic_flash_off_24.xml | 9 ++ .../src/main/res/drawable/ic_flash_on_24.xml | 9 ++ features/qr-scanning/impl/build.gradle.kts | 1 + .../qrscanning/inner/MLKitBarcodeAnalyzer.kt | 12 +- .../qrscanning/presentation/CameraView.kt | 7 +- .../presentation/QrScanningContent.kt | 73 ++++++++-- 8 files changed, 253 insertions(+), 71 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt create mode 100644 core/ui/src/main/res/drawable/ic_flash_off_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_flash_on_24.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt index 6151499106..c9e0526b4e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt @@ -2,16 +2,10 @@ package com.tangem.core.ui.components.appbar import androidx.annotation.DrawableRes import androidx.compose.animation.* -import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.size import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource @@ -29,57 +23,31 @@ fun AppBarWithBackButtonAndIcon( onIconClick: (() -> Unit)? = null, backgroundColor: Color = TangemTheme.colors.background.secondary, ) { - Row( - modifier = modifier - .background(color = backgroundColor) - .fillMaxWidth() - .padding(all = TangemTheme.dimens.spacing16), - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(backIconRes ?: R.drawable.ic_back_24), - contentDescription = null, - modifier = Modifier - .size(size = TangemTheme.dimens.size24) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), - ) { onBackClick() }, - tint = TangemTheme.colors.icon.primary1, - ) - AnimatedContent( - targetState = text, - modifier = Modifier.weight(1f), - transitionSpec = { fadeIn().togetherWith(fadeOut()) }, - label = "Toolbar title change", - ) { - if (!it.isNullOrBlank()) { - Text( - text = it, - color = TangemTheme.colors.text.primary1, - maxLines = 1, - style = TangemTheme.typography.subtitle1, - ) + AppBarWithBackButtonAndIconContent( + onBackClick = onBackClick, + modifier = modifier, + text = text, + backIconRes = backIconRes, + backgroundColor = backgroundColor, + iconContent = { + AnimatedContent( + targetState = iconRes, + transitionSpec = { (fadeIn() + scaleIn()).togetherWith(fadeOut() + scaleOut()) }, + label = "Toolbar icon change", + ) { + if (onIconClick != null && it != null) { + Icon( + painter = painterResource(it), + contentDescription = null, + modifier = Modifier + .size(size = TangemTheme.dimens.size24) + .clickable { onIconClick() }, + tint = TangemTheme.colors.icon.primary1, + ) + } } - } - AnimatedContent( - targetState = iconRes, - transitionSpec = { (fadeIn() + scaleIn()).togetherWith(fadeOut() + scaleOut()) }, - label = "Toolbar icon change", - ) { - if (onIconClick != null && it != null) { - Icon( - painter = painterResource(it), - contentDescription = null, - modifier = Modifier - .size(size = TangemTheme.dimens.size24) - .clickable { onIconClick() }, - tint = TangemTheme.colors.icon.primary1, - ) - } - } - } + }, + ) } @Preview(widthDp = 360, heightDp = 56, showBackground = true) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt new file mode 100644 index 0000000000..6402068907 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt @@ -0,0 +1,131 @@ +package com.tangem.core.ui.components.appbar + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme + +/** + * App bar with back button and icon content for any possible number/style of icons + * + * @param onBackClick callback for back button + * @param modifier modifier + * @param text appbar title + * @param backIconRes icon for back button + * @param backgroundColor background color + * @param iconContent icon content + */ +@Composable +fun AppBarWithBackButtonAndIconContent( + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + text: String? = null, + @DrawableRes backIconRes: Int? = null, + backIconTint: Color = TangemTheme.colors.icon.primary1, + backgroundColor: Color = TangemTheme.colors.background.secondary, + iconContent: @Composable () -> Unit, +) { + Row( + modifier = modifier + .background(color = backgroundColor) + .fillMaxWidth() + .padding(all = TangemTheme.dimens.spacing16), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(backIconRes ?: R.drawable.ic_back_24), + contentDescription = null, + modifier = Modifier + .size(size = TangemTheme.dimens.size24) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + ) { onBackClick() }, + tint = backIconTint, + ) + AnimatedContent( + targetState = text, + modifier = Modifier.weight(1f), + transitionSpec = { fadeIn().togetherWith(fadeOut()) }, + label = "Toolbar title change", + ) { + if (!it.isNullOrBlank()) { + Text( + text = it, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + style = TangemTheme.typography.subtitle1, + ) + } + } + iconContent() + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +private fun PreviewAppBarWithBackButtonAndIconInLightTheme() { + TangemTheme(isDark = false) { + AppBarWithBackButtonAndIconContent( + text = "Title", + onBackClick = {}, + iconContent = { + Row { + Icon( + painter = painterResource(id = R.drawable.ic_qrcode_scan_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + Icon( + painter = painterResource(id = R.drawable.ic_flash_on_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + } + }, + ) + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +private fun PreviewAppBarWithBackButtonAndIconInDarkTheme() { + TangemTheme(isDark = true) { + AppBarWithBackButtonAndIconContent( + text = "Title", + onBackClick = {}, + iconContent = { + Row { + Icon( + painter = painterResource(id = R.drawable.ic_qrcode_scan_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + Icon( + painter = painterResource(id = R.drawable.ic_flash_on_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + } + }, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_flash_off_24.xml b/core/ui/src/main/res/drawable/ic_flash_off_24.xml new file mode 100644 index 0000000000..f7dae29762 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_flash_off_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_flash_on_24.xml b/core/ui/src/main/res/drawable/ic_flash_on_24.xml new file mode 100644 index 0000000000..32fd52a0e2 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_flash_on_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/qr-scanning/impl/build.gradle.kts b/features/qr-scanning/impl/build.gradle.kts index 1d6c8db728..712e2a79ce 100644 --- a/features/qr-scanning/impl/build.gradle.kts +++ b/features/qr-scanning/impl/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.core.navigation) implementation(deps.androidx.fragment.ktx) + implementation(deps.androidx.activity.compose) /** Camera */ implementation(deps.camera.camera2) diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/inner/MLKitBarcodeAnalyzer.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/inner/MLKitBarcodeAnalyzer.kt index 9a339e2d92..17cf43ec3d 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/inner/MLKitBarcodeAnalyzer.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/inner/MLKitBarcodeAnalyzer.kt @@ -16,9 +16,14 @@ class MLKitBarcodeAnalyzer(private val onScanned: (String) -> Unit) : ImageAnaly @ExperimentalGetImage override fun analyze(imageProxy: ImageProxy) { val mediaImage = imageProxy.image - if (mediaImage != null && !isScanning) { + if (mediaImage != null) { val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) + analyze(image, imageProxy::close) + } + } + fun analyze(image: InputImage, onClose: (() -> Unit)? = null) { + if (!isScanning) { isScanning = true scanner.process(image) .addOnSuccessListener { codes -> @@ -28,13 +33,12 @@ class MLKitBarcodeAnalyzer(private val onScanned: (String) -> Unit) : ImageAnaly onScanned.invoke(it) } } - isScanning = false - imageProxy.close() + onClose?.invoke() } .addOnFailureListener { isScanning = false - imageProxy.close() + onClose?.invoke() } } } diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/CameraView.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/CameraView.kt index d3526d4e5a..09760ea974 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/CameraView.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/CameraView.kt @@ -6,6 +6,7 @@ import androidx.camera.view.LifecycleCameraController import androidx.camera.view.PreviewView import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -15,7 +16,7 @@ import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer import java.util.concurrent.ExecutorService @Composable -internal fun CameraView(executor: () -> ExecutorService, analyzer: () -> MLKitBarcodeAnalyzer) { +internal fun CameraView(executor: () -> ExecutorService, analyzer: () -> MLKitBarcodeAnalyzer, isFlash: Boolean) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current @@ -29,6 +30,10 @@ internal fun CameraView(executor: () -> ExecutorService, analyzer: () -> MLKitBa imageAnalysisBackpressureStrategy = ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST } + LaunchedEffect(key1 = isFlash) { + cameraController.cameraControl?.enableTorch(isFlash) + } + previewView.controller = cameraController AndroidView({ previewView }, modifier = Modifier.fillMaxSize()) diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt index 0a7ef5a8ec..8d169703a6 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt @@ -1,10 +1,16 @@ package com.tangem.feature.qrscanning.presentation +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.* import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.runtime.Composable +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size @@ -12,12 +18,15 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Constraints -import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon +import com.google.mlkit.vision.common.InputImage +import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIconContent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemColorPalette @@ -35,6 +44,16 @@ internal fun QrScanningContent( analyzer: () -> MLKitBarcodeAnalyzer, uiState: QrScanningState, ) { + val context = LocalContext.current + var isFlash by remember { mutableStateOf(false) } + val galleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { + val selectedImage = it ?: Uri.EMPTY + if (selectedImage != Uri.EMPTY) { + val image = InputImage.fromFilePath(context, selectedImage) + analyzer().analyze(image) + } + } + Box( modifier = Modifier .fillMaxSize(), @@ -42,16 +61,52 @@ internal fun QrScanningContent( CameraView( executor = executor, analyzer = analyzer, + isFlash = isFlash, ) Overlay( message = uiState.message, ) - AppBarWithBackButtonAndIcon( + AppBarWithBackButtonAndIconContent( onBackClick = uiState.onBackClick, - iconRes = R.drawable.ic_gallery_24, - onIconClick = uiState.onGalleryClicked, - backgroundColor = Color.Transparent, modifier = Modifier.statusBarsPadding(), + backgroundColor = Color.Transparent, + backIconTint = TangemColorPalette.White, + iconContent = { + Row { + AnimatedContent( + targetState = isFlash, + transitionSpec = { fadeIn().togetherWith(fadeOut()) }, + label = "Flash Change", + ) { + val flashIconRes = if (it) R.drawable.ic_flash_on_24 else R.drawable.ic_flash_off_24 + Icon( + painter = painterResource(flashIconRes), + contentDescription = null, + modifier = Modifier + .padding(end = TangemTheme.dimens.spacing20) + .size(size = TangemTheme.dimens.size24) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + onClick = { isFlash = !isFlash }, + ), + tint = TangemColorPalette.White, + ) + } + Icon( + painter = painterResource(R.drawable.ic_gallery_24), + contentDescription = null, + modifier = Modifier + .size(size = TangemTheme.dimens.size24) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + onClick = { galleryLauncher.launch("image/*") }, + ), + tint = TangemColorPalette.White, + ) + } + }, ) } }