Skip to content

Commit

Permalink
Merge pull request #4034 from tangem/feature/AND-9602-Stories-Impleme…
Browse files Browse the repository at this point in the history
…nted-core-stories-component

AND-9602 Stories: Implemented core stories component
  • Loading branch information
iMaks99 authored Jan 9, 2025
2 parents da2c2ac + dab3752 commit 889fcac
Show file tree
Hide file tree
Showing 8 changed files with 367 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ internal fun StoriesScreen(
}
}

// todo refactor AND-9605
StoriesScreenContent(
modifier = Modifier
.fillMaxSize()
Expand All @@ -75,6 +76,7 @@ internal fun StoriesScreen(
)
}

@Deprecated("Use StoriesContainer from core/ui")
@Suppress("LongMethod")
@Composable
private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: Modifier = Modifier) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import org.rekotlin.StateType

// todo refactor AND-9605
data class HomeState(
val scanInProgress: Boolean = false,
val stories: ImmutableList<Stories> = Stories.entries.toImmutableList(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

/**
Expand Down Expand Up @@ -124,3 +128,17 @@ private fun DrawScope.drawLinearIndicator(

private fun DrawScope.drawLinearIndicatorBackground(color: Color, strokeWidth: Float, strokeCap: 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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)
},
)
},
)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 889fcac

Please sign in to comment.