Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-14 15:39:54 +03:00
parent 70d2f5ee06
commit 93abd29e1a
32 changed files with 609 additions and 118 deletions

View file

@ -0,0 +1,29 @@
package com.tangem.core.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.extensions.toQrCode
import com.tangem.core.ui.res.TangemTheme
@Composable
fun rememberQrPainters(
content: List<String>,
size: Dp = TangemTheme.dimens.size248,
padding: Dp = TangemTheme.dimens.spacing0,
): List<BitmapPainter> {
val density = LocalDensity.current
return remember(content) {
content.map { code ->
BitmapPainter(
code.toQrCode(
sizePx = with(density) { size.roundToPx() },
paddingPx = with(density) { padding.roundToPx() },
).asImageBitmap(),
)
}
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.core.ui.components.bottomsheets
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import com.tangem.core.ui.res.TangemTheme
/**
* Tangem bottom sheet with custom draggable header and config
*
* @param config data model containing logic and ui models
* @param content custom bottom sheet content
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TangemBottomSheet(
config: TangemBottomSheetConfig,
content: @Composable ColumnScope.(TangemBottomSheetConfigContent) -> Unit,
) {
ModalBottomSheet(
onDismissRequest = config.onDismissRequest,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
containerColor = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.bottomSheetLarge,
dragHandle = { TangemBottomSheetDraggableHeader() },
) {
content(config.content)
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.core.ui.components.bottomsheets
/**
* Tangem bottom sheet config
*
* @property isShow flag that determine if bottom sheet is shown
* @property onDismissRequest lambda be invoked when bottom sheet is dismissed
* @property content content config
*/
data class TangemBottomSheetConfig(
val isShow: Boolean,
val onDismissRequest: () -> Unit,
val content: TangemBottomSheetConfigContent,
)

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.components.bottomsheets
/**
* General interface for bottom sheet config model
*/
interface TangemBottomSheetConfigContent

View file

@ -0,0 +1,33 @@
package com.tangem.core.ui.components.bottomsheets
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
@Composable
fun TangemBottomSheetDraggableHeader() {
Surface(
modifier = Modifier
.height(TangemTheme.dimens.size20),
color = TangemTheme.colors.background.primary,
) {
Box(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing8)
.size(
width = TangemTheme.dimens.size32,
height = TangemTheme.dimens.size4,
)
.background(
color = TangemTheme.colors.icon.inactive,
shape = TangemTheme.shapes.roundedCornersSmall,
),
)
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.core.ui.components.bottomsheets.tokenreceive
data class AddressModel(
val value: String,
val type: Type = Type.Default,
) {
enum class Type {
Legacy, Default
}
}

View file

@ -0,0 +1,192 @@
package com.tangem.core.ui.components.bottomsheets.tokenreceive
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import com.tangem.core.ui.R
import com.tangem.core.ui.components.MiddleEllipsisText
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.rememberQrPainters
import com.tangem.core.ui.extensions.shareText
import com.tangem.core.ui.res.TangemTheme
@Composable
fun TokenReceiveBottomSheet(config: TangemBottomSheetConfig) {
if (config.content is TokenReceiveBottomSheetConfig && config.isShow) {
TangemBottomSheet(config) { content ->
TokenReceiveBottomSheetContent(
content = content as TokenReceiveBottomSheetConfig,
)
}
}
}
@Composable
private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfig) {
var selectedAddress by remember { mutableStateOf(content.addresses.first()) }
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing24,
top = TangemTheme.dimens.spacing24,
end = TangemTheme.dimens.spacing24,
bottom = TangemTheme.dimens.spacing16,
),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing24),
) {
QrCodeContent(
content = content,
onAddressChange = { selectedAddress = it },
)
Text(
text = stringResource(R.string.receive_bottom_sheet_warning_message_full, content.name),
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption,
)
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
val clipboardManager = LocalClipboardManager.current
val hapticFeedback = LocalHapticFeedback.current
val context = LocalContext.current
SecondaryButtonIconStart(
modifier = Modifier.weight(1f),
text = stringResource(id = R.string.common_copy),
iconResId = R.drawable.ic_copy_24,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clipboardManager.setText(AnnotatedString(selectedAddress.value))
},
)
SecondaryButtonIconStart(
modifier = Modifier.weight(1f),
text = stringResource(id = R.string.common_share),
iconResId = R.drawable.ic_share_24,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
context.shareText(selectedAddress.value)
},
)
}
}
}
@Suppress("LongMethod")
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun QrCodeContent(content: TokenReceiveBottomSheetConfig, onAddressChange: (AddressModel) -> Unit) {
val qrCodes = rememberQrPainters(content.addresses.map(AddressModel::value))
val pagerState = rememberPagerState()
val pageCount = content.addresses.count()
LaunchedEffect(key1 = pagerState.currentPage) {
onAddressChange.invoke(content.addresses[pagerState.currentPage])
}
HorizontalPager(
pageCount = pageCount,
state = pagerState,
) { currentPage ->
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24),
) {
Text(
text = stringResource(
R.string.receive_bottom_sheet_warning_message,
getName(content = content, index = pagerState.currentPage),
content.symbol,
content.network,
),
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
style = TangemTheme.typography.h3,
)
Image(
painter = qrCodes[currentPage],
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.size(TangemTheme.dimens.size248),
)
MiddleEllipsisText(
text = content.addresses[currentPage].value,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
style = TangemTheme.typography.subtitle1,
)
}
}
if (pageCount > 1) {
val indicatorState = rememberLazyListState()
val selectedColor = TangemTheme.colors.icon.primary1
val unselectedColor = TangemTheme.colors.icon.informative
LazyRow(
modifier = Modifier
.height(TangemTheme.dimens.size20),
state = indicatorState,
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
repeat(pageCount) { iteration ->
item(key = iteration) {
val color = if (pagerState.currentPage == iteration) {
selectedColor
} else {
unselectedColor
}
Box(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing4,
top = TangemTheme.dimens.spacing6,
end = TangemTheme.dimens.spacing4,
bottom = TangemTheme.dimens.spacing6,
)
.background(color, CircleShape)
.size(TangemTheme.dimens.size7),
)
}
}
}
}
}
@Composable
private fun getName(content: TokenReceiveBottomSheetConfig, index: Int): String {
return if (content.addresses.size < 2) {
content.name
} else {
"${
stringResource(
id = when (content.addresses[index].type) {
AddressModel.Type.Default -> R.string.address_type_default
AddressModel.Type.Legacy -> R.string.address_type_legacy
},
)
} ${content.name}"
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.core.ui.components.bottomsheets.tokenreceive
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
class TokenReceiveBottomSheetConfig(
val name: String,
val symbol: String,
val network: String,
val addresses: List<AddressModel>,
) : TangemBottomSheetConfigContent

View file

@ -0,0 +1,15 @@
package com.tangem.core.ui.extensions
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
fun Context.shareText(text: String) {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
ContextCompat.startActivity(this, shareIntent, null)
}

View file

@ -0,0 +1,32 @@
package com.tangem.core.ui.extensions
import android.graphics.Bitmap
import android.graphics.Color
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import java.util.Hashtable
@Suppress("MagicNumber")
fun String.toQrCode(sizePx: Int = 256, paddingPx: Int = 0): Bitmap {
val hintMap = Hashtable<EncodeHintType, Any>()
hintMap[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M // H = 30% damage
hintMap[EncodeHintType.MARGIN] = paddingPx
val qrCodeWriter = QRCodeWriter()
val bitMatrix = qrCodeWriter.encode(this, BarcodeFormat.QR_CODE, sizePx, sizePx, hintMap)
val width = bitMatrix.width
val bmp = Bitmap.createBitmap(width, width, Bitmap.Config.RGB_565)
for (x in 0 until width) {
for (y in 0 until width) {
bmp.setPixel(
y,
x,
if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE,
)
}
}
return bmp
}

View file

@ -81,6 +81,7 @@ data class TangemDimens internal constructor(
val size158: Dp = 158.dp,
val size164: Dp = 164.dp,
val size200: Dp = 200.dp,
val size248: Dp = 248.dp,
// endregion Size
// region Spacing
val spacing0: Dp = 0.dp,

View file

@ -13,6 +13,7 @@ data class TangemShapes internal constructor(
val roundedCornersXMedium: Shape,
val roundedCornersLarge: Shape,
val bottomSheet: Shape,
val bottomSheetLarge: Shape,
) {
constructor(dimens: TangemDimens) : this(
roundedCornersSmall = RoundedCornerShape(size = dimens.radius2),
@ -25,5 +26,9 @@ data class TangemShapes internal constructor(
topStart = dimens.radius16,
topEnd = dimens.radius16,
),
bottomSheetLarge = RoundedCornerShape(
topStart = dimens.radius24,
topEnd = dimens.radius24,
),
)
}