Updated on 2026-08-14

This commit is contained in:
Tangem 2022-06-08 04:07:06 +03:00
parent 691fd5bab6
commit 0df972955a
27 changed files with 1013 additions and 365 deletions

@ -1 +1 @@
Subproject commit 7f058ec409b4eaaf8688ecd4bf944ef8d58d3564
Subproject commit 64ae04d2086e5a605dd4da3cba4af32a60d46c79

View file

@ -0,0 +1,14 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
/**
[REDACTED_AUTHOR]
*/
@Composable
fun Dp.toPx(): Float {
val currentDp = this
return with(LocalDensity.current) { currentDp.toPx() }
}

View file

@ -93,32 +93,39 @@ fun View.invisible(invisible: Boolean = true, invokeBeforeStateChanged: (() -> U
}
fun Context.dpToPixels(dp: Int): Int =
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
).toInt()
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
).toInt()
fun Context.pixelsToDp(pixels: Int): Int {
return (pixels.toFloat() /
(resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
(resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
.toInt()
}
fun Context.dpToPixels(dp: Float): Float =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, this.resources.displayMetrics)
fun Context.pixelsToDp(pixels: Float): Float =
(pixels / (resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
tailrec fun Context?.getActivity(): Activity? = this as? Activity
?: (this as? ContextWrapper)?.baseContext?.getActivity()
?: (this as? ContextWrapper)?.baseContext?.getActivity()
fun MaterialCardView.setMargins(
marginLeftDp: Int = 16,
marginTopDp: Int = 8,
marginRightDp: Int = 16,
marginBottomDp: Int = 8
marginLeftDp: Int = 16,
marginTopDp: Int = 8,
marginRightDp: Int = 16,
marginBottomDp: Int = 8
) {
val params = this.layoutParams
(params as ViewGroup.MarginLayoutParams).setMargins(
context.dpToPixels(marginLeftDp),
context.dpToPixels(marginTopDp),
context.dpToPixels(marginRightDp),
context.dpToPixels(marginBottomDp)
context.dpToPixels(marginLeftDp),
context.dpToPixels(marginTopDp),
context.dpToPixels(marginRightDp),
context.dpToPixels(marginBottomDp)
)
this.layoutParams = params
}
@ -128,11 +135,11 @@ fun Activity.setSystemBarTextColor(setTextDark: Boolean) {
val flags = this.window.decorView.systemUiVisibility
// Update the SystemUiVisibility dependening on whether we want a Light or Dark theme.
this.window.decorView.systemUiVisibility =
if (setTextDark) {
flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
} else {
flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
if (setTextDark) {
flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
} else {
flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
}
}
@ -150,7 +157,7 @@ fun Context.copyToClipboard(value: Any, label: String = "") {
fun Context.getFromClipboard(default: CharSequence? = null): CharSequence? {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
?: return default
?: return default
val clipData = clipboard.primaryClip ?: return default
if (clipData.itemCount == 0) return default
@ -172,10 +179,10 @@ fun Fragment.shareText(text: String) {
}
fun Context.safeStartActivity(
intent: Intent,
options: Bundle? = null,
fallback: ((ActivityNotFoundException) -> Unit)? = null,
finally: VoidCallback? = null
intent: Intent,
options: Bundle? = null,
fallback: ((ActivityNotFoundException) -> Unit)? = null,
finally: VoidCallback? = null
) {
try {
this.startActivity(intent, options)

View file

@ -1,153 +0,0 @@
package com.tangem.tap.features.home.compose
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.scaleIn
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.wallet.R
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun FirstStoriesContent(
paused: Boolean, duration: Int = 8_000,
hideContent: (Boolean) -> Unit
) {
val screenState = remember { mutableStateOf(StartingScreenState.INIT) }
val progress = remember { Animatable(0f) }
LaunchedEffect(paused) {
if (paused) {
progress.stop()
} else {
progress.animateTo(
targetValue = 2f,
animationSpec = tween(
durationMillis = duration,
easing = LinearEasing
)
)
}
}
when (progress.value) {
in 0f..0.2f -> screenState.value = StartingScreenState.INIT
in 0.2f..0.3f -> screenState.value = StartingScreenState.BUY
in 0.3f..0.4f -> screenState.value = StartingScreenState.STORE
in 0.4f..0.5f -> screenState.value = StartingScreenState.SEND
in 0.5f..0.6f -> screenState.value = StartingScreenState.PAY
in 0.6f..0.7f -> screenState.value = StartingScreenState.EXCHANGE
in 0.7f..0.8f -> screenState.value = StartingScreenState.BORROW
in 0.8f..1f -> screenState.value = StartingScreenState.LEND
in 1f..1.2f -> screenState.value = StartingScreenState.SHOW_CARD
in 1.2f..2f -> screenState.value = StartingScreenState.MEET_TANGEM
}
if (screenState.value == StartingScreenState.INIT) hideContent(true)
if (screenState.value == StartingScreenState.BUY) hideContent(false)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier =
Modifier
.fillMaxSize(),
) {
val text = when (screenState.value) {
StartingScreenState.INIT -> null
StartingScreenState.BUY -> R.string.story_meet_buy
StartingScreenState.STORE -> R.string.story_meet_store
StartingScreenState.SEND -> R.string.story_meet_send
StartingScreenState.PAY -> R.string.story_meet_pay
StartingScreenState.EXCHANGE -> R.string.story_meet_exchange
StartingScreenState.BORROW -> R.string.story_meet_borrow
StartingScreenState.LEND -> R.string.story_meet_lend
StartingScreenState.SHOW_CARD -> R.string.story_meet_title
StartingScreenState.MEET_TANGEM -> R.string.story_meet_title
}
val style = TextStyle(
fontSize = 60.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White,
textAlign = TextAlign.Center,
)
if (screenState.value != StartingScreenState.MEET_TANGEM) {
TextAutoSize(
modifier = Modifier
.padding(start = 20.dp, end = 20.dp, bottom = 100.dp)
.alpha(if (screenState.value == StartingScreenState.SHOW_CARD) 0f else 1f),
text = text?.let { stringResource(text) } ?: "",
textStyle = style,
fontSizeRange = FontSizeRange(20.sp, 60.sp)
)
}
AnimatedVisibility(
visible = screenState.value == StartingScreenState.MEET_TANGEM,
enter = slideInVertically() { it / 2 }
) {
TextAutoSize(
modifier = Modifier
.fillMaxWidth()
.padding(start = 40.dp, end = 40.dp),
text = text?.let { stringResource(text) } ?: "",
textStyle = style,
fontSizeRange = FontSizeRange(20.sp, 60.sp)
)
}
AnimatedVisibility(
visible = screenState.value == StartingScreenState.SHOW_CARD ||
screenState.value == StartingScreenState.MEET_TANGEM,
enter = scaleIn(initialScale = 3f)
) {
Image(
painter = painterResource(
id = R.drawable.meet_tangem
),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
}
}
}
enum class StartingScreenState {
INIT, BUY, STORE, SEND, PAY, EXCHANGE, BORROW, LEND, SHOW_CARD, MEET_TANGEM
}
@Preview
@Composable
fun Stories1Preview() {
FirstStoriesContent(false, 7_000) {}
}

View file

@ -22,10 +22,12 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.SpacerS24
import androidx.compose.ui.unit.sp
import com.tangem.tap.common.compose.SpacerS24
import com.tangem.tap.features.home.compose.content.*
import com.tangem.tap.features.home.compose.views.HomeButtons
import com.tangem.tap.features.home.compose.views.StoriesProgressBar
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.wallet.R
@ -39,8 +41,6 @@ fun StoriesScreen(
onSearchTokensClick: () -> Unit,
) {
val steps = 6
val stepDuration = 8_000
val currentStep = remember { mutableStateOf(1) }
val isDarkBackground = currentStep.value !in 3..5
@ -55,7 +55,7 @@ fun StoriesScreen(
val isPressed = remember { mutableStateOf(false) }
val needsToBePaused =
remember(homeState) { mutableStateOf(homeState.value.btnScanState.progressState == ProgressState.Loading) }
val pause = isPressed.value || needsToBePaused.value
val isPaused = isPressed.value || needsToBePaused.value
val hideContent = remember { mutableStateOf(true) }
@ -122,9 +122,8 @@ fun StoriesScreen(
StoriesProgressBar(
steps = steps,
currentStep = currentStep.value,
// paused = isPressed.value,
stepDuration = stepDuration,
paused = pause,
stepDuration = currentStep.duration(),
paused = isPaused,
onStepFinished = goToNextScreen,
)
Image(
@ -139,12 +138,12 @@ fun StoriesScreen(
colorFilter = if (isDarkBackground) null else ColorFilter.tint(Color.Black)
)
when (currentStep.value) {
1 -> FirstStoriesContent(pause, stepDuration) { hideContent.value = it }
2 -> StoriesRevolutionaryWallet()
3 -> StoriesUltraSecureBackup()
4 -> StoriesThousandsOfCurrencies()
5 -> StoriesWeb3()
6 -> StoriesWalletForEveryone()
1 -> FirstStoriesContent(isPaused, currentStep.duration()) { hideContent.value = it }
2 -> StoriesRevolutionaryWallet(currentStep.duration())
3 -> StoriesUltraSecureBackup(isPaused, currentStep.duration())
4 -> StoriesCurrencies(isPaused, currentStep.duration())
5 -> StoriesWeb3(isPaused, currentStep.duration())
6 -> StoriesWalletForEveryone(currentStep.duration())
}
}
Column(
@ -157,8 +156,7 @@ fun StoriesScreen(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.height(48.dp)
,
.height(48.dp),
colors = ButtonDefaults.textButtonColors(
backgroundColor = Color.White,
contentColor = Color(0xFF080C10)
@ -188,9 +186,7 @@ fun StoriesScreen(
}
}
@Preview
@Composable
fun InstagramScreenPreview() {
StoriesScreen(onScanButtonClick = {}, onShopButtonClick = {}, onSearchTokensClick = {})
private fun MutableState<Int>.duration(): Int = when (this.value) {
1 -> 8000
else -> 6000
}

View file

@ -1,162 +0,0 @@
package com.tangem.tap.features.home.compose
import android.content.Context
import android.graphics.Typeface
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tap.common.compose.SpacerS16
import com.tangem.tap.common.compose.SpacerS24
import com.tangem.tap.common.compose.extensions.toAndroidGraphicsColor
import com.tangem.wallet.R
@Composable
fun StoriesGeneralContent(
titleText: String,
subtitleText: String,
imageSource: Int?,
isDarkBackground: Boolean,
subtitleTextId: Int? = null,
imageComposable: (() -> Unit)? = null,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier =
Modifier
.fillMaxWidth()
.fillMaxHeight(),
) {
Text(
text = titleText,
fontSize = 32.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier
.padding(start = 40.dp, end = 40.dp),
color = if (isDarkBackground) Color.White else Color(0xFF090E13),
textAlign = TextAlign.Center
)
SpacerS16()
SubtitleText(subtitleText, subtitleTextId)
SpacerS24()
if (imageSource != null) {
Image(
painter = painterResource(id = imageSource),
contentDescription = null,
contentScale = if (isDarkBackground) ContentScale.Inside else ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
}
imageComposable?.invoke()
}
}
@Composable
fun SubtitleText(subtitleText: String, subtitleTextId: Int?) {
val color = Color(0xFFA6AAAD)
if (subtitleTextId == null) {
Text(
text = subtitleText,
fontSize = 20.sp,
fontWeight = FontWeight.Normal,
modifier = Modifier.padding(start = 40.dp, end = 40.dp),
color = color,
textAlign = TextAlign.Center
)
} else {
HtmlText(subtitleTextId) { context ->
val padding = context.dpToPx(40f).toInt()
TextView(context).apply {
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
setPadding(padding, 0, padding, 0)
textAlignment = View.TEXT_ALIGNMENT_CENTER
typeface = Typeface.DEFAULT
setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
setTextColor(color.toAndroidGraphicsColor())
}
}
}
}
@Composable
fun StoriesRevolutionaryWallet() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_awe_title),
subtitleText = stringResource(id = R.string.story_awe_description),
imageSource = R.drawable.revolutionary_wallet,
isDarkBackground = true
)
}
@Composable
fun StoriesUltraSecureBackup() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_backup_title),
subtitleText = stringResource(id = R.string.story_backup_description),
imageSource = R.drawable.floating_cards,
subtitleTextId = R.string.story_backup_description,
isDarkBackground = false
)
}
@Composable
fun StoriesThousandsOfCurrencies() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_currencies_title),
subtitleText = stringResource(id = R.string.story_currencies_description),
imageSource = R.drawable.thousands_of_currencies,
isDarkBackground = false
)
}
@Composable
fun StoriesWeb3() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_web3_title),
subtitleText = stringResource(id = R.string.story_web3_description),
imageSource = R.drawable.web_3,
isDarkBackground = false
)
}
@Composable
fun StoriesWalletForEveryone() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_finish_title),
subtitleText = stringResource(id = R.string.story_finish_description),
imageSource = R.drawable.wallet_for_everyone,
isDarkBackground = true
)
}
@Composable
fun HtmlText(
stringResId: Int,
modifier: Modifier = Modifier,
factory: (Context) -> TextView
) {
AndroidView(factory, modifier) { it.text = it.context.getText(stringResId) }
}

View file

@ -0,0 +1,251 @@
package com.tangem.tap.features.home.compose.content
import android.content.Context
import android.graphics.Typeface
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tap.common.compose.SpacerH
import com.tangem.tap.common.compose.SpacerH16
import com.tangem.tap.common.compose.extensions.toAndroidGraphicsColor
import com.tangem.tap.features.home.compose.uiTools.StoriesBottomImageAnimation
import com.tangem.tap.features.home.compose.uiTools.StoriesTextAnimation
import com.tangem.wallet.R
@Composable
fun StoriesRevolutionaryWallet(stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_awe_title),
subtitleText = stringResource(id = R.string.story_awe_description),
isDarkBackground = true,
)
},
bottomContent = {
StoriesBottomImageAnimation(
totalDuration = stepDuration,
firstStepDuration = 300,
) { modifier ->
StoriesImage(
modifier = modifier,
drawableResId = R.drawable.revolutionary_wallet,
isDarkBackground = true,
)
}
}
)
}
@Composable
fun StoriesUltraSecureBackup(isPaused: Boolean, stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_backup_title),
subtitleText = stringResource(id = R.string.story_backup_description),
isDarkBackground = false,
)
},
bottomContent = {
FloatingCardsContent(isPaused, stepDuration)
}
)
}
@Composable
fun StoriesCurrencies(isPaused: Boolean, stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_currencies_title),
subtitleText = stringResource(id = R.string.story_currencies_description),
isDarkBackground = false,
)
},
bottomContent = {
StoriesCurrenciesContent(paused = isPaused, duration = stepDuration)
}
)
}
@Composable
fun StoriesWeb3(isPaused: Boolean, stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_web3_title),
subtitleText = stringResource(id = R.string.story_web3_description),
isDarkBackground = false,
)
},
bottomContent = {
StoriesWeb3Content(paused = isPaused, duration = stepDuration)
}
)
}
@Composable
fun StoriesWalletForEveryone(stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_finish_title),
subtitleText = stringResource(id = R.string.story_finish_description),
isDarkBackground = true,
)
},
bottomContent = {
StoriesBottomImageAnimation(
totalDuration = stepDuration,
firstStepDuration = 500,
) { modifier ->
StoriesImage(
modifier = modifier,
drawableResId = R.drawable.wallet_for_everyone,
isDarkBackground = true,
)
}
}
)
}
@Composable
private fun SplitContent(
topContent: @Composable () -> Unit,
bottomContent: @Composable () -> Unit,
) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top
) {
topContent()
bottomContent()
}
}
@Composable
private fun TopContent(
titleText: String,
subtitleText: String,
isDarkBackground: Boolean,
subtitleTextId: Int? = null,
) {
SpacerH(32.dp)
StoriesTitleText(
text = titleText,
isDarkBackground = isDarkBackground,
)
SpacerH16()
StoriesSubtitleText(
subtitleText = subtitleText,
subtitleTextId = subtitleTextId,
)
SpacerH(32.dp)
}
@Composable
private fun StoriesTitleText(
text: String,
isDarkBackground: Boolean,
) {
StoriesTextAnimation(
slideInDuration = 500,
slideInDelay = 150,
) { modifier ->
Text(
modifier = modifier
.padding(start = 40.dp, end = 40.dp),
text = text,
fontSize = 32.sp,
fontWeight = FontWeight.SemiBold,
color = if (isDarkBackground) Color.White else Color(0xFF090E13),
textAlign = TextAlign.Center
)
}
}
@Composable
private fun StoriesSubtitleText(
subtitleText: String,
subtitleTextId: Int?,
) {
val color = Color(0xFFA6AAAD)
StoriesTextAnimation(
slideInDuration = 500,
slideInDelay = 400,
) { modifier ->
val internalModifier = modifier
.padding(start = 40.dp, end = 40.dp)
if (subtitleTextId == null) {
Text(
text = subtitleText,
fontSize = 20.sp,
fontWeight = FontWeight.Normal,
modifier = internalModifier,
color = color,
textAlign = TextAlign.Center
)
} else {
HtmlText(
modifier = internalModifier,
stringResId = subtitleTextId,
) { context ->
val padding = context.dpToPx(40f).toInt()
TextView(context).apply {
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
setPadding(padding, 0, padding, 0)
textAlignment = View.TEXT_ALIGNMENT_CENTER
typeface = Typeface.DEFAULT
setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
setTextColor(color.toAndroidGraphicsColor())
}
}
}
}
}
@Composable
private fun HtmlText(
stringResId: Int,
modifier: Modifier = Modifier,
factory: (Context) -> TextView
) {
AndroidView(factory, modifier) { it.text = it.context.getText(stringResId) }
}
@Composable
fun StoriesImage(
modifier: Modifier = Modifier,
@DrawableRes drawableResId: Int,
isDarkBackground: Boolean,
) {
Image(
painter = painterResource(id = drawableResId),
contentDescription = null,
contentScale = if (isDarkBackground) ContentScale.Inside else ContentScale.FillWidth,
modifier = modifier.fillMaxWidth()
)
}

View file

@ -0,0 +1,161 @@
package com.tangem.tap.features.home.compose.content
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tangem_sdk_new.extensions.pxToDp
import com.tangem.tap.common.compose.SpacerH
import com.tangem.tap.common.compose.extensions.toPx
import com.tangem.tap.common.extensions.isEven
import com.tangem.tap.features.home.compose.uiTools.HorizontalSlidingImage
import com.tangem.wallet.R
@Composable
fun StoriesCurrenciesContent(
paused: Boolean,
duration: Int,
) {
val currencyDrawableList = remember {
listOf(
R.drawable.currency0,
R.drawable.currency1,
R.drawable.currency2,
R.drawable.currency3,
R.drawable.currency4,
)
}
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val decreaseRate = remember { 1f / currencyDrawableList.size }
val designItemHeight = remember { 82.dp }
LightenBox {
Column(modifier = Modifier.graphicsLayer(clip = false)) {
currencyDrawableList.forEachIndexed { index, drawableResId ->
val painter = painterResource(id = drawableResId)
val scaledItemSize = scaleToDesignSize(painter.dpSize(), designItemHeight = designItemHeight)
val itemOversizedScreenWidthBy = scaledItemSize.width - screenWidth
val moveItemToStartOfScreen = itemOversizedScreenWidthBy / 2
val chessOffset = if (index.isEven()) 0.dp else scaledItemSize.halfHeight()
val animateFrom = chessOffset - moveItemToStartOfScreen
val animateTo = 50.dp - (50.dp * index * decreaseRate)
HorizontalSlidingImage(
paused = paused,
duration = duration,
painter = painter,
itemSize = scaledItemSize,
startOffset = animateFrom.toPx(),
targetOffset = animateTo.toPx(),
contentDescription = "Currency row",
)
SpacerH(12.dp)
}
}
}
}
@Composable
fun StoriesWeb3Content(
paused: Boolean,
duration: Int,
) {
val dappsItemList = remember {
listOf(
R.drawable.dapps0,
R.drawable.dapps1,
R.drawable.dapps2,
R.drawable.dapps3,
R.drawable.dapps4,
R.drawable.dapps5,
)
}
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val decreaseRate = remember { 1f / dappsItemList.size }
val designItemHeight = 50.dp
LightenBox {
Column(modifier = Modifier.graphicsLayer(clip = false)) {
dappsItemList.forEachIndexed { index, drawableResId ->
val painter = painterResource(id = drawableResId)
val scaledItemSize = scaleToDesignSize(painter.dpSize(), designItemHeight = designItemHeight)
val itemOversizedScreenWidthBy = scaledItemSize.width - screenWidth
val moveItemToStartOfScreen = itemOversizedScreenWidthBy / 2
val chessOffset = if (index.isEven()) 0.dp else scaledItemSize.width / 3
val animateFrom = chessOffset - moveItemToStartOfScreen
val animateTo = 90.dp - (90.dp * index * decreaseRate)
HorizontalSlidingImage(
paused = paused,
duration = duration,
painter = painter,
itemSize = scaledItemSize,
startOffset = animateFrom.toPx(),
targetOffset = animateTo.toPx(),
contentDescription = "Web3 row",
)
SpacerH(14.dp)
}
}
}
}
@Composable
private fun LightenBox(content: @Composable () -> Unit) {
Box() {
content()
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 170.dp)
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color.White.copy(alpha = 0.3f),
Color.White
)
)
)
) {}
}
}
@Composable
private fun Painter.dpSize(): DpSize = DpSize(
intrinsicSize.width.pxToDp().dp,
intrinsicSize.height.pxToDp().dp,
)
@Composable
private fun Float.dpToPx(): Float = LocalContext.current.dpToPx(this)
@Composable
private fun Float.pxToDp(): Float = LocalContext.current.pxToDp(this)
private fun scaleToDesignSize(itemSize: DpSize, designItemHeight: Dp): DpSize {
val scaleRate = itemSize.height / designItemHeight
return itemSize / scaleRate
}
private fun DpSize.halfWidth(): Dp = this.width / 2
private fun DpSize.halfHeight(): Dp = this.height / 2

View file

@ -0,0 +1,161 @@
package com.tangem.tap.features.home.compose.content
import androidx.annotation.StringRes
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.tap.features.home.compose.uiTools.StoriesBottomImageAnimation
import com.tangem.tap.features.home.compose.uiTools.StoriesTextAnimation
import com.tangem.tap.features.home.compose.views.FontSizeRange
import com.tangem.tap.features.home.compose.views.TextAutoSize
import com.tangem.wallet.R
@Composable
fun FirstStoriesContent(
isPaused: Boolean, duration: Int = 8_000,
hideContent: (Boolean) -> Unit
) {
val screenState = remember { mutableStateOf(StartingScreenState.INIT) }
val progress = remember { Animatable(0f) }
LaunchedEffect(isPaused) {
if (isPaused) {
progress.stop()
} else {
progress.animateTo(
targetValue = 2f,
animationSpec = tween(
durationMillis = duration,
easing = LinearEasing
)
)
}
}
when (progress.value) {
in 0f..0.2f -> screenState.value = StartingScreenState.INIT
in 0.2f..0.3f -> screenState.value = StartingScreenState.BUY
in 0.3f..0.4f -> screenState.value = StartingScreenState.STORE
in 0.4f..0.5f -> screenState.value = StartingScreenState.SEND
in 0.5f..0.6f -> screenState.value = StartingScreenState.PAY
in 0.6f..0.7f -> screenState.value = StartingScreenState.EXCHANGE
in 0.7f..0.8f -> screenState.value = StartingScreenState.BORROW
in 0.8f..1f -> screenState.value = StartingScreenState.LEND
in 1f..1.2f -> screenState.value = StartingScreenState.SHOW_CARD
in 1.2f..2f -> screenState.value = StartingScreenState.MEET_TANGEM
}
if (screenState.value == StartingScreenState.INIT) hideContent(true)
if (screenState.value == StartingScreenState.BUY) hideContent(false)
val style = TextStyle(
fontSize = 60.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White,
textAlign = TextAlign.Center,
)
val textId = screenState.textId()
Box(modifier = Modifier.fillMaxSize()) {
if (screenState.isSplashingTextDisplaying()) {
TextAutoSize(
modifier = Modifier
.align(Alignment.Center)
.padding(start = 20.dp, end = 20.dp, bottom = 100.dp),
text = textId?.let { stringResource(textId) } ?: "",
textStyle = style,
fontSizeRange = FontSizeRange(20.sp, 60.sp)
)
} else {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.weight(0.7f)
) {
if (screenState.isMeetTangemDisplaying()) {
StoriesTextAnimation(
slideInDelay = 0,
) { modifier ->
TextAutoSize(
modifier = modifier
.padding(start = 20.dp, end = 20.dp, top = 50.dp)
.alpha(if (screenState.value == StartingScreenState.SHOW_CARD) 0f else 1f),
text = textId?.let { stringResource(textId) } ?: "",
textStyle = style,
fontSizeRange = FontSizeRange(20.sp, 60.sp)
)
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1.25f)
) {
StoriesBottomImageAnimation(
totalDuration = duration,
firstStepDuration = 500,
) { modifier ->
Image(
modifier = modifier.fillMaxWidth(),
painter = painterResource(id = R.drawable.meet_tangem),
contentDescription = "Tangem Wallet card",
contentScale = ContentScale.FillWidth,
)
}
}
}
}
}
}
enum class StartingScreenState {
INIT, BUY, STORE, SEND, PAY, EXCHANGE, BORROW, LEND, SHOW_CARD, MEET_TANGEM
}
@StringRes
private fun MutableState<StartingScreenState>.textId(): Int? = when (this.value) {
StartingScreenState.INIT -> null
StartingScreenState.BUY -> R.string.story_meet_buy
StartingScreenState.STORE -> R.string.story_meet_store
StartingScreenState.SEND -> R.string.story_meet_send
StartingScreenState.PAY -> R.string.story_meet_pay
StartingScreenState.EXCHANGE -> R.string.story_meet_exchange
StartingScreenState.BORROW -> R.string.story_meet_borrow
StartingScreenState.LEND -> R.string.story_meet_lend
StartingScreenState.SHOW_CARD -> R.string.story_meet_title
StartingScreenState.MEET_TANGEM -> R.string.story_meet_title
}
private fun MutableState<StartingScreenState>.isSplashingTextDisplaying(): Boolean {
return this.value != StartingScreenState.MEET_TANGEM &&
this.value != StartingScreenState.SHOW_CARD
}
private fun MutableState<StartingScreenState>.isTangemCardDisplaying(): Boolean {
return this.value == StartingScreenState.SHOW_CARD ||
this.value == StartingScreenState.MEET_TANGEM
}
private fun MutableState<StartingScreenState>.isMeetTangemDisplaying(): Boolean {
return this.value == StartingScreenState.MEET_TANGEM
}

View file

@ -0,0 +1,103 @@
package com.tangem.tap.features.home.compose.content
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import com.tangem.tap.features.home.compose.uiTools.AnimatedValue
import com.tangem.tap.features.home.compose.uiTools.asImageBitmap
import com.tangem.tap.features.home.compose.uiTools.toAnimatable
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Composable
fun FloatingCardsContent(
isPaused: Boolean,
stepDuration: Int,
) {
val imageBitmap = asImageBitmap(R.drawable.card_placeholder_wallet)
val cards = listOf(
FloatingCard.first(),
FloatingCard.second(),
FloatingCard.third(),
)
Box() {
cards.forEach { floatingCard ->
FloatingCard.Item(
isPaused = isPaused,
imageBitmap = imageBitmap,
cardValues = floatingCard,
stepDuration = stepDuration,
)
}
}
}
private data class CardValues(
val translateX: AnimatedValue = AnimatedValue(0f, 0f),
val translateY: AnimatedValue = AnimatedValue(0f, 0f),
val rotationX: AnimatedValue = AnimatedValue(0f, 0f),
val rotationY: AnimatedValue = AnimatedValue(0f, 0f),
val rotationZ: AnimatedValue = AnimatedValue(0f, 0f),
val scale: AnimatedValue = AnimatedValue(1f, 1f),
)
private class FloatingCard {
companion object {
@Composable
fun Item(
isPaused: Boolean,
stepDuration: Int,
imageBitmap: ImageBitmap,
cardValues: CardValues,
) {
Image(
bitmap = imageBitmap,
contentDescription = "Floating Tangem card",
modifier = Modifier
.graphicsLayer(
translationX = cardValues.translateX.toAnimatable(isPaused, stepDuration).value,
translationY = cardValues.translateY.toAnimatable(isPaused, stepDuration).value,
rotationX = cardValues.rotationX.toAnimatable(isPaused, stepDuration).value,
rotationY = cardValues.rotationY.toAnimatable(isPaused, stepDuration).value,
rotationZ = cardValues.rotationZ.toAnimatable(isPaused, stepDuration).value,
scaleX = cardValues.scale.toAnimatable(isPaused, stepDuration).value,
scaleY = cardValues.scale.toAnimatable(isPaused, stepDuration).value,
),
)
}
fun first(): CardValues = CardValues(
translateX = -400f to -350f,
translateY = 30f to 32f,
rotationX = 10f to 12f,
rotationY = 15f to 15f,
rotationZ = 40f to 27f,
scale = 0.6f to 0.6f,
)
fun second(): CardValues = CardValues(
translateX = 350f to 300f,
translateY = 0f to 50f,
rotationX = 30f to 48f,
rotationY = 0f to 5f,
rotationZ = -34f to -42f,
scale = 0.47f to 0.35f,
)
fun third(): CardValues = CardValues(
translateX = 320f to 250f,
translateY = 500f to 500f,
rotationX = 0f to 3f,
rotationY = 10f to 10f,
rotationZ = -45f to -30f,
scale = 0.6f to 0.7f,
)
}
}

View file

@ -0,0 +1,47 @@
package com.tangem.tap.features.home.compose.uiTools
import androidx.compose.animation.core.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
typealias AnimatedValue = Pair<Float, Float>
@Composable
fun AnimatedValue.toAnimatable(
isPaused: Boolean,
duration: Int,
easing: Easing = LinearEasing,
): Animatable<Float, AnimationVector1D> {
return animatable(
values = this,
isPaused = isPaused,
duration = duration,
easing = easing,
)
}
@Composable
fun animatable(
values: AnimatedValue,
isPaused: Boolean,
duration: Int,
easing: Easing = LinearEasing,
): Animatable<Float, AnimationVector1D> {
val animatable = remember { Animatable(values.first) }
LaunchedEffect(isPaused) {
if (isPaused) {
animatable.stop()
} else {
animatable.animateTo(
targetValue = values.second,
animationSpec = tween(
durationMillis = duration,
easing = easing
)
)
}
}
return animatable
}

View file

@ -0,0 +1,174 @@
package com.tangem.tap.features.home.compose.uiTools
import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.animation.core.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
@Composable
fun HorizontalSlidingImage(
painter: Painter,
paused: Boolean,
duration: Int,
itemSize: DpSize,
startOffset: Float,
targetOffset: Float,
contentDescription: String,
) {
val offsetX = remember { Animatable(startOffset * -1f) }
Image(
modifier = Modifier
.offset { IntOffset(offsetX.value.toInt(), 0) }
.requiredWidth(itemSize.width)
.requiredHeight(itemSize.height),
alignment = Alignment.TopStart,
contentScale = ContentScale.FillBounds,
painter = painter,
contentDescription = contentDescription,
)
LaunchedEffect(paused) {
if (paused) {
offsetX.stop()
} else {
offsetX.animateTo(
targetValue = (startOffset + targetOffset) * -1f,
animationSpec = tween(
durationMillis = duration,
easing = LinearEasing,
),
)
}
}
}
@Composable
fun StoriesTextAnimation(
slideInDuration: Int = 500,
slideInDelay: Int = 200,
slideDistance: Dp = 60.dp,
label: String = "",
content: @Composable (Modifier) -> Unit
) {
val isLaunched = remember { mutableStateOf(false) }
val transition = updateTransition(targetState = isLaunched.value, label = label)
val offsetY = transition.animateDp(
transitionSpec = {
tween(
durationMillis = slideInDuration,
delayMillis = slideInDelay,
easing = FastOutSlowInEasing
)
},
label = "Slide in"
) { value -> if (value) 0.dp else slideDistance }
val alpha = transition.animateFloat(
transitionSpec = {
tween(
durationMillis = slideInDuration * 2,
delayMillis = slideInDelay,
easing = FastOutSlowInEasing
)
},
label = "Visibility"
) { value -> if (value) 1f else 0f }
content(
Modifier
.absoluteOffset(y = offsetY.value)
.alpha(alpha.value)
)
LaunchedEffect(Unit) { isLaunched.value = true }
}
@Composable
fun StoriesBottomImageAnimation(
initialScale: Float = 2f,
firstStepDuration: Int,
totalDuration: Int,
content: @Composable (Modifier) -> Unit
) {
val scaleSwitchBarrier = remember { 1.15f }
val secondStepDuration = remember { totalDuration - firstStepDuration }
val isFirstStepLaunched = remember { mutableStateOf(false) }
val isSecondStepLaunched = remember { mutableStateOf(false) }
val firstTransition = updateTransition(targetState = isFirstStepLaunched.value, label = "Bottom image animation")
val secondTransition = updateTransition(targetState = isSecondStepLaunched.value, label = "Bottom image animation")
val fadeIn = firstTransition.animateFloat(
transitionSpec = {
tween(
durationMillis = 400,
)
},
label = "Fade in animation"
) { value -> if (value) 1f else 0f }
val firstScaleStep = firstTransition.animateFloat(
transitionSpec = {
tween(
durationMillis = firstStepDuration,
easing = FastOutLinearInEasing,
)
},
label = "Scale"
) { value -> if (value) scaleSwitchBarrier else initialScale }
val secondScaleStep = secondTransition.animateFloat(
transitionSpec = {
tween(
durationMillis = secondStepDuration,
easing = LinearEasing,
)
},
label = "Scale"
) { value -> if (value) 1f else scaleSwitchBarrier }
if (firstScaleStep.value == scaleSwitchBarrier) isSecondStepLaunched.value = true
val modifier = if (!isSecondStepLaunched.value) {
Modifier.scale(firstScaleStep.value)
} else {
Modifier.scale(secondScaleStep.value)
}.alpha(fadeIn.value)
content(modifier)
LaunchedEffect(Unit) { isFirstStepLaunched.value = true }
}
@Composable
fun asImageBitmap(@DrawableRes drawableId: Int): ImageBitmap {
val drawable = AppCompatResources.getDrawable(LocalContext.current, drawableId)
?: throw NullPointerException()
return drawable.toBitmap().asImageBitmap()
}

View file

@ -0,0 +1,51 @@
package com.tangem.tap.features.home.compose.uiTools
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.tap.features.home.compose.StoriesScreen
import com.tangem.tap.features.home.compose.content.*
/**
[REDACTED_AUTHOR]
*/
@Preview
@Composable
fun StoriesScreenPreview() {
StoriesScreen(onScanButtonClick = {}, onShopButtonClick = {}, onSearchTokensClick = {})
}
@Preview
@Composable
fun Stories1Preview() {
FirstStoriesContent(false, 8000) {}
}
@Preview
@Composable
private fun RevolutionaryWalletPreview() {
StoriesRevolutionaryWallet(6000)
}
@Preview
@Composable
private fun UltraSecureBackupPreview() {
StoriesUltraSecureBackup(false, 6000)
}
@Preview
@Composable
private fun CurrenciesPreview() {
StoriesCurrencies(false, 6000)
}
@Preview
@Composable
private fun Web3Preview() {
StoriesWeb3(false, 6000)
}
@Preview
@Composable
private fun WalletForEveryonePreview() {
StoriesWalletForEveryone(6000)
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.home.compose
package com.tangem.tap.features.home.compose.views
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -12,7 +11,6 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun TextAutoSize(
modifier: Modifier = Modifier,

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.home.compose
package com.tangem.tap.features.home.compose.views
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.home.compose
package com.tangem.tap.features.home.compose.views
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB