Updated on 2026-08-14

This commit is contained in:
Tangem 2024-01-16 15:42:00 +03:00
parent 0b7e1f6bb5
commit 85e61772a1
8 changed files with 253 additions and 71 deletions

View file

@ -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)

View file

@ -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,
)
}
},
)
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M3.27,3L2,4.27L7,9.27V13H10V22L13.58,15.86L17.73,20L19,18.73L3.27,3ZM17,10H13L17,2H7V4.18L15.46,12.64L17,10Z"
android:fillColor="#1E1E1E"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M7,2V13H10V22L17,10H13L17,2H7Z"
android:fillColor="#1E1E1E"/>
</vector>

View file

@ -19,6 +19,7 @@ dependencies {
implementation(projects.core.navigation)
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.activity.compose)
/** Camera */
implementation(deps.camera.camera2)

View file

@ -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()
}
}
}

View file

@ -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())

View file

@ -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,
)
}
},
)
}
}