Updated on 2026-08-14
This commit is contained in:
parent
70d2f5ee06
commit
93abd29e1a
32 changed files with 609 additions and 118 deletions
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.ui.components.bottomsheets
|
||||
|
||||
/**
|
||||
* General interface for bottom sheet config model
|
||||
*/
|
||||
interface TangemBottomSheetConfigContent
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue