Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-16 12:09:24 +03:00
parent 70419c49a2
commit 7f6f605186
38 changed files with 1217 additions and 214 deletions

View file

@ -55,7 +55,7 @@
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/SplashTheme"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustNothing">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View file

@ -43,7 +43,7 @@ import kotlinx.coroutines.launch
internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestContent, modifier: Modifier = Modifier) {
val coroutineScope = rememberCoroutineScope()
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed),
bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed, LocalDensity.current),
)
BackHandler(

View file

@ -6,7 +6,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -166,7 +165,7 @@ private fun AmountItem(
.clickable(
enabled = onChangeApproveType != null,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
indication = ripple(),
onClick = { isExpandSelector = true },
),
) {

View file

@ -104,13 +104,13 @@ data class TokenMarketInfoResponse(
data class Links(
@Json(name = "official_links")
val officialLinks: List<Link>?,
val officialLinks: List<Link>? = null,
@Json(name = "social")
val social: List<Link>?,
val social: List<Link>? = null,
@Json(name = "repository")
val repository: List<Link>?,
val repository: List<Link>? = null,
@Json(name = "blockchain_site")
val blockchainSite: List<Link>?,
val blockchainSite: List<Link>? = null,
)
data class Link(

View file

@ -117,6 +117,7 @@
<string name="common_import">Importe</string>
<string name="common_in_progress">En progreso</string>
<string name="common_later">Más tarde</string>
<string name="common_left">%1$s quedan</string>
<string name="common_locked">Bloqueado</string>
<string name="common_main_network">Red principal</string>
<string name="common_network_fee_title">Tarifa de la red</string>
@ -295,6 +296,7 @@
<string name="give_permission_unlimited">Ilimitado</string>
<string name="home_button_order">Ordenar </string>
<string name="home_button_scan">Escanee</string>
<string name="information_generated_with_ai">Esta información fue generada con IA</string>
<string name="initial_message_change_access_code_body">Toque para cambiar la contraseña</string>
<string name="initial_message_change_passcode_body">Toque para cambiar la contraseña</string>
<string name="initial_message_create_wallet_body">Para crear la billetera, toque la tarjeta como se muestra arriba y no la retire hasta el final de la operación</string>
@ -348,6 +350,7 @@
<string name="markets_common_my_portfolio">Mi portafolio</string>
<string name="markets_common_title">Mercado</string>
<string name="markets_generate_addresses_notification">Para generar direcciones para las redes seleccionadas, debe escanear su tarjeta Tangem</string>
<string name="markets_hint">Para agregar tokens, abra esta página o toque la barra de búsqueda</string>
<string name="markets_insights_info_description_message">Los datos de este apartado proceden de las siguientes redes: %s</string>
<string name="markets_loading_error_title">No se pueden cargar los datos…</string>
<string name="markets_loading_no_data_title">Sin datos</string>
@ -358,12 +361,12 @@
<string name="markets_search_token_no_result_title">Sin resultado</string>
<string name="markets_select_network">Seleccione una red</string>
<string name="markets_select_wallet">Seleccione una billetera</string>
<string name="markets_selector_interval_1m_title">1 mes</string>
<string name="markets_selector_interval_1m_title">1 mo</string>
<string name="markets_selector_interval_1y_title">1 año</string>
<string name="markets_selector_interval_24h_title">24 horas</string>
<string name="markets_selector_interval_3m_title">3 meses</string>
<string name="markets_selector_interval_6m_title">6 meses</string>
<string name="markets_selector_interval_7d_title">7 días</string>
<string name="markets_selector_interval_24h_title">24h</string>
<string name="markets_selector_interval_3m_title">3mos</string>
<string name="markets_selector_interval_6m_title">6mos</string>
<string name="markets_selector_interval_7d_title">7d</string>
<string name="markets_selector_interval_all_title">Todo</string>
<string name="markets_sort_by_experienced_buyers_title">Compradores experimentados</string>
<string name="markets_sort_by_rating_title">Evaluación</string>
@ -395,7 +398,7 @@
<string name="markets_token_details_liquidity_description">El cambio en la cantidad de liquidez disponible para el token durante el período de tiempo especificado</string>
<string name="markets_token_details_liquidity_index">Índice de liquidez</string>
<string name="markets_token_details_low">Bajo</string>
<string name="markets_token_details_market_capitalization">Capitalización de mercado</string>
<string name="markets_token_details_market_capitalization">Capital. de mercado</string>
<string name="markets_token_details_market_capitalization_description">El valor total de una criptomoneda calculado multiplicando su precio por la cantidad de monedas en circulación</string>
<string name="markets_token_details_market_rating">Evaluación de mercado</string>
<string name="markets_token_details_market_rating_description">Posición en la clasificación de criptomonedas entre todas las monedas según la capitalización de mercado</string>
@ -412,6 +415,7 @@
<string name="markets_token_details_trading_volume_24h_description">La cantidad total de una criptomoneda que se ha negociado en las últimas 24 horas, lo que indica el nivel de actividad y liquidez en el mercado.</string>
<string name="markets_tooltip_message">Tire hacia arriba o toque la barra de búsqueda para agregar tokens directamente desde el mercado</string>
<string name="markets_tooltip_title">Agregar tokens</string>
<string name="nfc_error_unavailable">NFC no está disponible en su dispositivo</string>
<string name="onboarding_access_code_feature_1_description">Deba configurar un único código de acceso para proteger todas sus tarjetas</string>
<string name="onboarding_access_code_feature_1_title">Proteger</string>
<string name="onboarding_access_code_feature_2_description">Puede configurar un código de acceso individual en cada tarjeta más tarde</string>
@ -637,6 +641,7 @@
<string name="settings_forget_wallet_footer">Esto eliminará la wallet de la aplicación. La wallet en sí puede\nañadirse de nuevo.</string>
<string name="settings_wallet_name_title">Nombre</string>
<string name="staking_amount_requirement_error">El monto del staking debe ser al menos %s</string>
<string name="staking_amount_tron_integer_error">El monto del staking se redondeará a %1$s TRX debido a las reglas de la red.</string>
<string name="staking_claim_unstaked">Unstaking de la reclamación</string>
<string name="staking_details_annual_percentage_rate">Porcentaje de rendimiento anual</string>
<string name="staking_details_annual_percentage_rate_info">El porcentaje de rendimiento anual que podrá ganar como participante en el staking.</string>
@ -660,6 +665,7 @@
<string name="staking_details_unbonding_period_info">El período que debes esperar después de solicitar el retiro de fondos del staking antes de que los tokens estén disponibles.</string>
<string name="staking_details_warmup_period">Periodo de calentamiento</string>
<string name="staking_details_warmup_period_info">El tiempo permitido para activar la participación en la apuesta.</string>
<string name="staking_legal">Al utilizar la función de staking, usted acepta %1$s y %2$s del proveedor</string>
<string name="staking_locked">Bloqueado</string>
<string name="staking_migrate">Migrar</string>
<string name="staking_native">Native staking</string>
@ -671,6 +677,7 @@
<string name="staking_notification_earn_rewards_title">Gane recompensas por staking</string>
<string name="staking_notification_unstake_text">Sus fondos estarán disponibles para su uso después del período de desvinculación de %s.</string>
<string name="staking_notification_withdraw_text">Ya puede retirar sus fondos, estarán disponibles para usar de inmediato.</string>
<string name="staking_notifications_revote_tron_text">Hacer staking en la red Tron con un nuevo validador transferirá automáticamente todos los fondos de staking a este validador.</string>
<string name="staking_ready_to_withdraw">Listo para retirar</string>
<string name="staking_rebond">Reunir</string>
<string name="staking_restake">Haga el staking de nuevo</string>
@ -690,8 +697,11 @@
<string name="staking_rewards">Recompensas</string>
<string name="staking_stake_locked">El stake está bloqueado</string>
<string name="staking_stake_more">Hacer más staking</string>
<string name="staking_tap_to_unlock">Toque para desbloquear</string>
<string name="staking_tap_to_withdraw">Toque para retirar</string>
<string name="staking_title_stake">Stake %s</string>
<string name="staking_title_unstake">El unstaking de %s</string>
<string name="staking_transaction_in_progress_text">¡La transacción se está procesando! La validación está en curso en la cadena de bloques. Esto puede tardar unos minutos.</string>
<string name="staking_unbonding">Desunión</string>
<string name="staking_unlocked_locked">Desbloquear</string>
<string name="staking_unstaked">Sin staking</string>
@ -701,6 +711,7 @@
<string name="staking_vote">Votar</string>
<string name="staking_vote_locked">Voto bloqueado</string>
<string name="staking_withdraw">Retirar</string>
<string name="staking_your_stakes">Sus stakes</string>
<string name="story_awe_description">Almacene sus activos crypto de forma segura manteniendo las claves privadas contenidas en su tarjeta</string>
<string name="story_awe_title">Billetera de hardware revolucionaria</string>
<string name="story_backup_description">Hasta 3 tarjetas físicas por billetera</string>

View file

@ -117,6 +117,7 @@
<string name="common_import">Importez</string>
<string name="common_in_progress">En cours</string>
<string name="common_later">Plus tard</string>
<string name="common_left">Il reste %1$s</string>
<string name="common_locked">Verrouillé</string>
<string name="common_main_network">Réseau principal</string>
<string name="common_network_fee_title">Commissions du réseau</string>
@ -295,6 +296,7 @@
<string name="give_permission_unlimited">Illimité</string>
<string name="home_button_order">Commandez</string>
<string name="home_button_scan">Scannez</string>
<string name="information_generated_with_ai">Ces informations ont été générées avec l\'IA</string>
<string name="initial_message_change_access_code_body">Touchez, pour modifier le code d\'accès</string>
<string name="initial_message_change_passcode_body">Touchez, pour modifier le mot de passe</string>
<string name="initial_message_create_wallet_body">Pour créer le portefeuille, appuyez sur la carte comme indiqué ci-dessus et ne la retirez pas jusqu\'à la fin de l\'opération</string>
@ -348,6 +350,7 @@
<string name="markets_common_my_portfolio">Mon portfolio</string>
<string name="markets_common_title">Marché</string>
<string name="markets_generate_addresses_notification">Pour générer des adresses pour les réseaux sélectionnés, vous devez scanner votre carte Tangem</string>
<string name="markets_hint">Pour ajouter des jetons, faites-le apparaître ou appuyez sur la barre de recherche</string>
<string name="markets_insights_info_description_message">Les données de cette section proviennent des réseaux suivants : %s</string>
<string name="markets_loading_error_title">Impossible de charger les données…</string>
<string name="markets_loading_no_data_title">Aucune donnée</string>
@ -358,11 +361,11 @@
<string name="markets_search_token_no_result_title">Aucun résultat</string>
<string name="markets_select_network">Sélectionnez un réseau</string>
<string name="markets_select_wallet">Sélectionnez un portefeuille</string>
<string name="markets_selector_interval_1m_title">1 mois</string>
<string name="markets_selector_interval_1m_title">1 mo</string>
<string name="markets_selector_interval_1y_title">1 an</string>
<string name="markets_selector_interval_24h_title">24 heures</string>
<string name="markets_selector_interval_3m_title">3 mois</string>
<string name="markets_selector_interval_6m_title">6 mois</string>
<string name="markets_selector_interval_24h_title">24h</string>
<string name="markets_selector_interval_3m_title">3mos</string>
<string name="markets_selector_interval_6m_title">6mos</string>
<string name="markets_selector_interval_7d_title">7j</string>
<string name="markets_selector_interval_all_title">Tout</string>
<string name="markets_sort_by_experienced_buyers_title">Acheteurs expérimentés</string>
@ -395,8 +398,8 @@
<string name="markets_token_details_liquidity_description">Le changement dans la quantité de liquidité disponible pour le jeton pendant la période spécifiée</string>
<string name="markets_token_details_liquidity_index">Indice de liquidité</string>
<string name="markets_token_details_low">Faible</string>
<string name="markets_token_details_market_capitalization">Capitalisation boursière</string>
<string name="markets_token_details_market_capitalization_description">La valeur totale d\'une crypto-monnaie calculée en multipliant son prix actuel par le nbre de pièces en circulation</string>
<string name="markets_token_details_market_capitalization">Cap. boursière</string>
<string name="markets_token_details_market_capitalization_description">La valeur d\'une crypto-monnaie est calculée en multipliant son prix par le nombre de pièces en circulation</string>
<string name="markets_token_details_market_rating">Évaluation du marché</string>
<string name="markets_token_details_market_rating_description">Position dans le classement des crypto-monnaies entre toutes les pièces en fonction de la capitalisation boursière</string>
<string name="markets_token_details_max_supply">Approvisionnement maximal</string>
@ -412,6 +415,7 @@
<string name="markets_token_details_trading_volume_24h_description">Le montant total d\'une crypto-monnaie qui a été échangé au cours des dernières 24 heures, indiquant le niveau d\'activité et de liquidité du marché</string>
<string name="markets_tooltip_message">Tirez vers le haut ou appuyez sur la barre de recherche pour ajouter des jetons directement depuis le marché</string>
<string name="markets_tooltip_title">Ajouter des jetons</string>
<string name="nfc_error_unavailable">NFC n\'est pas disponible sur votre appareil</string>
<string name="onboarding_access_code_feature_1_description">Vous devez définir un seul code d\'accès pour protéger toutes vos cartes</string>
<string name="onboarding_access_code_feature_1_title">Protéger</string>
<string name="onboarding_access_code_feature_2_description">Vous pourrez définir un code d\'accès individuel sur chaque carte plus tard</string>
@ -637,6 +641,7 @@
<string name="settings_forget_wallet_footer">Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau.</string>
<string name="settings_wallet_name_title">Nom</string>
<string name="staking_amount_requirement_error">Le montant à staker doit être au moins %s</string>
<string name="staking_amount_tron_integer_error">Le montant du staking sera arrondi à %1$s TRX en raison des règles du réseau.</string>
<string name="staking_claim_unstaked">Réclamation déstakée</string>
<string name="staking_details_annual_percentage_rate">Pourcentage de rendement annuel</string>
<string name="staking_details_annual_percentage_rate_info">Le pourcentage de rendement annuel que vous pouvez gagner en participant au staking.</string>
@ -660,6 +665,7 @@
<string name="staking_details_unbonding_period_info">La période que vous devez attendre après avoir demandé le retrait des fonds du staking avant que les jetons ne soient disponibles.</string>
<string name="staking_details_warmup_period">Période d\'échauffement</string>
<string name="staking_details_warmup_period_info">Le temps imparti pour activer la participation au staking.</string>
<string name="staking_legal">En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur</string>
<string name="staking_locked">Bloqué</string>
<string name="staking_migrate">Migrer</string>
<string name="staking_native">Native staking</string>
@ -671,6 +677,7 @@
<string name="staking_notification_earn_rewards_title">Gagnez des récompenses de staking</string>
<string name="staking_notification_unstake_text">Vos fonds seront disponibles pour utilisation après la période de désengagement %s.</string>
<string name="staking_notification_withdraw_text">Vous pouvez désormais retirer vos fonds, ils seront disponibles à l\'utilisation immédiatement</string>
<string name="staking_notifications_revote_tron_text">Le staking dans le réseau Tron avec un nouveau validateur transférera automatiquement tous les fonds précédemment stakés vers ce validateur</string>
<string name="staking_ready_to_withdraw">Prêt à retirer</string>
<string name="staking_rebond">Réassocier</string>
<string name="staking_restake">Restakez</string>
@ -690,8 +697,11 @@
<string name="staking_rewards">Récompenses</string>
<string name="staking_stake_locked">Stake verrouillé</string>
<string name="staking_stake_more">Staker plus</string>
<string name="staking_tap_to_unlock">Appuyez pour déverrouiller</string>
<string name="staking_tap_to_withdraw">Appuyez pour retirer</string>
<string name="staking_title_stake">Stake %s</string>
<string name="staking_title_unstake">déstaker %s</string>
<string name="staking_transaction_in_progress_text">La transaction est en cours de traitement ! La validation est actuellement en cours dans la blockchain. Cela peut prendre quelques minutes.</string>
<string name="staking_unbonding">Dissociation</string>
<string name="staking_unlocked_locked">Débloquer</string>
<string name="staking_unstaked">Non-staké</string>
@ -701,6 +711,7 @@
<string name="staking_vote">Voter</string>
<string name="staking_vote_locked">Vote bloqué</string>
<string name="staking_withdraw">Retirer</string>
<string name="staking_your_stakes">Vos stakes</string>
<string name="story_awe_description">Stockez vos actifs crypto en toute sécurité tout en conservant les clés privées contenues dans votre carte</string>
<string name="story_awe_title">Portefeuille matériel révolutionnaire</string>
<string name="story_backup_description">Jusqu\'à 3 cartes physiques pour un portefeuille</string>

View file

@ -358,13 +358,13 @@
<string name="markets_search_token_no_result_title">検索結果はありません</string>
<string name="markets_select_network">ネットワークを選択</string>
<string name="markets_select_wallet">ウォレットを選択</string>
<string name="markets_selector_interval_1m_title">1ヶ月</string>
<string name="markets_selector_interval_1y_title">1</string>
<string name="markets_selector_interval_24h_title">24時間</string>
<string name="markets_selector_interval_3m_title">3ヶ月</string>
<string name="markets_selector_interval_6m_title">6ヶ月</string>
<string name="markets_selector_interval_7d_title">7</string>
<string name="markets_selector_interval_all_title">すべて</string>
<string name="markets_selector_interval_1m_title">1m</string>
<string name="markets_selector_interval_1y_title">1y</string>
<string name="markets_selector_interval_24h_title">24h</string>
<string name="markets_selector_interval_3m_title">3m</string>
<string name="markets_selector_interval_6m_title">6m</string>
<string name="markets_selector_interval_7d_title">7d</string>
<string name="markets_selector_interval_all_title">全部</string>
<string name="markets_sort_by_experienced_buyers_title">経験豊富な買い手</string>
<string name="markets_sort_by_rating_title">格付け</string>
<string name="markets_sort_by_title">並べ替え</string>
@ -637,6 +637,7 @@
<string name="settings_forget_wallet_footer">これにより、ウォレットがアプリから削除されます。ウォレットは再度追加できます。</string>
<string name="settings_wallet_name_title">名前</string>
<string name="staking_amount_requirement_error">ステーキング金額は %s 以上である必要があります</string>
<string name="staking_amount_tron_integer_error">ネットワークルールにより、ステーキング金額は%1$s TRX に切り上げられます。</string>
<string name="staking_claim_unstaked">ステーキング解除分を請求する</string>
<string name="staking_details_annual_percentage_rate">年率</string>
<string name="staking_details_annual_percentage_rate_info">ステーキングに参加することで得られる年間収益率。</string>

View file

@ -1,15 +1,14 @@
package com.tangem.core.ui.components
import android.os.Build
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.ime
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Immutable
sealed interface Keyboard {
val height: Dp
@ -30,6 +29,34 @@ val Keyboard.isOpened: Boolean
@Composable
fun keyboardAsState(): State<Keyboard> {
val bottom = WindowInsets.ime.getBottom(LocalDensity.current)
val isImeVisible = bottom > 0
return rememberUpdatedState(if (isImeVisible) Keyboard.Opened(bottom.dp) else Keyboard.Closed)
val bottomDp = with(LocalDensity.current) { bottom.toDp() }
val keyboardStateInternal by rememberUpdatedState(
if (bottom > 0) Keyboard.Opened(bottomDp) else Keyboard.Closed,
)
val keyboardState = remember { mutableStateOf(keyboardStateInternal) }
LaunchedEffect(keyboardStateInternal) {
val falsePositive = Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q &&
keyboardStateInternal is Keyboard.Opened &&
keyboardStateInternal.height < 50.dp
// FIX android <=10 devices can randomly send ime paddings,
// which leads to a false positive keyboard opening ([REDACTED_TASK_KEY])
if (falsePositive) return@LaunchedEffect
keyboardState.value = keyboardStateInternal
}
return keyboardState
}
@Composable
fun rememberIsKeyboardVisible(): State<Boolean> {
val keyboard by keyboardAsState()
return remember(keyboard) {
derivedStateOf {
keyboard.isOpened
}
}
}

View file

@ -8,7 +8,7 @@ import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@ -47,7 +47,7 @@ fun TangemSwitch(
modifier = modifier
.clickable(
interactionSource = interactionSource,
indication = rememberRipple(
indication = ripple(
bounded = false,
color = Color.Transparent,
),

View file

@ -8,8 +8,8 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.ripple
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.drawscope.Stroke
@ -39,7 +39,7 @@ fun TangemRadioButton(
modifier = modifier.clickable(
enabled = isEnabled,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = TangemTheme.dimens.size16),
indication = ripple(bounded = false, radius = TangemTheme.dimens.size16),
onClick = onClick,
),
) {

View file

@ -1,5 +1,6 @@
package com.tangem.core.ui.components.bottomsheets
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
@ -14,7 +15,6 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import kotlinx.coroutines.coroutineScope
@ -150,17 +150,15 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
) {
val model = config.content as? T ?: return
val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
ModalBottomSheet(
// FIXME temporary solution to fix height of the bottom sheet
modifier = modifier.heightIn(max = LocalWindowSize.current.height - statusBarHeight),
modifier = modifier.statusBarsPadding(),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = containerColor,
shape = TangemTheme.shapes.bottomSheetLarge,
windowInsets = WindowInsetsZero,
contentWindowInsets = { WindowInsetsZero },
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
) {
Column(

View file

@ -6,9 +6,9 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@ -86,7 +86,7 @@ fun InputRowBestRate(
.padding(vertical = TangemTheme.dimens.spacing10)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
indication = ripple(bounded = false),
onClick = onIconClick,
),
)

View file

@ -8,9 +8,9 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterVertically
@ -94,7 +94,7 @@ fun InputRowDefault(
.clickable(
enabled = onIconClick != null,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
indication = ripple(bounded = false),
) { onIconClick?.invoke() },
)
}

View file

@ -6,9 +6,9 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@ -98,7 +98,7 @@ fun InputRowEnter(
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
indication = ripple(bounded = false),
) { onIconClick?.invoke() },
)
}

View file

@ -8,9 +8,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@ -104,7 +104,7 @@ fun InputRowEnterAmount(
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
indication = ripple(bounded = false),
) { onIconClick?.invoke() },
)
}

View file

@ -5,9 +5,9 @@ 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.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterVertically
@ -111,7 +111,7 @@ fun InputRowImage(
.align(CenterVertically)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
indication = ripple(bounded = false),
) { onIconClick?.invoke() },
)
}

View file

@ -5,7 +5,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@ -64,7 +64,7 @@ fun InputRowImageSelector(
.clickable(
onClick = onSelect,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
indication = ripple(),
)
.padding(TangemTheme.dimens.spacing12),
) {

View file

@ -9,9 +9,9 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
@ -65,7 +65,7 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(radius = TangemTheme.dimens.radius8),
indication = ripple(radius = TangemTheme.dimens.radius8),
enabled = isPasteEnabled,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
@ -91,7 +91,7 @@ fun CrossIcon(onClick: (String) -> Unit, modifier: Modifier = Modifier) {
.size(TangemTheme.dimens.size24)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(radius = TangemTheme.dimens.radius12),
indication = ripple(radius = TangemTheme.dimens.radius12),
onClick = { onClick("") },
),
)

View file

@ -7,7 +7,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ripple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -125,7 +125,7 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier =
}
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
indication = ripple(bounded = false),
) {
config.onCloseClick?.invoke()
},

View file

@ -5,9 +5,9 @@ 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.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@ -95,7 +95,7 @@ private fun Content(config: NotificationConfig) {
.size(TangemTheme.dimens.size20)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(radius = TangemTheme.dimens.radius10),
indication = ripple(radius = TangemTheme.dimens.radius10),
onClick = it,
),
)

View file

@ -0,0 +1,126 @@
@file:Suppress("UnnecessaryParentheses")
package com.tangem.core.ui.components.progressbar
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.layout.layout
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.progressBarRangeInfo
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.offset
import com.tangem.core.ui.res.TangemTheme
import kotlin.math.abs
/**
* Progress indicators express an unspecified wait time or display the length of a process.
*
* By default there is no animation between [progress] values. You can use
*
* @param progress The progress of this progress indicator, where 0.0 represents no progress and 1.0
* represents full progress. Values outside of this range are coerced into the range.
* @param modifier the [Modifier] to be applied to this progress indicator
* @param color The color of the progress indicator.
* @param backgroundColor The color of the background behind the indicator, visible when the
* progress has not reached that area of the overall indicator yet.
* @param strokeCap stroke cap to use for the ends of this progress indicator
*/
@Composable
fun LinearProgressIndicator(
progress: () -> Float,
modifier: Modifier = Modifier,
color: Color = TangemTheme.colors.text.accent,
backgroundColor: Color = TangemTheme.colors.background.tertiary,
strokeCap: StrokeCap = StrokeCap.Round,
) {
val coercedProgress = { progress().coerceIn(0f, 1f) }
Canvas(
modifier
.increaseSemanticsBounds()
.semantics(mergeDescendants = true) {
progressBarRangeInfo = ProgressBarRangeInfo(coercedProgress(), 0f..1f)
}
.size(height = 4.dp, width = 24.dp),
) {
val strokeWidth = size.height
drawLinearIndicatorBackground(backgroundColor, strokeWidth, strokeCap)
drawLinearIndicator(0f, coercedProgress(), color, strokeWidth, strokeCap)
}
}
internal fun Modifier.increaseSemanticsBounds(): Modifier {
val padding = 10.dp
return this
.layout { measurable, constraints ->
val paddingPx = padding.roundToPx()
// We need to add vertical padding to the semantics bounds in other to meet
// screenreader green box minimum size, but we also want to
// preserve a visual appearance and layout size below that minimum
// in order to maintain backwards compatibility. This custom
// layout effectively implements "negative padding".
val newConstraint = constraints.offset(0, paddingPx * 2)
val placeable = measurable.measure(newConstraint)
// But when actually placing the placeable, create the layout without additional
// space. Place the placeable where it would've been without any extra padding.
val height = placeable.height - paddingPx * 2
val width = placeable.width
layout(width, height) {
placeable.place(0, -paddingPx)
}
}
.semantics(mergeDescendants = true) {}
.padding(vertical = padding)
}
private fun DrawScope.drawLinearIndicator(
startFraction: Float,
endFraction: Float,
color: Color,
strokeWidth: Float,
strokeCap: StrokeCap,
) {
val width = size.width
val height = size.height
// Start drawing from the vertical center of the stroke
val yOffset = height / 2
val isLtr = layoutDirection == LayoutDirection.Ltr
val barStart = (if (isLtr) startFraction else 1f - endFraction) * width
val barEnd = (if (isLtr) endFraction else 1f - startFraction) * width
// if there isn't enough space to draw the stroke caps, fall back to StrokeCap.Butt
if (strokeCap == StrokeCap.Butt || height > width) {
// Progress line
drawLine(color, Offset(barStart, yOffset), Offset(barEnd, yOffset), strokeWidth)
} else {
// need to adjust barStart and barEnd for the stroke caps
val strokeCapOffset = strokeWidth / 2
val coerceRange = strokeCapOffset..(width - strokeCapOffset)
val adjustedBarStart = barStart.coerceIn(coerceRange)
val adjustedBarEnd = barEnd.coerceIn(coerceRange)
if (abs(endFraction - startFraction) > 0) {
// Progress line
drawLine(
color,
Offset(adjustedBarStart, yOffset),
Offset(adjustedBarEnd, yOffset),
strokeWidth,
strokeCap,
)
}
}
}
private fun DrawScope.drawLinearIndicatorBackground(color: Color, strokeWidth: Float, strokeCap: StrokeCap) =
drawLinearIndicator(0f, 1f, color, strokeWidth, strokeCap)

View file

@ -6,10 +6,10 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@ -67,7 +67,7 @@ fun RoundableCornersRow(
.size(TangemTheme.dimens.size16)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = TangemTheme.dimens.radius10),
indication = ripple(bounded = false, radius = TangemTheme.dimens.radius10),
onClick = iconClick,
),
painter = painterResource(id = R.drawable.ic_alert_24),

View file

@ -0,0 +1,122 @@
@file:Suppress("all")
package com.tangem.core.ui.components.sheetscaffold
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import kotlin.math.roundToInt
/**
* This Modifier allows configuring an [AnchoredDraggableState]'s anchors based on this layout
* node's size and offsetting it. It considers lookahead and reports the appropriate size and
* measurement for the appropriate phase.
*
* @param state The state the anchors should be attached to
* @param orientation The orientation the component should be offset in
* @param anchors Lambda to calculate the anchors based on this layout's size and the incoming
* constraints. These can be useful to avoid subcomposition.
*/
@OptIn(ExperimentalFoundationApi::class)
internal fun <T> Modifier.draggableAnchors(
state: AnchoredDraggableState<T>,
orientation: Orientation,
anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
) = this then DraggableAnchorsElement(state, anchors, orientation)
@OptIn(ExperimentalFoundationApi::class)
private class DraggableAnchorsElement<T>(
private val state: AnchoredDraggableState<T>,
private val anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
private val orientation: Orientation,
) : ModifierNodeElement<DraggableAnchorsNode<T>>() {
@OptIn(ExperimentalFoundationApi::class)
override fun create() = DraggableAnchorsNode(state, anchors, orientation)
override fun update(node: DraggableAnchorsNode<T>) {
node.state = state
node.anchors = anchors
node.orientation = orientation
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is DraggableAnchorsElement<*>) return false
if (state != other.state) return false
if (anchors !== other.anchors) return false
if (orientation != other.orientation) return false
return true
}
override fun hashCode(): Int {
var result = state.hashCode()
result = 31 * result + anchors.hashCode()
result = 31 * result + orientation.hashCode()
return result
}
override fun InspectorInfo.inspectableProperties() {
debugInspectorInfo {
properties["state"] = state
properties["anchors"] = anchors
properties["orientation"] = orientation
}
}
}
@OptIn(ExperimentalFoundationApi::class)
private class DraggableAnchorsNode<T>(
var state: AnchoredDraggableState<T>,
var anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
var orientation: Orientation,
) : Modifier.Node(), LayoutModifierNode {
private var didLookahead: Boolean = false
override fun onDetach() {
didLookahead = false
}
@OptIn(ExperimentalFoundationApi::class)
override fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult {
val placeable = measurable.measure(constraints)
// If we are in a lookahead pass, we only want to update the anchors here and not in
// post-lookahead. If there is no lookahead happening (!isLookingAhead && !didLookahead),
// update the anchors in the main pass.
if (!isLookingAhead || !didLookahead) {
val size = IntSize(placeable.width, placeable.height)
val newAnchorResult = anchors(size, constraints)
state.updateAnchors(newAnchorResult.first, newAnchorResult.second)
}
didLookahead = isLookingAhead || didLookahead
return layout(placeable.width, placeable.height) {
// In a lookahead pass, we use the position of the current target as this is where any
// ongoing animations would move. If the component is in a settled state, lookahead
// and post-lookahead will converge.
val offset = if (isLookingAhead) {
state.anchors.positionOf(state.targetValue)
} else {
state.requireOffset()
}
val xOffset = if (orientation == Orientation.Horizontal) offset else 0f
val yOffset = if (orientation == Orientation.Vertical) offset else 0f
placeable.place(xOffset.roundToInt(), yOffset.roundToInt())
}
}
}

View file

@ -0,0 +1,334 @@
@file:Suppress("all")
package com.tangem.core.ui.components.sheetscaffold
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxOfOrNull
import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue.*
import com.tangem.core.ui.res.TangemTheme
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
/**
* <a href="https://m3.material.io/components/bottom-sheets/overview" class="external"
* target="_blank">Material Design standard bottom sheet scaffold</a>.
*
* Standard bottom sheets co-exist with the screens main UI region and allow for simultaneously
* viewing and interacting with both regions. They are commonly used to keep a feature or secondary
* content visible on screen when content in main UI region is frequently scrolled or panned.
*
* ![Bottom sheet
* image](https://developer.android.com/images/reference/androidx/compose/material3/bottom_sheet.png)
*
* This component provides API to put together several material components to construct your screen,
* by ensuring proper layout strategy for them and collecting necessary data so these components
* will work together correctly.
*
* @param sheetContent the content of the bottom sheet
* @param modifier the [Modifier] to be applied to this scaffold
* @param scaffoldState the state of the bottom sheet scaffold
* @param sheetPeekHeight the height of the bottom sheet when it is collapsed
* @param sheetMaxWidth [Dp] that defines what the maximum width the sheet will take. Pass in
* [Dp.Unspecified] for a sheet that spans the entire screen width.
* @param sheetShape the shape of the bottom sheet
* @param sheetContainerColor the background color of the bottom sheet
* @param sheetContentColor the preferred content color provided by the bottom sheet to its
* children. Defaults to the matching content color for [sheetContainerColor], or if that is not a
* color from the theme, this will keep the same content color set above the bottom sheet.
* @param sheetTonalElevation when [sheetContainerColor] is [ColorScheme.surface], a translucent
* primary color overlay is applied on top of the container. A higher tonal elevation value will
* result in a darker color in light theme and lighter color in dark theme. See also: [Surface].
* @param sheetShadowElevation the shadow elevation of the bottom sheet
* @param sheetSwipeEnabled whether the sheet swiping is enabled and should react to the user's
* input
* @param topBar top app bar of the screen, typically a [SmallTopAppBar]
* @param snackbarHost component to host [Snackbar]s that are pushed to be shown via
* [SnackbarHostState.showSnackbar], typically a [SnackbarHost]
* @param containerColor the color used for the background of this scaffold. Use [Color.Transparent]
* to have no color.
* @param contentColor the preferred color for content inside this scaffold. Defaults to either the
* matching content color for [containerColor], or to the current [LocalContentColor] if
* [containerColor] is not a color from the theme.
* @param content content of the screen. The lambda receives a [PaddingValues] that should be
* applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to
* properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to
* the child of the scroll, and not on the scroll itself.
*/
@Composable
fun TangemBottomSheetScaffold(
sheetContent: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
scaffoldState: TangemBottomSheetScaffoldState = rememberTangemBottomSheetScaffoldState(),
sheetPeekHeight: Dp,
sheetMaxWidth: Dp = 640.dp,
sheetShape: Shape = TangemTheme.shapes.bottomSheetLarge,
sheetContainerColor: Color = Color.White, // FIXME
sheetContentColor: Color = contentColorFor(sheetContainerColor),
sheetTonalElevation: Dp = 0.dp,
sheetShadowElevation: Dp = 1.dp,
sheetSwipeEnabled: Boolean = true,
topBar: @Composable (() -> Unit)? = null,
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
containerColor: Color = TangemTheme.colors.background.secondary,
contentColor: Color = contentColorFor(containerColor),
content: @Composable (PaddingValues) -> Unit,
) {
BottomSheetScaffoldLayout(
modifier = modifier,
topBar = topBar,
body = { content(PaddingValues(bottom = sheetPeekHeight)) },
snackbarHost = { snackbarHost(scaffoldState.snackbarHostState) },
sheetOffset = { scaffoldState.bottomSheetState.requireOffset() },
sheetState = scaffoldState.bottomSheetState,
containerColor = containerColor,
contentColor = contentColor,
bottomSheet = {
StandardBottomSheet(
state = scaffoldState.bottomSheetState,
peekHeight = sheetPeekHeight,
sheetMaxWidth = sheetMaxWidth,
sheetSwipeEnabled = sheetSwipeEnabled,
shape = sheetShape,
containerColor = sheetContainerColor,
contentColor = sheetContentColor,
tonalElevation = sheetTonalElevation,
shadowElevation = sheetShadowElevation,
content = sheetContent,
)
},
)
}
/**
* State of the [TangemBottomSheetScaffold] composable.
*
* @param bottomSheetState the state of the persistent bottom sheet
* @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold
*/
@Stable
class TangemBottomSheetScaffoldState(
val bottomSheetState: TangemSheetState,
val snackbarHostState: SnackbarHostState,
)
/**
* Create and [remember] a [TangemBottomSheetScaffoldState].
*
* @param bottomSheetState the state of the standard bottom sheet. See
* [rememberTangemStandardBottomSheetState]
* @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold
*/
@Composable
fun rememberTangemBottomSheetScaffoldState(
bottomSheetState: TangemSheetState = rememberTangemStandardBottomSheetState(),
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
): TangemBottomSheetScaffoldState {
return remember(bottomSheetState, snackbarHostState) {
TangemBottomSheetScaffoldState(
bottomSheetState = bottomSheetState,
snackbarHostState = snackbarHostState,
)
}
}
/**
* Create and [remember] a [TangemSheetState] for [TangemBottomSheetScaffold].
*
* @param initialValue the initial value of the state. Should be either [PartiallyExpanded] or
* [Expanded] if [skipHiddenState] is true
* @param confirmValueChange optional callback invoked to confirm or veto a pending state change
* @param [skipHiddenState] whether Hidden state is skipped for [TangemBottomSheetScaffold]
*/
@Composable
fun rememberTangemStandardBottomSheetState(
initialValue: TangemSheetValue = PartiallyExpanded,
confirmValueChange: (TangemSheetValue) -> Boolean = { true },
skipHiddenState: Boolean = true,
) = rememberSheetState(
confirmValueChange = confirmValueChange,
initialValue = initialValue,
skipHiddenState = skipHiddenState,
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun StandardBottomSheet(
state: TangemSheetState,
peekHeight: Dp,
sheetMaxWidth: Dp,
sheetSwipeEnabled: Boolean,
shape: Shape,
containerColor: Color,
contentColor: Color,
tonalElevation: Dp,
shadowElevation: Dp,
content: @Composable ColumnScope.() -> Unit,
) {
val scope = rememberCoroutineScope()
val orientation = Orientation.Vertical
val peekHeightPx = with(LocalDensity.current) { peekHeight.toPx() }
val nestedScroll =
if (sheetSwipeEnabled) {
Modifier.nestedScroll(
remember(state.anchoredDraggableState) {
consumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
sheetState = state,
orientation = orientation,
onFling = { scope.launch { state.settle(it) } },
)
},
)
} else {
Modifier
}
Surface(
modifier = Modifier
.widthIn(max = sheetMaxWidth)
.fillMaxWidth()
.requiredHeightIn(min = peekHeight)
.then(nestedScroll)
.draggableAnchors(
state = state.anchoredDraggableState,
orientation = orientation,
anchors = { sheetSize, constraints ->
val layoutHeight = constraints.maxHeight.toFloat()
val sheetHeight = sheetSize.height.toFloat()
val newAnchors = DraggableAnchors {
if (!state.skipPartiallyExpanded) {
PartiallyExpanded at (layoutHeight - peekHeightPx)
}
if (sheetHeight != peekHeightPx) {
Expanded at maxOf(layoutHeight - sheetHeight, 0f)
}
if (!state.skipHiddenState) {
Hidden at layoutHeight
}
}
val newTarget =
when (val oldTarget = state.anchoredDraggableState.targetValue) {
Hidden -> if (newAnchors.hasAnchorFor(Hidden)) Hidden else oldTarget
PartiallyExpanded ->
when {
newAnchors.hasAnchorFor(PartiallyExpanded) -> PartiallyExpanded
newAnchors.hasAnchorFor(Expanded) -> Expanded
newAnchors.hasAnchorFor(Hidden) -> Hidden
else -> oldTarget
}
Expanded ->
when {
newAnchors.hasAnchorFor(Expanded) -> Expanded
newAnchors.hasAnchorFor(PartiallyExpanded) -> PartiallyExpanded
newAnchors.hasAnchorFor(Hidden) -> Hidden
else -> oldTarget
}
}
newAnchors to newTarget
},
)
.anchoredDraggable(
state = state.anchoredDraggableState,
orientation = orientation,
enabled = sheetSwipeEnabled,
),
shape = shape,
color = containerColor,
contentColor = contentColor,
tonalElevation = tonalElevation,
shadowElevation = shadowElevation,
) {
Column(Modifier.fillMaxWidth()) {
content()
}
}
}
@Composable
private fun BottomSheetScaffoldLayout(
modifier: Modifier,
topBar: @Composable (() -> Unit)?,
body: @Composable () -> Unit,
bottomSheet: @Composable () -> Unit,
snackbarHost: @Composable () -> Unit,
sheetOffset: () -> Float,
sheetState: TangemSheetState,
containerColor: Color,
contentColor: Color,
) {
Layout(
contents =
listOf<@Composable () -> Unit>(
topBar ?: {},
{
Surface(
modifier = modifier,
color = containerColor,
contentColor = contentColor,
content = body,
)
},
bottomSheet,
snackbarHost,
),
) {
(topBarMeasurables, bodyMeasurables, bottomSheetMeasurables, snackbarHostMeasurables),
constraints,
->
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val sheetPlaceables = bottomSheetMeasurables.fastMap { it.measure(looseConstraints) }
val topBarPlaceables = topBarMeasurables.fastMap { it.measure(looseConstraints) }
val topBarHeight = topBarPlaceables.fastMaxOfOrNull { it.height } ?: 0
val bodyConstraints = looseConstraints.copy(maxHeight = layoutHeight - topBarHeight)
val bodyPlaceables = bodyMeasurables.fastMap { it.measure(bodyConstraints) }
val snackbarPlaceables = snackbarHostMeasurables.fastMap { it.measure(looseConstraints) }
layout(layoutWidth, layoutHeight) {
val sheetWidth = sheetPlaceables.fastMaxOfOrNull { it.width } ?: 0
val sheetOffsetX = Integer.max(0, (layoutWidth - sheetWidth) / 2)
val snackbarWidth = snackbarPlaceables.fastMaxOfOrNull { it.width } ?: 0
val snackbarHeight = snackbarPlaceables.fastMaxOfOrNull { it.height } ?: 0
val snackbarOffsetX = (layoutWidth - snackbarWidth) / 2
val snackbarOffsetY =
when (sheetState.currentValue) {
PartiallyExpanded -> sheetOffset().roundToInt() - snackbarHeight
Expanded,
Hidden,
-> layoutHeight - snackbarHeight
}
// Placement order is important for elevation
bodyPlaceables.fastForEach { it.placeRelative(0, topBarHeight) }
topBarPlaceables.fastForEach { it.placeRelative(0, 0) }
sheetPlaceables.fastForEach { it.placeRelative(sheetOffsetX, 0) }
snackbarPlaceables.fastForEach { it.placeRelative(snackbarOffsetX, snackbarOffsetY) }
}
}
}

View file

@ -0,0 +1,339 @@
@file:Suppress("all")
package com.tangem.core.ui.components.sheetscaffold
import androidx.compose.animation.core.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue.*
import kotlinx.coroutines.CancellationException
/**
* State of a sheet composable
*
* Contains states relating to its swipe position as well as animations between state values.
*
* @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is large enough,
* should be skipped. If true, the sheet will always expand to the [Expanded] state and move to
* the [Hidden] state if available when hiding the sheet, either programmatically or by user
* interaction.
* @param initialValue The initial value of the state.
* @param density The density that this state can use to convert values to and from dp.
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
* @param skipHiddenState Whether the hidden state should be skipped. If true, the sheet will always
* expand to the [Expanded] state and move to the [PartiallyExpanded] if available, either
* programmatically or by user interaction.
*/
@Stable
@OptIn(ExperimentalFoundationApi::class)
class TangemSheetState(
internal val skipPartiallyExpanded: Boolean,
density: Density,
initialValue: TangemSheetValue = Hidden,
confirmValueChange: (TangemSheetValue) -> Boolean = { true },
internal val skipHiddenState: Boolean = false,
) {
init {
if (skipPartiallyExpanded) {
require(initialValue != PartiallyExpanded) {
"The initial value must not be set to PartiallyExpanded if skipPartiallyExpanded " +
"is set to true."
}
}
if (skipHiddenState) {
require(initialValue != Hidden) {
"The initial value must not be set to Hidden if skipHiddenState is set to true."
}
}
}
/**
* The current value of the state.
*
* If no swipe or animation is in progress, this corresponds to the state the bottom sheet is
* currently in. If a swipe or an animation is in progress, this corresponds the state the sheet
* was in before the swipe or animation started.
*/
val currentValue: TangemSheetValue
get() = anchoredDraggableState.currentValue
/**
* The target value of the bottom sheet state.
*
* If a swipe is in progress, this is the value that the sheet would animate to if the swipe
* finishes. If an animation is running, this is the target value of that animation. Finally, if
* no swipe or animation is in progress, this is the same as the [currentValue].
*/
val targetValue: TangemSheetValue
get() = anchoredDraggableState.targetValue
/** Whether the modal bottom sheet is visible. */
val isVisible: Boolean
get() = anchoredDraggableState.currentValue != Hidden
/**
* Require the current offset (in pixels) of the bottom sheet.
*
* The offset will be initialized during the first measurement phase of the provided sheet
* content.
*
* These are the phases: Composition { -> Effects } -> Layout { Measurement -> Placement } ->
* Drawing
*
* During the first composition, an [IllegalStateException] is thrown. In subsequent
* compositions, the offset will be derived from the anchors of the previous pass. Always prefer
* accessing the offset from a LaunchedEffect as it will be scheduled to be executed the next
* frame, after layout.
*
* @throws IllegalStateException If the offset has not been initialized yet
*/
fun requireOffset(): Float = anchoredDraggableState.requireOffset()
/** Whether the sheet has an expanded state defined. */
val hasExpandedState: Boolean
get() = anchoredDraggableState.anchors.hasAnchorFor(Expanded)
/** Whether the modal bottom sheet has a partially expanded state defined. */
val hasPartiallyExpandedState: Boolean
get() = anchoredDraggableState.anchors.hasAnchorFor(PartiallyExpanded)
/**
* Fully expand the bottom sheet with animation and suspend until it is fully expanded or
* animation has been cancelled.
* *
*
* @throws [CancellationException] if the animation is interrupted
*/
suspend fun expand() {
anchoredDraggableState.animateTo(Expanded)
}
/**
* Animate the bottom sheet and suspend until it is partially expanded or animation has been
* cancelled.
*
* @throws [CancellationException] if the animation is interrupted
* @throws [IllegalStateException] if [skipPartiallyExpanded] is set to true
*/
suspend fun partialExpand() {
check(!skipPartiallyExpanded) {
"Attempted to animate to partial expanded when skipPartiallyExpanded was enabled. Set" +
" skipPartiallyExpanded to false to use this function."
}
animateTo(PartiallyExpanded)
}
/**
* Expand the bottom sheet with animation and suspend until it is [PartiallyExpanded] if defined
* else [Expanded].
*
* @throws [CancellationException] if the animation is interrupted
*/
suspend fun show() {
val targetValue =
when {
hasPartiallyExpandedState -> PartiallyExpanded
else -> Expanded
}
animateTo(targetValue)
}
/**
* Hide the bottom sheet with animation and suspend until it is fully hidden or animation has
* been cancelled.
*
* @throws [CancellationException] if the animation is interrupted
*/
suspend fun hide() {
check(!skipHiddenState) {
"Attempted to animate to hidden when skipHiddenState was enabled. Set skipHiddenState" +
" to false to use this function."
}
animateTo(Hidden)
}
/**
* Animate to a [targetValue]. If the [targetValue] is not in the set of anchors, the
* [currentValue] will be updated to the [targetValue] without updating the offset.
*
* @param targetValue The target value of the animation
* @throws CancellationException if the interaction interrupted by another interaction like a
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo]
* call.
*/
internal suspend fun animateTo(
targetValue: TangemSheetValue,
velocity: Float = anchoredDraggableState.lastVelocity,
) {
anchoredDraggableState.animateToWithDecay(targetValue, velocity)
}
/**
* Snap to a [targetValue] without any animation.
*
* @param targetValue The target value of the animation
* @throws CancellationException if the interaction interrupted by another interaction like a
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo]
* call.
*/
internal suspend fun snapTo(targetValue: TangemSheetValue) {
anchoredDraggableState.snapTo(targetValue)
}
/**
* Find the closest anchor taking into account the velocity and settle at it with an animation.
*/
internal suspend fun settle(velocity: Float) {
anchoredDraggableState.settle(velocity)
}
internal var anchoredDraggableState =
AnchoredDraggableState(
initialValue = initialValue,
snapAnimationSpec = BottomSheetAnimationSpec,
decayAnimationSpec = exponentialDecay(
frictionMultiplier = 10f,
absVelocityThreshold = 0.5f,
),
confirmValueChange = confirmValueChange,
positionalThreshold = { with(density) { 56.dp.toPx() } },
velocityThreshold = { with(density) { 125.dp.toPx() } },
)
internal val offset: Float
get() = anchoredDraggableState.offset
companion object {
/** The default [Saver] implementation for [TangemSheetState]. */
fun Saver(
skipPartiallyExpanded: Boolean,
confirmValueChange: (TangemSheetValue) -> Boolean,
density: Density,
skipHiddenState: Boolean,
) = Saver<TangemSheetState, TangemSheetValue>(
save = { it.currentValue },
restore = { savedValue ->
TangemSheetState(
skipPartiallyExpanded,
density,
savedValue,
confirmValueChange,
skipHiddenState,
)
},
)
}
}
/** Possible values of [TangemSheetState]. */
enum class TangemSheetValue {
/** The sheet is not visible. */
Hidden,
/** The sheet is visible at full height. */
Expanded,
/** The sheet is partially visible. */
PartiallyExpanded,
}
@OptIn(ExperimentalFoundationApi::class)
internal fun consumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
sheetState: TangemSheetState,
orientation: Orientation,
onFling: (velocity: Float) -> Unit,
): NestedScrollConnection = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
return if (delta < 0 && source == NestedScrollSource.UserInput) {
sheetState.anchoredDraggableState.dispatchRawDelta(delta).toOffset()
} else {
Offset.Zero
}
}
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
return if (source == NestedScrollSource.UserInput) {
sheetState.anchoredDraggableState.dispatchRawDelta(available.toFloat()).toOffset()
} else {
Offset.Zero
}
}
override suspend fun onPreFling(available: Velocity): Velocity {
val toFling = available.toFloat()
val currentOffset = sheetState.requireOffset()
val minAnchor = sheetState.anchoredDraggableState.anchors.minAnchor()
return if (toFling < 0 && currentOffset > minAnchor) {
onFling(toFling)
// since we go to the anchor with tween settling, consume all for the best UX
available
} else {
Velocity.Zero
}
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
onFling(available.toFloat())
return available
}
private fun Float.toOffset(): Offset = Offset(
x = if (orientation == Orientation.Horizontal) this else 0f,
y = if (orientation == Orientation.Vertical) this else 0f,
)
@JvmName("velocityToFloat")
private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y
@JvmName("offsetToFloat")
private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y
}
@Composable
internal fun rememberSheetState(
skipPartiallyExpanded: Boolean = false,
confirmValueChange: (TangemSheetValue) -> Boolean = { true },
initialValue: TangemSheetValue = Hidden,
skipHiddenState: Boolean = false,
): TangemSheetState {
val density = LocalDensity.current
return rememberSaveable(
skipPartiallyExpanded,
confirmValueChange,
skipHiddenState,
saver =
TangemSheetState.Saver(
skipPartiallyExpanded = skipPartiallyExpanded,
confirmValueChange = confirmValueChange,
density = density,
skipHiddenState = skipHiddenState,
),
) {
TangemSheetState(
skipPartiallyExpanded,
density,
initialValue,
confirmValueChange,
skipHiddenState,
)
}
}
/** The default animation spec used by [TangemSheetState]. */
private val BottomSheetAnimationSpec: AnimationSpec<Float> =
spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMedium,
)

View file

@ -0,0 +1,102 @@
package com.tangem.core.ui.reordarable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.zIndex
import org.burnoutcrew.reorderable.ReorderableState
/**
* Fixed version of ReorderableItem from reorderable library.
* The original version of ReorderableItem is somehow causing crashes.
* Probably because of compose compiler's bug and new foundation `animateItem` API.
* @see [org.burnoutcrew.reorderable.ReorderableItem]
*/
@Suppress("ComposableFunctionName")
@Composable
inline fun LazyItemScope.ReorderableItem(
reorderableState: ReorderableState<*>,
key: Any?,
modifier: Modifier = Modifier,
index: Int? = null,
orientationLocked: Boolean = true,
content: @Composable BoxScope.(isDragging: Boolean) -> Unit,
) = ReorderableItem(reorderableState, key, modifier, Modifier.animateItem(), orientationLocked, index, content)
/**
* Fixed version of ReorderableItem from reorderable library.
* The original version of ReorderableItem is somehow causing crashes.
* Probably because of compose compiler's bug and new foundation `animateItem` API.
* @see [org.burnoutcrew.reorderable.ReorderableItem]
*/
@Suppress("ComposableFunctionName")
@Composable
inline fun LazyGridItemScope.ReorderableItem(
reorderableState: ReorderableState<*>,
key: Any?,
modifier: Modifier = Modifier,
index: Int? = null,
content: @Composable BoxScope.(isDragging: Boolean) -> Unit,
) = ReorderableItem(reorderableState, key, modifier, Modifier.animateItem(), false, index, content)
/**
* Fixed version of ReorderableItem from reorderable library.
* The original version of ReorderableItem is somehow causing crashes.
* Probably because of compose compiler's bug and new foundation `animateItem` API.
* @see [org.burnoutcrew.reorderable.ReorderableItem]
*/
@Composable
inline fun ReorderableItem(
state: ReorderableState<*>,
key: Any?,
modifier: Modifier = Modifier,
defaultDraggingModifier: Modifier = Modifier,
orientationLocked: Boolean = true,
index: Int? = null,
content: @Composable BoxScope.(isDragging: Boolean) -> Unit,
) {
val isDragging = if (index != null) {
index == state.draggingItemIndex
} else {
key == state.draggingItemKey
}
val draggingModifier =
if (isDragging) {
Modifier
.zIndex(1f)
.graphicsLayer {
translationX = if (!orientationLocked || !state.isVerticalScroll) state.draggingItemLeft else 0f
translationY = if (!orientationLocked || state.isVerticalScroll) state.draggingItemTop else 0f
}
} else {
val cancel = if (index != null) {
index == state.dragCancelledAnimation.position?.index
} else {
key == state.dragCancelledAnimation.position?.key
}
if (cancel) {
Modifier.zIndex(1f)
.graphicsLayer {
translationX = if (!orientationLocked || !state.isVerticalScroll) {
state.dragCancelledAnimation.offset.x
} else {
0f
}
translationY = if (!orientationLocked || state.isVerticalScroll) {
state.dragCancelledAnimation.offset.y
} else {
0f
}
}
} else {
defaultDraggingModifier
}
}
Box(modifier = modifier.then(draggingModifier)) {
content(isDragging)
}
}

View file

@ -36,7 +36,6 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.disableNestedScroll
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.features.markets.details.impl.ui.components.InfoBottomSheet
import com.tangem.features.markets.details.impl.ui.components.MarketTokenDetailsChart
@ -105,7 +104,6 @@ private fun Content(
SpacerH4()
LazyColumn(
modifier = Modifier.disableNestedScroll(),
contentPadding = PaddingValues(bottom = bottomBarHeight),
) {
item("header") {

View file

@ -19,6 +19,7 @@ import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.progressbar.LinearProgressIndicator
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemAnimations
import com.tangem.core.ui.res.TangemTheme
@ -119,7 +120,7 @@ private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifi
.fillMaxWidth(),
progress = { animatedIndicatorFraction },
color = TangemTheme.colors.text.accent,
trackColor = TangemTheme.colors.background.tertiary,
backgroundColor = TangemTheme.colors.background.tertiary,
strokeCap = StrokeCap.Round,
)
Row(

View file

@ -87,7 +87,7 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing12,
bottom = 8.dp,
)
.onGloballyPositioned {
if (it.size.height > 0) {
@ -95,7 +95,8 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi
onHeaderSizeChange(it.size.height.toDp())
}
}
},
}
.padding(bottom = 4.dp),
state = state.searchBar,
)
Column(Modifier.padding(horizontal = TangemTheme.dimens.size16)) {

View file

@ -17,7 +17,6 @@ import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.disableNestedScroll
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
import kotlinx.coroutines.launch
@ -52,7 +51,7 @@ internal fun MarketsListLazyColumn(
if (state is ListUM.Loading) {
LazyColumn(
modifier = Modifier.disableNestedScroll(),
modifier = modifier,
state = rememberLazyListState(),
contentPadding = PaddingValues(bottom = bottomBarHeight),
userScrollEnabled = false,
@ -63,7 +62,7 @@ internal fun MarketsListLazyColumn(
}
} else {
LazyColumn(
modifier = modifier.disableNestedScroll(),
modifier = modifier,
state = lazyListState,
contentPadding = PaddingValues(bottom = bottomBarHeight),
userScrollEnabled = true,

View file

@ -1,9 +1,6 @@
package com.tangem.feature.referral.ui
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
@ -11,7 +8,6 @@ import androidx.compose.material3.SheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.feature.referral.models.ReferralStateHolder
@ -24,19 +20,18 @@ internal fun ReferralBottomSheet(
onDismissRequest: () -> Unit,
config: ReferralStateHolder.ReferralInfoState,
) {
val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
if (isVisible) {
ModalBottomSheet(
modifier = Modifier.height(LocalWindowSize.current.height - statusBarHeight),
modifier = Modifier.statusBarsPadding(),
onDismissRequest = onDismissRequest,
sheetState = sheetState,
shape = RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
),
windowInsets = WindowInsetsZero,
contentWindowInsets = { WindowInsetsZero },
containerColor = TangemTheme.colors.background.primary,
) {
AgreementBottomSheetContent(

View file

@ -15,8 +15,8 @@ import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@ -196,7 +196,7 @@ private fun BannerBlock(onClick: () -> Unit) {
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius14))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
indication = ripple(),
onClick = onClick,
),
) {
@ -258,7 +258,7 @@ private fun StakingRewardBlock(
.background(TangemTheme.colors.background.action)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
indication = ripple(),
enabled = rewardBlockType == RewardBlockType.Rewards,
onClick = onRewardsClick,
),
@ -286,7 +286,7 @@ private fun ActiveStakingBlock(
.background(TangemTheme.colors.background.action)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
indication = ripple(),
enabled = balance.isClickable,
onClick = {
onAnalytic()

View file

@ -5,7 +5,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@ -28,7 +28,7 @@ internal fun ValidatorBlock(validatorState: ValidatorState, onClick: () -> Unit)
.clickable(
enabled = validatorState.isClickable,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
indication = ripple(),
onClick = onClick,
),
) {

View file

@ -14,7 +14,7 @@ import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ripple
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -103,7 +103,7 @@ fun TransactionCard(
.height(TangemTheme.dimens.size116)
.width(TangemTheme.dimens.size102)
.clickable(
indication = rememberRipple(bounded = false),
indication = ripple(bounded = false),
interactionSource = remember { MutableInteractionSource() },
) { onChangeTokenClick() },
)
@ -164,7 +164,7 @@ fun TransactionCardEmpty(
.height(TangemTheme.dimens.size116)
.width(TangemTheme.dimens.size102)
.clickable(
indication = rememberRipple(bounded = false),
indication = ripple(bounded = false),
interactionSource = remember { MutableInteractionSource() },
) { onChangeTokenClick() },
)

View file

@ -5,9 +5,9 @@ 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.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@ -42,7 +42,7 @@ internal fun StakingBalanceBlock(
.background(TangemTheme.colors.background.primary)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
indication = ripple(),
onClick = state.onStakeClicked,
)
.padding(TangemTheme.dimens.spacing12),

View file

@ -34,6 +34,7 @@ import com.tangem.core.ui.components.buttons.actions.RoundedActionButton
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.reordarable.ReorderableItem
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.WindowInsetsZero
@ -43,7 +44,6 @@ import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkT
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
import org.burnoutcrew.reorderable.ReorderableItem
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
import org.burnoutcrew.reorderable.reorderable

View file

@ -19,6 +19,7 @@ import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.*
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -44,9 +45,7 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
@ -54,7 +53,7 @@ import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBot
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.components.sheetscaffold.*
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar
import com.tangem.core.ui.components.snackbar.TangemSnackbar
import com.tangem.core.ui.components.transactions.state.TxHistoryState
@ -146,7 +145,7 @@ private fun WalletContent(
val listState = rememberLazyListState()
val scaffoldContent: @Composable () -> Unit = {
val scaffoldContent: @Composable (PaddingValues?) -> Unit = { paddingValues ->
val movableItemModifier = Modifier.changeWalletAnimator(walletsListState)
val lazyTxHistoryItems = (selectedWallet as? TxHistoryStateHolder)?.let { walletState ->
@ -165,12 +164,20 @@ private fun WalletContent(
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val marketHintAproxHeight = with(LocalDensity.current) {
TangemTheme.typography.caption2.lineHeight.toDp() * 2
} + 40.dp
val contentPadding = paddingValues?.let {
PaddingValues(
bottom = it.calculateBottomPadding() + marketHintAproxHeight + 52.dp,
)
} ?: PaddingValues(bottom = TangemTheme.dimens.spacing92 + bottomBarHeight)
LazyColumn(
modifier = Modifier.testTag(TestTags.MAIN_SCREEN),
state = listState,
contentPadding = PaddingValues(
bottom = 184.dp + bottomBarHeight,
),
contentPadding = contentPadding,
horizontalAlignment = Alignment.CenterHorizontally,
) {
item(
@ -262,7 +269,7 @@ private fun WalletContent(
state = state,
selectedWallet = selectedWallet,
snackbarHostState = snackbarHostState,
content = scaffoldContent,
content = { scaffoldContent(null) },
)
}
}
@ -345,29 +352,13 @@ private inline fun BaseScaffoldWithMarkets(
crossinline bottomSheetContent: @Composable () -> Unit,
alertConfig: WalletAlertState?,
noinline onBottomSheetStateChange: (BottomSheetState) -> Unit,
crossinline content: @Composable () -> Unit,
crossinline content: @Composable (PaddingValues) -> Unit,
) {
// show the bottom sheet if there is at least one multicurrency wallet
val showManageTokensBottomSheet = remember(state.wallets) {
state.wallets.any { it is WalletState.MultiCurrency }
}
val bottomSheetState = rememberSheetStateEnhanced(
initialValue = if (showManageTokensBottomSheet) SheetValue.PartiallyExpanded else SheetValue.Hidden,
confirmValueChange = remember(showManageTokensBottomSheet) {
{ sheetValue ->
when {
sheetValue == SheetValue.Hidden && showManageTokensBottomSheet -> false
sheetValue != SheetValue.Hidden && !showManageTokensBottomSheet -> false
else -> true
}
}
},
skipHiddenState = showManageTokensBottomSheet,
)
val bottomSheetState = rememberTangemStandardBottomSheetState()
val keyboardShown = keyboardAsState()
val isKeyboardVisible by rememberIsKeyboardVisible()
val scaffoldState = rememberBottomSheetScaffoldState(
val scaffoldState = rememberTangemBottomSheetScaffoldState(
bottomSheetState = bottomSheetState,
snackbarHostState = snackbarHostState,
)
@ -398,13 +389,11 @@ private inline fun BaseScaffoldWithMarkets(
BottomSheetStateEffects(
bottomSheetState = bottomSheetState,
showManageTokensBottomSheet = showManageTokensBottomSheet,
alertConfig = alertConfig,
keyboardShown = keyboardShown,
onBottomSheetStateChange = onBottomSheetStateChange,
)
BottomSheetScaffold(
TangemBottomSheetScaffold(
snackbarHost = {
WalletSnackbarHost(
snackbarHostState = it,
@ -418,27 +407,26 @@ private inline fun BaseScaffoldWithMarkets(
sheetContainerColor = backgroundColor.value,
scaffoldState = scaffoldState,
sheetPeekHeight = peekHeight,
sheetDragHandle = {
Hand(modifier = Modifier.background(color = backgroundColor.value))
},
sheetTonalElevation = 8.dp,
sheetShadowElevation = 8.dp,
sheetShape = TangemTheme.shapes.bottomSheetLarge,
sheetContent = {
Box(
modifier = Modifier.sizeIn(maxHeight = maxHeight - statusBarHeight - handComposableComponentHeight),
) {
bottomSheetContent()
}
// hide bottom sheet when back pressed
BackHandler(
keyboardShown.value is Keyboard.Closed &&
bottomSheetState.currentValue == SheetValue.Expanded,
isKeyboardVisible.not() &&
bottomSheetState.currentValue == TangemSheetValue.Expanded,
) {
coroutineScope.launch { bottomSheetState.partialExpand() }
}
Column(
modifier = Modifier.sizeIn(maxHeight = maxHeight - statusBarHeight),
) {
Hand(Modifier.drawBehind { drawRect(backgroundColor.value) })
bottomSheetContent()
}
},
content = { _ ->
content = { paddingValues ->
val pullRefreshState = rememberPullRefreshState(
refreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
onRefresh = {
@ -456,13 +444,11 @@ private inline fun BaseScaffoldWithMarkets(
)
Column {
WalletTopBar(
config = state.topBarConfig,
)
WalletTopBar(config = state.topBarConfig)
Box(
modifier = Modifier.pullRefresh(pullRefreshState),
) {
content()
content(paddingValues)
WalletPullToRefreshIndicator(
isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
@ -474,7 +460,8 @@ private inline fun BaseScaffoldWithMarkets(
BottomSheetScrim(
color = BottomSheetDefaults.ScrimColor,
visible = bottomSheetState.targetValue == SheetValue.Expanded || state.showMarketsOnboarding,
visible = bottomSheetState.targetValue == TangemSheetValue.Expanded ||
state.showMarketsOnboarding,
onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } },
)
@ -492,7 +479,7 @@ private inline fun BaseScaffoldWithMarkets(
)
LaunchedEffect(state.showMarketsOnboarding, bottomSheetState.targetValue) {
if (state.showMarketsOnboarding && bottomSheetState.targetValue == SheetValue.Expanded) {
if (state.showMarketsOnboarding && bottomSheetState.targetValue == TangemSheetValue.Expanded) {
state.onDismissMarketsOnboarding()
}
}
@ -503,7 +490,7 @@ private inline fun BaseScaffoldWithMarkets(
@Composable
private fun MarketsTooltip(
availableHeight: Dp,
bottomSheetState: SheetState,
bottomSheetState: TangemSheetState,
isVisible: Boolean,
modifier: Modifier = Modifier,
) {
@ -532,7 +519,7 @@ private fun MarketsTooltip(
val slideOffset = 40.dp.toPx()
AnimatedVisibility(
modifier = modifier.absoluteOffset(y = tooltipOffset),
modifier = modifier.offset { IntOffset(x = 0, y = tooltipOffset.roundToPx()) },
visible = visible,
enter = slideIn(
animationSpec = spring(
@ -560,7 +547,7 @@ internal fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier) {
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "To add tokens pull this up or tap the search bar",
text = stringResource(R.string.markets_hint),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
@ -647,70 +634,46 @@ private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: (
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("CyclomaticComplexMethod", "MagicNumber", "LongMethod")
@Composable
private fun BottomSheetStateEffects(
bottomSheetState: SheetState,
showManageTokensBottomSheet: Boolean,
bottomSheetState: TangemSheetState,
alertConfig: WalletAlertState?,
keyboardShown: State<Keyboard>,
onBottomSheetStateChange: (BottomSheetState) -> Unit,
) {
// Bottom sheet during initialization internally expand partially after its content was remeasured,
// therefore initialValue = SheetValue.Hidden in rememberStandardBottomSheetState doesn't work as expected
// so we have to manually restrict expansion in this case
LaunchedEffect(bottomSheetState.targetValue, bottomSheetState.currentValue) {
if (!showManageTokensBottomSheet &&
(bottomSheetState.targetValue != SheetValue.Hidden || bottomSheetState.currentValue != SheetValue.Hidden)
) {
bottomSheetState.hide()
}
}
// react to changes in wallet list
LaunchedEffect(showManageTokensBottomSheet) {
when {
showManageTokensBottomSheet && bottomSheetState.currentValue != SheetValue.PartiallyExpanded -> {
bottomSheetState.partialExpand()
}
!showManageTokensBottomSheet && bottomSheetState.targetValue != SheetValue.Hidden -> {
bottomSheetState.hide()
}
}
}
val systemUiController = rememberSystemUiController()
val navigationBarColor = TangemTheme.colors.background.primary
LaunchedEffect(key1 = bottomSheetState.targetValue, navigationBarColor) {
when (bottomSheetState.targetValue) {
SheetValue.Hidden,
SheetValue.Expanded,
TangemSheetValue.Hidden,
TangemSheetValue.Expanded,
-> systemUiController.setNavigationBarColor(
color = Color.Transparent,
darkIcons = navigationBarColor.luminance() > 0.5f,
navigationBarContrastEnforced = true,
)
SheetValue.PartiallyExpanded,
TangemSheetValue.PartiallyExpanded,
-> systemUiController.setNavigationBarColor(navigationBarColor)
}
}
DisposableEffect(showManageTokensBottomSheet) {
// make navigation bar transparent when leaving the screen
DisposableEffect(Unit) {
onDispose {
if (showManageTokensBottomSheet) {
systemUiController.setNavigationBarColor(
color = Color.Transparent,
darkIcons = navigationBarColor.luminance() > 0.5f,
navigationBarContrastEnforced = false,
)
}
systemUiController.setNavigationBarColor(
color = Color.Transparent,
darkIcons = navigationBarColor.luminance() > 0.5f,
navigationBarContrastEnforced = false,
)
}
}
// expand bottom sheet when keyboard appears
LaunchedEffect(keyboardShown.value is Keyboard.Opened) {
if (keyboardShown.value is Keyboard.Opened && alertConfig == null) {
val isKeyboardVisible by rememberIsKeyboardVisible()
LaunchedEffect(isKeyboardVisible) {
if (isKeyboardVisible && alertConfig == null) {
bottomSheetState.expand()
}
}
@ -719,8 +682,8 @@ private fun BottomSheetStateEffects(
// hide keyboard when bottom sheet is about to be hidden
LaunchedEffect(Unit) {
snapshotFlow {
bottomSheetState.currentValue == SheetValue.Expanded &&
bottomSheetState.targetValue == SheetValue.PartiallyExpanded
bottomSheetState.currentValue == TangemSheetValue.Expanded &&
bottomSheetState.targetValue == TangemSheetValue.PartiallyExpanded
}.collect { sheetHasBeenHidden ->
if (sheetHasBeenHidden) {
keyboardController?.hide()
@ -728,7 +691,7 @@ private fun BottomSheetStateEffects(
}
}
val isSheetHidden = bottomSheetState.targetValue == SheetValue.PartiallyExpanded
val isSheetHidden = bottomSheetState.targetValue == TangemSheetValue.PartiallyExpanded
LaunchedEffect(isSheetHidden) {
onBottomSheetStateChange(
if (isSheetHidden) {
@ -740,30 +703,6 @@ private fun BottomSheetStateEffects(
}
}
/**
* Use a standard method when this is fixed https://issuetracker.google.com/issues/314796718
* Current material3 version: 1.2.0
*/
@Composable
@ExperimentalMaterial3Api
private fun rememberSheetStateEnhanced(
skipPartiallyExpanded: Boolean = false,
confirmValueChange: (SheetValue) -> Boolean = { true },
initialValue: SheetValue = SheetValue.Hidden,
skipHiddenState: Boolean = false,
): SheetState {
val density = LocalDensity.current
return remember(initialValue, skipPartiallyExpanded, confirmValueChange, skipHiddenState) {
SheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
density = density,
initialValue = initialValue,
confirmValueChange = confirmValueChange,
skipHiddenState = skipHiddenState,
)
}
}
@Composable
private fun WalletSnackbarHost(
snackbarHostState: SnackbarHostState,

View file

@ -26,10 +26,10 @@ androidxWindowManager = "1.3.0"
# region Compose
compose-compiler = "1.5.9"
compose-runtime = "1.6.1"
compose-foundation = "1.6.1"
compose-material = "1.6.4"
compose-material3 = "1.2.0"
compose-runtime = "1.7.1"
compose-foundation = "1.7.1"
compose-material = "1.7.1"
compose-material3 = "1.3.0"
compose-constraint = "1.0.1"
compose-navigation = "2.7.7"
compose-accompanist = "0.30.1"