diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5979dc8c9a..4a84a2b29f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,7 +55,7 @@ android:launchMode="singleTop" android:screenOrientation="portrait" android:theme="@style/SplashTheme" - android:windowSoftInputMode="adjustResize"> + android:windowSoftInputMode="adjustNothing"> diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt index 3bb17cddb9..f5b7759a82 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt @@ -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( diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index f10c86f973..52398bd178 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -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 }, ), ) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt index 21b38fdfb0..13e4a315e1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt @@ -104,13 +104,13 @@ data class TokenMarketInfoResponse( data class Links( @Json(name = "official_links") - val officialLinks: List?, + val officialLinks: List? = null, @Json(name = "social") - val social: List?, + val social: List? = null, @Json(name = "repository") - val repository: List?, + val repository: List? = null, @Json(name = "blockchain_site") - val blockchainSite: List?, + val blockchainSite: List? = null, ) data class Link( diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index b44332a6c6..3c8fb3e199 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -117,6 +117,7 @@ Importe En progreso Más tarde + %1$s quedan Bloqueado Red principal Tarifa de la red @@ -295,6 +296,7 @@ Ilimitado Ordenar Escanee + Esta información fue generada con IA Toque para cambiar la contraseña Toque para cambiar la contraseña Para crear la billetera, toque la tarjeta como se muestra arriba y no la retire hasta el final de la operación @@ -348,6 +350,7 @@ Mi portafolio Mercado Para generar direcciones para las redes seleccionadas, debe escanear su tarjeta Tangem + Para agregar tokens, abra esta página o toque la barra de búsqueda Los datos de este apartado proceden de las siguientes redes: %s No se pueden cargar los datos… Sin datos @@ -358,12 +361,12 @@ Sin resultado Seleccione una red Seleccione una billetera - 1 mes + 1 mo 1 año - 24 horas - 3 meses - 6 meses - 7 días + 24h + 3mos + 6mos + 7d Todo Compradores experimentados Evaluación @@ -395,7 +398,7 @@ El cambio en la cantidad de liquidez disponible para el token durante el período de tiempo especificado Índice de liquidez Bajo - Capitalización de mercado + Capital. de mercado El valor total de una criptomoneda calculado multiplicando su precio por la cantidad de monedas en circulación Evaluación de mercado Posición en la clasificación de criptomonedas entre todas las monedas según la capitalización de mercado @@ -412,6 +415,7 @@ 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. Tire hacia arriba o toque la barra de búsqueda para agregar tokens directamente desde el mercado Agregar tokens + NFC no está disponible en su dispositivo Deba configurar un único código de acceso para proteger todas sus tarjetas Proteger Puede configurar un código de acceso individual en cada tarjeta más tarde @@ -637,6 +641,7 @@ Esto eliminará la wallet de la aplicación. La wallet en sí puede\nañadirse de nuevo. Nombre El monto del staking debe ser al menos %s + El monto del staking se redondeará a %1$s TRX debido a las reglas de la red. Unstaking de la reclamación Porcentaje de rendimiento anual El porcentaje de rendimiento anual que podrá ganar como participante en el staking. @@ -660,6 +665,7 @@ El período que debes esperar después de solicitar el retiro de fondos del staking antes de que los tokens estén disponibles. Periodo de calentamiento El tiempo permitido para activar la participación en la apuesta. + Al utilizar la función de staking, usted acepta %1$s y %2$s del proveedor Bloqueado Migrar Native staking @@ -671,6 +677,7 @@ Gane recompensas por staking Sus fondos estarán disponibles para su uso después del período de desvinculación de %s. Ya puede retirar sus fondos, estarán disponibles para usar de inmediato. + Hacer staking en la red Tron con un nuevo validador transferirá automáticamente todos los fondos de staking a este validador. Listo para retirar Reunir Haga el staking de nuevo @@ -690,8 +697,11 @@ Recompensas El stake está bloqueado Hacer más staking + Toque para desbloquear + Toque para retirar Stake %s El unstaking de %s + ¡La transacción se está procesando! La validación está en curso en la cadena de bloques. Esto puede tardar unos minutos. Desunión Desbloquear Sin staking @@ -701,6 +711,7 @@ Votar Voto bloqueado Retirar + Sus stakes Almacene sus activos crypto de forma segura manteniendo las claves privadas contenidas en su tarjeta Billetera de hardware revolucionaria Hasta 3 tarjetas físicas por billetera diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index be874b7bdb..d2745aa359 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -117,6 +117,7 @@ Importez En cours Plus tard + Il reste %1$s Verrouillé Réseau principal Commissions du réseau @@ -295,6 +296,7 @@ Illimité Commandez Scannez + Ces informations ont été générées avec l\'IA Touchez, pour modifier le code d\'accès Touchez, pour modifier le mot de passe 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 @@ -348,6 +350,7 @@ Mon portfolio Marché Pour générer des adresses pour les réseaux sélectionnés, vous devez scanner votre carte Tangem + Pour ajouter des jetons, faites-le apparaître ou appuyez sur la barre de recherche Les données de cette section proviennent des réseaux suivants : %s Impossible de charger les données… Aucune donnée @@ -358,11 +361,11 @@ Aucun résultat Sélectionnez un réseau Sélectionnez un portefeuille - 1 mois + 1 mo 1 an - 24 heures - 3 mois - 6 mois + 24h + 3mos + 6mos 7j Tout Acheteurs expérimentés @@ -395,8 +398,8 @@ Le changement dans la quantité de liquidité disponible pour le jeton pendant la période spécifiée Indice de liquidité Faible - Capitalisation boursière - La valeur totale d\'une crypto-monnaie calculée en multipliant son prix actuel par le nbre de pièces en circulation + Cap. boursière + La valeur d\'une crypto-monnaie est calculée en multipliant son prix par le nombre de pièces en circulation Évaluation du marché Position dans le classement des crypto-monnaies entre toutes les pièces en fonction de la capitalisation boursière Approvisionnement maximal @@ -412,6 +415,7 @@ 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é Tirez vers le haut ou appuyez sur la barre de recherche pour ajouter des jetons directement depuis le marché Ajouter des jetons + NFC n\'est pas disponible sur votre appareil Vous devez définir un seul code d\'accès pour protéger toutes vos cartes Protéger Vous pourrez définir un code d\'accès individuel sur chaque carte plus tard @@ -637,6 +641,7 @@ Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau. Nom Le montant à staker doit être au moins %s + Le montant du staking sera arrondi à %1$s TRX en raison des règles du réseau. Réclamation déstakée Pourcentage de rendement annuel Le pourcentage de rendement annuel que vous pouvez gagner en participant au staking. @@ -660,6 +665,7 @@ La période que vous devez attendre après avoir demandé le retrait des fonds du staking avant que les jetons ne soient disponibles. Période d\'échauffement Le temps imparti pour activer la participation au staking. + En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur Bloqué Migrer Native staking @@ -671,6 +677,7 @@ Gagnez des récompenses de staking Vos fonds seront disponibles pour utilisation après la période de désengagement %s. Vous pouvez désormais retirer vos fonds, ils seront disponibles à l\'utilisation immédiatement + 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 Prêt à retirer Réassocier Restakez @@ -690,8 +697,11 @@ Récompenses Stake verrouillé Staker plus + Appuyez pour déverrouiller + Appuyez pour retirer Stake %s déstaker %s + La transaction est en cours de traitement ! La validation est actuellement en cours dans la blockchain. Cela peut prendre quelques minutes. Dissociation Débloquer Non-staké @@ -701,6 +711,7 @@ Voter Vote bloqué Retirer + Vos stakes Stockez vos actifs crypto en toute sécurité tout en conservant les clés privées contenues dans votre carte Portefeuille matériel révolutionnaire Jusqu\'à 3 cartes physiques pour un portefeuille diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 9c10dc67c2..61aa69c13e 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -358,13 +358,13 @@ 検索結果はありません ネットワークを選択 ウォレットを選択 - 1ヶ月 - 1年 - 24時間 - 3ヶ月 - 6ヶ月 - 7日 - すべて + 1m + 1y + 24h + 3m + 6m + 7d + 全部 経験豊富な買い手 格付け 並べ替え @@ -637,6 +637,7 @@ これにより、ウォレットがアプリから削除されます。ウォレットは再度追加できます。 名前 ステーキング金額は %s 以上である必要があります + ネットワークルールにより、ステーキング金額は%1$s TRX に切り上げられます。 ステーキング解除分を請求する 年率 ステーキングに参加することで得られる年間収益率。 diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt index 72a9f3a11e..74029fd218 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt @@ -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 { 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 { + val keyboard by keyboardAsState() + return remember(keyboard) { + derivedStateOf { + keyboard.isOpened + } + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt index 7b3c924e95..972f347106 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt @@ -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, ), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt index 777062e8bd..31d56ce3c5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt @@ -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, ), ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt index 6ef3e53d1f..d9b884cb75 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt @@ -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 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( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt index 0a17c85b13..ef0308abd3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt @@ -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, ), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt index 3ef8fefbc8..cf5d0d6153 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt @@ -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() }, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt index 6a1b07ab53..6b8f360c17 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt @@ -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() }, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt index a4759a11e2..1e15b0b4dd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt @@ -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() }, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt index d816d77bf6..90abca3970 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt @@ -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() }, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt index e1aa081b50..6fcc6ef997 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt @@ -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), ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt index af8a6761b3..79c75c96e6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt @@ -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("") }, ), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt index 13a8aac488..a6031f054b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt @@ -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() }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt index 19d96e409b..2888045227 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt @@ -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, ), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/progressbar/LinearProgressIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/components/progressbar/LinearProgressIndicator.kt new file mode 100644 index 0000000000..41d6020243 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/progressbar/LinearProgressIndicator.kt @@ -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) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt index df80c7d5b3..79b1cdb350 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt @@ -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), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/DraggableAnchorsUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/DraggableAnchorsUtils.kt new file mode 100644 index 0000000000..f40ba67a78 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/DraggableAnchorsUtils.kt @@ -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 Modifier.draggableAnchors( + state: AnchoredDraggableState, + orientation: Orientation, + anchors: (size: IntSize, constraints: Constraints) -> Pair, T>, +) = this then DraggableAnchorsElement(state, anchors, orientation) + +@OptIn(ExperimentalFoundationApi::class) +private class DraggableAnchorsElement( + private val state: AnchoredDraggableState, + private val anchors: (size: IntSize, constraints: Constraints) -> Pair, T>, + private val orientation: Orientation, +) : ModifierNodeElement>() { + + @OptIn(ExperimentalFoundationApi::class) + override fun create() = DraggableAnchorsNode(state, anchors, orientation) + + override fun update(node: DraggableAnchorsNode) { + 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( + var state: AnchoredDraggableState, + var anchors: (size: IntSize, constraints: Constraints) -> Pair, 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()) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemBottomSheetScaffold.kt b/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemBottomSheetScaffold.kt new file mode 100644 index 0000000000..aa68f71190 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemBottomSheetScaffold.kt @@ -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 + +/** + * Material Design standard bottom sheet scaffold. + * + * Standard bottom sheets co-exist with the screen’s 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) } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemSheetState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemSheetState.kt new file mode 100644 index 0000000000..64dcea2dcb --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemSheetState.kt @@ -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( + 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 = + spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessMedium, + ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/reordarable/ReorderableItem.kt b/core/ui/src/main/java/com/tangem/core/ui/reordarable/ReorderableItem.kt new file mode 100644 index 0000000000..7df813a2e5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/reordarable/ReorderableItem.kt @@ -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) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt index 275a6427e7..bce07d98c0 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt @@ -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") { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt index 4e947ff908..e1b5444e2e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt @@ -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( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index 375d444943..9c52cb6bdb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -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)) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt index 2674b90648..5d70d6e85e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt @@ -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, diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt index 678775213a..0a3d43118a 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt @@ -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( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index b9b3c42672..6fa945af4f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -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() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt index abb57b51a7..be7909007e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt @@ -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, ), ) { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 9baf09e90a..e6866c7ba9 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -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() }, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt index 50ef80a078..1107eba6ae 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt @@ -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), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index b7df4b8f3e..17dca9d4fe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 106556de2d..fc8d3b6572 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -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, 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, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index aa6d57aaf2..7dcb445a2f 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -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"