Updated on 2026-08-14
This commit is contained in:
parent
2a689bbbe4
commit
ee04820594
24 changed files with 384 additions and 268 deletions
9
core/ui/src/main/res/drawable/ic_gallery_24.xml
Normal file
9
core/ui/src/main/res/drawable/ic_gallery_24.xml
Normal 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="M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5C4.47,3 3.961,3.211 3.586,3.586C3.211,3.961 3,4.47 3,5V19C3,19.53 3.211,20.039 3.586,20.414C3.961,20.789 4.47,21 5,21H19C19.53,21 20.039,20.789 20.414,20.414C20.789,20.039 21,19.53 21,19Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
|
|
@ -8,5 +8,6 @@ interface QrScanningRouter {
|
|||
|
||||
companion object {
|
||||
const val SOURCE_KEY = "source"
|
||||
const val NETWORK_KEY = "network"
|
||||
}
|
||||
}
|
||||
|
|
@ -8,17 +8,17 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "com.tangem.feature.qrscanning.impl"
|
||||
|
||||
viewBinding {
|
||||
enable = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Core */
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.navigation)
|
||||
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
implementation(deps.viewBindingDelegate)
|
||||
|
||||
/** Camera */
|
||||
implementation(deps.camera.camera2)
|
||||
|
|
@ -42,6 +42,12 @@ dependencies {
|
|||
|
||||
/** Compose */
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
|
||||
/** Other dependencies */
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.timber)
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.feature.qrscanning
|
||||
|
||||
const val SCAN_QR_REQUEST_CODE = 1001
|
||||
const val SCAN_RESULT = "scanResult"
|
||||
const val PERMISSION_REQUEST_CODE = 16
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package com.tangem.feature.qrscanning
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
internal class DefaultQrScanningRouter : QrScanningRouter {
|
||||
|
||||
override fun getEntryFragment(): Fragment = QrScanningFragment.create()
|
||||
}
|
||||
|
|
@ -3,98 +3,86 @@ package com.tangem.feature.qrscanning
|
|||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.tangem.feature.qrscanning.impl.R
|
||||
import com.tangem.feature.qrscanning.impl.databinding.LayoutQrScanningBinding
|
||||
import com.tangem.feature.qrscanning.inner.PreviewBinder
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer
|
||||
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningContent
|
||||
import com.tangem.feature.qrscanning.viewmodel.QrScanningViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
// import com.tangem.core.navigation.NavigationAction
|
||||
// import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
// import com.tangem.tap.features.details.ui.walletconnect.dialogs.PreviewBinder
|
||||
// import com.tangem.tap.store
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class QrScanningFragment : Fragment(R.layout.layout_qr_scanning) {
|
||||
internal class QrScanningFragment : ComposeFragment() {
|
||||
|
||||
private val binding: LayoutQrScanningBinding by viewBinding(LayoutQrScanningBinding::bind)
|
||||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
private val binder = PreviewBinder()
|
||||
@Inject
|
||||
lateinit var router: QrScanningRouter
|
||||
|
||||
private val innerRouter: QrScanningInnerRouter
|
||||
get() = requireNotNull(router as? QrScanningInnerRouter) {
|
||||
"innerRouter should be instance of QrScanningInnerRouter"
|
||||
}
|
||||
|
||||
private val viewModel by viewModels<QrScanningViewModel>()
|
||||
|
||||
private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null
|
||||
private var cameraExecutor: ExecutorService? = null
|
||||
private var cameraExecutor: ExecutorService by Delegates.notNull()
|
||||
|
||||
private val cameraPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||
if (!it) parentFragmentManager.popBackStack()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setFitSystemWindows(fit = true)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
if (!permissionIsGranted()) requestPermission()
|
||||
|
||||
cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
|
||||
viewModel.router = innerRouter
|
||||
cameraExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
cameraProviderFuture?.addListener(
|
||||
{
|
||||
val cameraProvider = cameraProviderFuture?.get()
|
||||
binder.bindPreview(
|
||||
context = requireContext(),
|
||||
binding = binding,
|
||||
lifecycleOwner = this,
|
||||
cameraProvider = requireNotNull(cameraProvider),
|
||||
cameraExecutor = requireNotNull(cameraExecutor),
|
||||
onScanned = { result ->
|
||||
parentFragmentManager.popBackStack()
|
||||
setFitSystemWindows(fit = false)
|
||||
if (result.isNotBlank()) {
|
||||
viewModel.onQrScanned(result)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
ContextCompat.getMainExecutor(requireContext()),
|
||||
)
|
||||
|
||||
binding.overlay.post {
|
||||
binding.overlay.setViewFinder()
|
||||
}
|
||||
requestCameraPermission()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
setFitSystemWindows(fit = true)
|
||||
cameraPermissionLauncher.unregister()
|
||||
cameraExecutor.shutdown()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
SystemBarsEffect {
|
||||
setSystemBarsColor(Color.Transparent, darkIcons = false)
|
||||
}
|
||||
QrScanningContent(
|
||||
executor = { cameraExecutor },
|
||||
analyzer = { MLKitBarcodeAnalyzer(viewModel::onQrScanned) },
|
||||
uiState = viewModel.uiState,
|
||||
)
|
||||
setFitSystemWindows(fit = false)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
if (requestCode != PERMISSION_REQUEST_CODE) return
|
||||
|
||||
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
parentFragmentManager.popBackStack()
|
||||
private fun requestCameraPermission() {
|
||||
if (
|
||||
ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.CAMERA,
|
||||
) == PackageManager.PERMISSION_DENIED
|
||||
) {
|
||||
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||
}
|
||||
}
|
||||
|
||||
private fun permissionIsGranted(): Boolean {
|
||||
val cameraPermission = ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
|
||||
return cameraPermission == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
private fun requestPermission() {
|
||||
requestPermissions(arrayOf(Manifest.permission.CAMERA), PERMISSION_REQUEST_CODE)
|
||||
}
|
||||
|
||||
private fun setFitSystemWindows(fit: Boolean) {
|
||||
activity?.window?.let {
|
||||
WindowCompat.setDecorFitsSystemWindows(it, fit)
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
package com.tangem.feature.qrscanning
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
|
||||
@Module
|
||||
@InstallIn(ActivityComponent::class)
|
||||
internal object QrScanningRouterModule {
|
||||
|
||||
@Provides
|
||||
@ActivityScoped
|
||||
fun provideQrScanRouter(): QrScanningRouter {
|
||||
return DefaultQrScanningRouter()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package com.tangem.feature.qrscanning
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter.Companion.SOURCE_KEY
|
||||
import com.tangem.feature.qrscanning.usecase.EmitQrScannedEventUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class QrScanningViewModel @Inject constructor(
|
||||
private val emitQrScannedEventUseCase: EmitQrScannedEventUseCase,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel() {
|
||||
|
||||
private val source: SourceType = savedStateHandle[SOURCE_KEY] ?: error("Source is mandatory")
|
||||
|
||||
fun onQrScanned(qrCode: String) {
|
||||
viewModelScope.launch {
|
||||
emitQrScannedEventUseCase.invoke(source, qrCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.feature.qrscanning
|
||||
package com.tangem.feature.qrscanning.di
|
||||
|
||||
import com.tangem.feature.qrscanning.repo.DefaultQrScanningEventsRepository
|
||||
import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.feature.qrscanning.di
|
||||
|
||||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.feature.qrscanning.navigation.DefaultQrScanningRouter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
|
||||
@Module
|
||||
@InstallIn(ActivityComponent::class)
|
||||
internal object QrScanningRouterModule {
|
||||
|
||||
@Provides
|
||||
@ActivityScoped
|
||||
fun provideQrScanRouter(reduxNavController: ReduxNavController): QrScanningRouter {
|
||||
return DefaultQrScanningRouter(reduxNavController)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,9 @@ import androidx.camera.core.ImageAnalysis
|
|||
import androidx.camera.core.ImageProxy
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
import javax.annotation.concurrent.Immutable
|
||||
|
||||
@Immutable
|
||||
class MLKitBarcodeAnalyzer(private val onScanned: (String) -> Unit) : ImageAnalysis.Analyzer {
|
||||
|
||||
private var isScanning: Boolean = false
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
package com.tangem.feature.qrscanning.inner
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Size
|
||||
import android.view.OrientationEventListener
|
||||
import android.view.Surface
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.tangem.feature.qrscanning.impl.databinding.LayoutQrScanningBinding
|
||||
import java.util.concurrent.ExecutorService
|
||||
|
||||
internal class PreviewBinder {
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun bindPreview(
|
||||
context: Context,
|
||||
binding: LayoutQrScanningBinding,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
cameraProvider: ProcessCameraProvider,
|
||||
cameraExecutor: ExecutorService,
|
||||
onScanned: (String) -> Unit,
|
||||
) {
|
||||
cameraProvider.unbindAll()
|
||||
|
||||
val preview: Preview = Preview.Builder()
|
||||
.build()
|
||||
|
||||
val imageAnalysis = ImageAnalysis.Builder()
|
||||
.setTargetResolution(Size(binding.cameraPreview.width, binding.cameraPreview.height))
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
|
||||
val orientationEventListener = object : OrientationEventListener(context) {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override fun onOrientationChanged(orientation: Int) {
|
||||
val rotation: Int = when (orientation) {
|
||||
in 45..134 -> Surface.ROTATION_270
|
||||
in 135..224 -> Surface.ROTATION_180
|
||||
in 225..314 -> Surface.ROTATION_90
|
||||
else -> Surface.ROTATION_0
|
||||
}
|
||||
|
||||
imageAnalysis.targetRotation = rotation
|
||||
}
|
||||
}
|
||||
orientationEventListener.enable()
|
||||
|
||||
val analyzer: ImageAnalysis.Analyzer = MLKitBarcodeAnalyzer {
|
||||
imageAnalysis.clearAnalyzer()
|
||||
onScanned.invoke(it)
|
||||
}
|
||||
|
||||
cameraExecutor.let {
|
||||
imageAnalysis.setAnalyzer(it, analyzer)
|
||||
}
|
||||
|
||||
preview.setSurfaceProvider(binding.cameraPreview.surfaceProvider)
|
||||
|
||||
val cameraSelector: CameraSelector = CameraSelector.Builder()
|
||||
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
||||
.build()
|
||||
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, imageAnalysis, preview)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package com.tangem.feature.qrscanning.inner
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.tangem.feature.qrscanning.impl.R
|
||||
|
||||
class ViewFinderOverlay(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
||||
|
||||
private val boxPaint: Paint = Paint().apply {
|
||||
color = ContextCompat.getColor(context, R.color.white)
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = context.resources.getDimensionPixelOffset(R.dimen.qr_border_stroke_width).toFloat()
|
||||
}
|
||||
|
||||
private val boxWidthRatio = 0.8F
|
||||
private val boxCornerRadius: Float =
|
||||
context.resources.getDimensionPixelOffset(R.dimen.qr_border_corner_radius).toFloat()
|
||||
|
||||
private var boxRect: RectF? = null
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun setViewFinder() {
|
||||
val overlayWidth = width.toFloat()
|
||||
val overlayHeight = height.toFloat()
|
||||
val boxSize = overlayWidth * boxWidthRatio
|
||||
val cx = overlayWidth / 2
|
||||
val cy = overlayHeight / 2
|
||||
boxRect = RectF(cx - boxSize / 2, cy - boxSize / 2, cx + boxSize / 2, cy + boxSize / 2)
|
||||
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
super.draw(canvas)
|
||||
boxRect?.let {
|
||||
canvas.drawRoundRect(it, boxCornerRadius, boxCornerRadius, boxPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.feature.qrscanning.navigation
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.feature.qrscanning.QrScanningFragment
|
||||
|
||||
class DefaultQrScanningRouter(
|
||||
private val reduxNavController: ReduxNavController,
|
||||
) : QrScanningInnerRouter {
|
||||
override fun getEntryFragment(): Fragment = QrScanningFragment.create()
|
||||
|
||||
override fun popBackStack() {
|
||||
reduxNavController.popBackStack()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.feature.qrscanning.navigation
|
||||
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
|
||||
interface QrScanningInnerRouter : QrScanningRouter {
|
||||
|
||||
fun popBackStack()
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.feature.qrscanning.presentation
|
||||
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
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.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer
|
||||
import java.util.concurrent.ExecutorService
|
||||
|
||||
@Composable
|
||||
internal fun CameraView(executor: () -> ExecutorService, analyzer: () -> MLKitBarcodeAnalyzer) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
val previewView = remember { PreviewView(context) }
|
||||
val cameraController = remember { LifecycleCameraController(context) }
|
||||
|
||||
cameraController.apply {
|
||||
bindToLifecycle(lifecycleOwner)
|
||||
cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||
setImageAnalysisAnalyzer(executor(), analyzer())
|
||||
imageAnalysisBackpressureStrategy = ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
|
||||
}
|
||||
|
||||
previewView.controller = cameraController
|
||||
|
||||
AndroidView({ previewView }, modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package com.tangem.feature.qrscanning.presentation
|
||||
|
||||
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.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
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.LocalDensity
|
||||
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.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.qrscanning.impl.R
|
||||
import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer
|
||||
import java.util.concurrent.ExecutorService
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val QR_CODE_SIZE = 0.8f
|
||||
|
||||
@Composable
|
||||
internal fun QrScanningContent(
|
||||
executor: () -> ExecutorService,
|
||||
analyzer: () -> MLKitBarcodeAnalyzer,
|
||||
uiState: QrScanningState,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
CameraView(
|
||||
executor = executor,
|
||||
analyzer = analyzer,
|
||||
)
|
||||
Overlay(
|
||||
message = uiState.message,
|
||||
)
|
||||
AppBarWithBackButtonAndIcon(
|
||||
onBackClick = uiState.onBackClick,
|
||||
iconRes = R.drawable.ic_gallery_24,
|
||||
onIconClick = uiState.onGalleryClicked,
|
||||
backgroundColor = Color.Transparent,
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Overlay(message: TextReference?, overlayColor: Color = TangemColorPalette.Black.copy(alpha = 0.7f)) {
|
||||
val whiteColor = TangemTheme.colors.text.constantWhite
|
||||
val borderWidth = with(LocalDensity.current) { TangemTheme.dimens.spacing4.toPx() }
|
||||
val borderLength = with(LocalDensity.current) { TangemTheme.dimens.size20.toPx() }
|
||||
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
val textPadding = with(LocalDensity.current) { TangemTheme.dimens.spacing24.toPx() }
|
||||
val style = TangemTheme.typography.caption2.copy(textAlign = TextAlign.Center)
|
||||
val text = message?.resolveReference()
|
||||
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val squareSize = size.minDimension * QR_CODE_SIZE
|
||||
val verticalOffset = (size.height - squareSize) / 2f
|
||||
val horizontalOffset = (size.width - squareSize) / 2f
|
||||
drawRect(
|
||||
color = overlayColor,
|
||||
topLeft = Offset.Zero,
|
||||
size = Size(size.width, verticalOffset),
|
||||
)
|
||||
drawRect(
|
||||
color = overlayColor,
|
||||
topLeft = Offset(0f, verticalOffset),
|
||||
size = Size(horizontalOffset, squareSize),
|
||||
)
|
||||
drawRect(
|
||||
color = overlayColor,
|
||||
topLeft = Offset(horizontalOffset + squareSize, verticalOffset),
|
||||
size = Size(horizontalOffset, squareSize),
|
||||
)
|
||||
drawRect(
|
||||
color = overlayColor,
|
||||
topLeft = Offset(0f, verticalOffset + squareSize),
|
||||
size = Size(size.width, verticalOffset),
|
||||
)
|
||||
drawRect(
|
||||
color = whiteColor,
|
||||
topLeft = Offset(horizontalOffset, verticalOffset),
|
||||
size = Size(squareSize, squareSize),
|
||||
style = Stroke(
|
||||
width = borderWidth,
|
||||
join = StrokeJoin.Round,
|
||||
pathEffect = PathEffect.dashPathEffect(
|
||||
intervals = floatArrayOf(2 * borderLength, squareSize - 2 * borderLength),
|
||||
phase = borderLength,
|
||||
),
|
||||
),
|
||||
)
|
||||
text?.let {
|
||||
val textLayoutResult = text.let {
|
||||
textMeasurer.measure(
|
||||
text = it,
|
||||
style = style,
|
||||
constraints = Constraints.fixedWidth(squareSize.roundToInt()),
|
||||
)
|
||||
}
|
||||
drawText(
|
||||
textLayoutResult = textLayoutResult,
|
||||
color = whiteColor,
|
||||
topLeft = Offset(
|
||||
x = horizontalOffset,
|
||||
y = verticalOffset + squareSize + textPadding,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.feature.qrscanning.presentation
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
data class QrScanningState(
|
||||
val message: TextReference?,
|
||||
val onQrScanned: (String) -> Unit,
|
||||
val onBackClick: () -> Unit,
|
||||
val onGalleryClicked: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.feature.qrscanning.presentation
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.feature.qrscanning.viewmodel.QrScanningClickIntents
|
||||
import com.tangem.feature.qrscanning.SourceType
|
||||
import com.tangem.feature.qrscanning.impl.R
|
||||
|
||||
internal class QrScanningStateFactory(
|
||||
val clickIntents: QrScanningClickIntents,
|
||||
) {
|
||||
|
||||
fun getInitialState(source: SourceType, network: String?): QrScanningState {
|
||||
val message = when (source) {
|
||||
SourceType.SEND -> network?.let { resourceReference(R.string.send_qrcode_scan_info, wrappedList(it)) }
|
||||
else -> null
|
||||
}
|
||||
|
||||
return QrScanningState(
|
||||
message = message,
|
||||
onBackClick = clickIntents::onBackClick,
|
||||
onQrScanned = clickIntents::onQrScanned,
|
||||
onGalleryClicked = clickIntents::onGalleryClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.feature.qrscanning.viewmodel
|
||||
|
||||
interface QrScanningClickIntents {
|
||||
|
||||
fun onBackClick()
|
||||
|
||||
fun onQrScanned(qrCode: String)
|
||||
|
||||
fun onGalleryClicked()
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.feature.qrscanning.viewmodel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter.Companion.NETWORK_KEY
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter.Companion.SOURCE_KEY
|
||||
import com.tangem.feature.qrscanning.SourceType
|
||||
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningState
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningStateFactory
|
||||
import com.tangem.feature.qrscanning.usecase.EmitQrScannedEventUseCase
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@HiltViewModel
|
||||
internal class QrScanningViewModel @Inject constructor(
|
||||
private val emitQrScannedEventUseCase: EmitQrScannedEventUseCase,
|
||||
private val dispatcher: CoroutineDispatcherProvider,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), QrScanningClickIntents {
|
||||
|
||||
private val source: SourceType = savedStateHandle[SOURCE_KEY] ?: error("Source is mandatory")
|
||||
private val network: String? = savedStateHandle[NETWORK_KEY]
|
||||
|
||||
private val factory = QrScanningStateFactory(
|
||||
clickIntents = this,
|
||||
)
|
||||
|
||||
var router: QrScanningInnerRouter by Delegates.notNull()
|
||||
|
||||
var uiState: QrScanningState by mutableStateOf(factory.getInitialState(source, network))
|
||||
private set
|
||||
|
||||
override fun onBackClick() = router.popBackStack()
|
||||
|
||||
override fun onQrScanned(qrCode: String) {
|
||||
if (qrCode.isNotBlank()) {
|
||||
router.popBackStack()
|
||||
viewModelScope.launch(dispatcher.main) {
|
||||
emitQrScannedEventUseCase.invoke(source, qrCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGalleryClicked() {
|
||||
// [REDACTED_JIRA]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="false"
|
||||
tools:background="@android:color/white">
|
||||
|
||||
<androidx.camera.view.PreviewView
|
||||
android:id="@+id/cameraPreview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.tangem.feature.qrscanning.inner.ViewFinderOverlay
|
||||
android:id="@+id/overlay"
|
||||
android:layerType="software"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</FrameLayout>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<dimen name="qr_border_stroke_width">4dp</dimen>
|
||||
<dimen name="qr_border_corner_radius">8dp</dimen>
|
||||
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue