-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
AND-9602 Stories: Implemented core stories component
- Loading branch information
Showing
6 changed files
with
382 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
124 changes: 124 additions & 0 deletions
124
core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
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 currentStoryContent content of current displaying story | ||
*/ | ||
@Composable | ||
fun StoriesContainer( | ||
config: StoriesContentConfig, | ||
modifier: Modifier = Modifier, | ||
currentStoryContent: @Composable BoxScope.(StoryConfig) -> Unit, | ||
) { | ||
var isPressed by remember { mutableStateOf(value = false) } | ||
val storyState by remember(config) { | ||
mutableStateOf( | ||
StoriesStepStateMachine( | ||
stories = config.stories, | ||
isRepeatable = config.isRepeatable, | ||
), | ||
) | ||
} | ||
|
||
val isPaused = isPressed || config.isHoldProgress | ||
|
||
Box(modifier = modifier) { | ||
StoriesClickableArea( | ||
onPress = { isPressed = it }, | ||
onPreviousStory = { | ||
storyState.prevStory() | ||
config.onPreviousStory(storyState.currentStory) | ||
}, | ||
onNextStory = { | ||
if (storyState.nextStory()) { | ||
config.onFinishStories() | ||
} else { | ||
config.onNextStory(storyState.currentStory) | ||
} | ||
}, | ||
) | ||
|
||
StoriesProgressBar( | ||
steps = storyState.steps, | ||
currentStep = storyState.currentIndex.intValue, | ||
stepDuration = storyState.currentStory.duration, | ||
paused = isPaused, | ||
onStepFinish = { | ||
if (storyState.nextStory()) { | ||
config.onFinishStories() | ||
} else { | ||
config.onNextStory(storyState.currentStory) | ||
} | ||
}, | ||
) | ||
|
||
currentStoryContent(storyState.currentStory) | ||
} | ||
} | ||
|
||
// 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)), | ||
currentStoryContent = { pageConfig -> | ||
if (pageConfig !is StoryConfigPreviewProviderData) return@StoriesContainer | ||
Text( | ||
text = pageConfig.title, | ||
color = TangemColorPalette.White, | ||
textAlign = TextAlign.Center, | ||
modifier = Modifier.align(Alignment.Center), | ||
) | ||
}, | ||
) | ||
} | ||
} | ||
|
||
private data class StoryContainerPreviewProviderData( | ||
override val isHoldProgress: Boolean = false, | ||
override val stories: ImmutableList<StoryConfig> = persistentListOf( | ||
StoryConfigPreviewProviderData(id = "FIRST", title = "Tangem"), | ||
StoryConfigPreviewProviderData(id = "SECOND", title = "Cold"), | ||
StoryConfigPreviewProviderData(id = "THIRD", title = "Wallet"), | ||
), | ||
override val onPreviousStory: (StoryConfig) -> Unit = {}, | ||
override val onNextStory: (StoryConfig) -> Unit = {}, | ||
override val onFinishStories: () -> Unit = {}, | ||
override val isRepeatable: Boolean = true, | ||
) : StoriesContentConfig | ||
|
||
private data class StoryConfigPreviewProviderData( | ||
override val id: String, | ||
override val duration: Int = 3_000, | ||
val title: String, | ||
) : StoryConfig | ||
// endregion |
56 changes: 56 additions & 0 deletions
56
core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesClickableArea.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}, | ||
) | ||
}, | ||
) | ||
} | ||
} |
127 changes: 127 additions & 0 deletions
127
core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesProgressBar.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesStepStateMachine.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package com.tangem.core.ui.components.stories.inner | ||
|
||
import androidx.compose.runtime.IntState | ||
import androidx.compose.runtime.asIntState | ||
import androidx.compose.runtime.mutableIntStateOf | ||
import com.tangem.core.ui.components.stories.model.StoryConfig | ||
import kotlinx.collections.immutable.ImmutableList | ||
|
||
internal class StoriesStepStateMachine( | ||
private val stories: ImmutableList<StoryConfig>, | ||
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 | ||
} | ||
} |
Oops, something went wrong.