Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-28 15:00:11 +05:00
parent f587974fc2
commit 43fb47e267
8 changed files with 368 additions and 1 deletions

View file

@ -57,6 +57,7 @@ internal fun StoriesScreen(
}
}
// todo refactor [REDACTED_TASK_KEY]
StoriesScreenContent(
modifier = Modifier
.fillMaxSize()
@ -75,6 +76,7 @@ internal fun StoriesScreen(
)
}
@Deprecated("Use StoriesContainer from core/ui")
@Suppress("LongMethod")
@Composable
private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: Modifier = Modifier) {

View file

@ -4,6 +4,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import org.rekotlin.StateType
// todo refactor [REDACTED_TASK_KEY]
data class HomeState(
val scanInProgress: Boolean = false,
val stories: ImmutableList<Stories> = Stories.entries.toImmutableList(),

View file

@ -2,7 +2,9 @@
package com.tangem.core.ui.components.progressbar
import android.content.res.Configuration
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
@ -15,10 +17,12 @@ 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.tooling.preview.Preview
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 com.tangem.core.ui.res.TangemThemePreview
import kotlin.math.abs
/**
@ -123,4 +127,18 @@ private fun DrawScope.drawLinearIndicator(
}
private fun DrawScope.drawLinearIndicatorBackground(color: Color, strokeWidth: Float, strokeCap: StrokeCap) =
drawLinearIndicator(0f, 1f, color, strokeWidth, strokeCap)
drawLinearIndicator(0f, 1f, color, strokeWidth, strokeCap)
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun LinearProgressIndicator_Preview() {
TangemThemePreview {
LinearProgressIndicator(
progress = { 0.5f },
modifier = Modifier.fillMaxWidth(),
)
}
}
// endregion

View file

@ -0,0 +1,106 @@
package com.tangem.core.ui.components.stories
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.stories.inner.StoriesClickableArea
import com.tangem.core.ui.components.stories.inner.StoriesProgressBar
import com.tangem.core.ui.components.stories.inner.StoriesStepStateMachine
import com.tangem.core.ui.components.stories.model.StoriesContentConfig
import com.tangem.core.ui.components.stories.model.StoryConfig
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* Stories container component.
* Implements base logic for switching story pages and handling clicks
*
* @param config stories configuration
* @param modifier composable modifier
* @param isPauseStories pause stories progression externally
* @param currentStoryContent content of current displaying story
*/
@Composable
inline fun <reified T : StoryConfig> StoriesContainer(
config: StoriesContentConfig<T>,
modifier: Modifier = Modifier,
isPauseStories: Boolean = false,
crossinline currentStoryContent: @Composable BoxScope.(T, Boolean) -> Unit,
) {
var isPressed by remember { mutableStateOf(value = false) }
val storyState by remember(config) {
mutableStateOf(
StoriesStepStateMachine(
stories = config.stories,
isRepeatable = config.isRestartable,
),
)
}
val isPaused = isPressed || isPauseStories
Box(modifier = modifier) {
StoriesClickableArea(
onPress = { isPressed = it },
onPreviousStory = storyState::prevStory,
onNextStory = storyState::nextStory,
)
StoriesProgressBar(
steps = storyState.steps,
currentStep = storyState.currentIndex.intValue,
stepDuration = storyState.currentStory.duration,
paused = isPaused,
onStepFinish = storyState::nextStory,
)
currentStoryContent(storyState.currentStory, isPaused)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun StoriesContainer_Preview() {
TangemThemePreview {
StoriesContainer(
config = StoryContainerPreviewProviderData(),
modifier = Modifier.background(Color(0xFF010101)),
isPauseStories = false,
currentStoryContent = { pageConfig, _ ->
Text(
text = pageConfig.title,
color = TangemColorPalette.White,
textAlign = TextAlign.Center,
modifier = Modifier.align(Alignment.Center),
)
},
)
}
}
private data class StoryContainerPreviewProviderData(
override val stories: ImmutableList<StoryConfigPreviewProviderData> = persistentListOf(
StoryConfigPreviewProviderData(title = "Tangem"),
StoryConfigPreviewProviderData(title = "Cold"),
StoryConfigPreviewProviderData(title = "Wallet"),
),
override val isRestartable: Boolean = true,
) : StoriesContentConfig<StoryConfigPreviewProviderData>
private data class StoryConfigPreviewProviderData(
override val duration: Int = 3_000,
val title: String,
) : StoryConfig
// endregion

View file

@ -0,0 +1,56 @@
package com.tangem.core.ui.components.stories.inner
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
private const val STORY_HOLD_DELAY = 200
@Composable
fun StoriesClickableArea(onPress: (Boolean) -> Unit, onPreviousStory: () -> Unit, onNextStory: () -> Unit) {
Row(
modifier = Modifier.fillMaxSize(),
) {
Box(
Modifier
.weight(1f)
.fillMaxHeight()
.pointerInput(Unit) {
detectTapGestures(
onPress = {
val pressStartTime = System.currentTimeMillis()
onPress(true)
this.tryAwaitRelease()
val pressEndTime = System.currentTimeMillis()
val totalPressTime = pressEndTime - pressStartTime
if (totalPressTime < STORY_HOLD_DELAY) onPreviousStory()
onPress(false)
},
)
},
)
Box(
Modifier
.weight(1f)
.fillMaxHeight()
.pointerInput(Unit) {
detectTapGestures(
onPress = {
val pressStartTime = System.currentTimeMillis()
onPress(true)
this.tryAwaitRelease()
val pressEndTime = System.currentTimeMillis()
val totalPressTime = pressEndTime - pressStartTime
if (totalPressTime < STORY_HOLD_DELAY) onNextStory()
onPress(false)
},
)
},
)
}
}

View file

@ -0,0 +1,127 @@
package com.tangem.core.ui.components.stories.inner
import android.provider.Settings
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import kotlinx.coroutines.delay
private const val STORIES_ANIMATION_SPEED_ZERO_DURATION = 3000L
const val STORY_DURATION = 8_000
private suspend fun animationProgress(
paused: Boolean,
animatorSpeed: Float,
stepDuration: Int,
progress: Animatable<Float, AnimationVector1D>,
onStepFinish: () -> Unit,
) {
if (paused) {
progress.stop()
} else {
if (animatorSpeed == 0f) {
progress.snapTo(1f)
delay(STORIES_ANIMATION_SPEED_ZERO_DURATION)
} else {
progress.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = (stepDuration * (1f - progress.value)).toInt(),
easing = LinearEasing,
),
)
progress.snapTo(0f)
}
onStepFinish()
}
}
@Composable
fun StoriesProgressBar(
steps: Int,
currentStep: Int,
paused: Boolean = false,
stepDuration: Int = STORY_DURATION,
onStepFinish: () -> Unit = {},
) {
val progress = remember(currentStep) { Animatable(initialValue = 0f) }
val context = LocalContext.current
val animatorSpeed = Settings.Global.getFloat(
context.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE,
1f,
)
LaunchedEffect(paused, currentStep, animatorSpeed) {
animationProgress(
paused = paused,
animatorSpeed = animatorSpeed,
stepDuration = stepDuration,
progress = progress,
onStepFinish = onStepFinish,
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(
start = 16.dp,
end = 16.dp,
top = 16.dp,
),
) {
for (index in 0..steps) {
Row(
modifier = Modifier
.height(2.dp)
.weight(1f)
.clip(RoundedCornerShape(2.dp))
.background(TangemColorPalette.White.copy(alpha = .2f)),
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(2.dp))
.background(TangemColorPalette.White)
.fillMaxHeight()
.let {
when (index) {
currentStep -> it.fillMaxWidth(progress.value)
in 0..currentStep -> it.fillMaxWidth(fraction = 1f)
else -> it
}
},
)
}
SpacerW4()
}
}
}
@Preview
@Composable
private fun StoriesProgressBarPreview() {
Box(
modifier = Modifier
.wrapContentSize()
.background(TangemColorPalette.Black)
.padding(vertical = TangemTheme.dimens.spacing16),
) {
StoriesProgressBar(steps = 5, currentStep = 3, paused = false)
}
}

View file

@ -0,0 +1,39 @@
package com.tangem.core.ui.components.stories.inner
import androidx.compose.runtime.IntState
import androidx.compose.runtime.asIntState
import androidx.compose.runtime.mutableIntStateOf
import kotlinx.collections.immutable.ImmutableList
class StoriesStepStateMachine<T>(
private val stories: ImmutableList<T>,
private val isRepeatable: Boolean,
) {
private var _currentIndex = mutableIntStateOf(FIRST_INDEX)
val steps = stories.lastIndex
val currentIndex: IntState
get() = _currentIndex.asIntState()
val currentStory
get() = stories[currentIndex.intValue.coerceIn(FIRST_INDEX, steps)]
fun nextStory(): Boolean {
_currentIndex.intValue = if (currentIndex.intValue == steps && isRepeatable) {
FIRST_INDEX
} else {
currentIndex.intValue.inc().coerceAtMost(stories.size)
}
return currentIndex.intValue > steps
}
fun prevStory() {
_currentIndex.intValue = currentIndex.intValue.dec().coerceAtLeast(FIRST_INDEX)
}
private companion object {
const val FIRST_INDEX = 0
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.core.ui.components.stories.model
import kotlinx.collections.immutable.ImmutableList
/**
* Config for stories component
*
* @property stories configuration list
* @property isRestartable indicates than stories progressions starts
*/
interface StoriesContentConfig<T : StoryConfig> {
val stories: ImmutableList<T>
val isRestartable: Boolean
}
interface StoryConfig {
val duration: Int
}