1+ package com.flipcash.app.tokens.internal.components.processing
2+
3+ import androidx.compose.animation.AnimatedContent
4+ import androidx.compose.animation.AnimatedVisibility
5+ import androidx.compose.animation.core.FastOutSlowInEasing
6+ import androidx.compose.animation.core.animateFloatAsState
7+ import androidx.compose.animation.core.tween
8+ import androidx.compose.animation.fadeIn
9+ import androidx.compose.animation.fadeOut
10+ import androidx.compose.animation.togetherWith
11+ import androidx.compose.foundation.Image
12+ import androidx.compose.foundation.layout.Arrangement
13+ import androidx.compose.foundation.layout.Column
14+ import androidx.compose.foundation.layout.Row
15+ import androidx.compose.foundation.layout.Spacer
16+ import androidx.compose.foundation.layout.fillMaxSize
17+ import androidx.compose.foundation.layout.height
18+ import androidx.compose.foundation.layout.padding
19+ import androidx.compose.foundation.layout.size
20+ import androidx.compose.material.Button
21+ import androidx.compose.material.Text
22+ import androidx.compose.runtime.Composable
23+ import androidx.compose.runtime.LaunchedEffect
24+ import androidx.compose.runtime.getValue
25+ import androidx.compose.runtime.mutableLongStateOf
26+ import androidx.compose.runtime.mutableStateOf
27+ import androidx.compose.runtime.remember
28+ import androidx.compose.runtime.setValue
29+ import androidx.compose.runtime.withFrameMillis
30+ import androidx.compose.ui.Alignment
31+ import androidx.compose.ui.Modifier
32+ import androidx.compose.ui.graphics.Color
33+ import androidx.compose.ui.graphics.StrokeCap
34+ import androidx.compose.ui.res.painterResource
35+ import androidx.compose.ui.tooling.preview.Preview
36+ import androidx.compose.ui.unit.dp
37+ import com.flipcash.app.theme.FlipcashPreview
38+ import com.flipcash.features.tokens.R
39+ import com.getcode.theme.CodeTheme
40+ import com.getcode.ui.theme.CodeButton
41+ import com.getcode.ui.theme.CodeCircularProgressIndicator
42+ import com.getcode.view.LoadingSuccessState
43+ import kotlinx.coroutines.isActive
44+ import kotlin.math.pow
45+ import kotlin.time.Duration
46+ import kotlin.time.Duration.Companion.seconds
47+
48+ @Composable
49+ internal fun ProcessingLoadingIndicator (
50+ processingState : LoadingSuccessState ,
51+ modifier : Modifier = Modifier ,
52+ duration : Duration = 60.seconds,
53+ ) {
54+ var elapsedMs by remember(processingState) { mutableLongStateOf(0L ) }
55+ val isLoading = processingState.state is LoadingSuccessState .State .Loading
56+
57+ LaunchedEffect (isLoading) {
58+ if (isLoading) {
59+ val startTime = withFrameMillis { it } - elapsedMs
60+ while (isActive) {
61+ withFrameMillis { frameTime ->
62+ elapsedMs = (frameTime - startTime).coerceAtLeast(0 )
63+ }
64+ }
65+ }
66+ }
67+
68+ val timedProgress = remember(elapsedMs) {
69+ val fraction = (elapsedMs / duration.inWholeMilliseconds.toFloat()).coerceIn(0f , 1f )
70+ val eased = 1f - (1f - fraction).pow(2 )
71+ 0.05f + eased * 0.85f
72+ }
73+
74+ // Tracks whether the fill-to-100% animation has finished.
75+ var fillComplete by remember { mutableStateOf(false ) }
76+
77+ val animatedProgress by animateFloatAsState(
78+ targetValue = when (processingState.state) {
79+ LoadingSuccessState .State .Loading -> timedProgress
80+ LoadingSuccessState .State .Success -> 1f
81+ LoadingSuccessState .State .Error -> timedProgress
82+ else -> 0.05f
83+ },
84+ animationSpec = when (processingState.state) {
85+ LoadingSuccessState .State .Success -> tween(durationMillis = 400 , easing = FastOutSlowInEasing )
86+ else -> tween(durationMillis = 100 )
87+ },
88+ finishedListener = { value ->
89+ if (processingState.state is LoadingSuccessState .State .Success && value == 1f ) {
90+ fillComplete = true
91+ }
92+ },
93+ label = " progress" ,
94+ )
95+
96+ // Show the ring while loading or filling to 100%.
97+ // Once filled, show the result icon via Crossfade.
98+ val displayedState = remember(processingState.state, fillComplete) {
99+ when {
100+ processingState.state is LoadingSuccessState .State .Success && ! fillComplete -> LoadingSuccessState .State .Loading
101+ else -> processingState.state
102+ }
103+ }
104+
105+ // Reset fillComplete when a new loading cycle starts.
106+ LaunchedEffect (isLoading) {
107+ if (isLoading) fillComplete = false
108+ }
109+ AnimatedContent (
110+ targetState = displayedState,
111+ transitionSpec = { fadeIn().togetherWith(fadeOut()) },
112+ modifier = modifier,
113+ ) { targetState ->
114+ when (targetState) {
115+ LoadingSuccessState .State .Error -> Image (
116+ modifier = modifier,
117+ painter = painterResource(R .drawable.ic_circle_exclamation_large),
118+ contentDescription = null ,
119+ )
120+ LoadingSuccessState .State .Idle -> Spacer (modifier)
121+ LoadingSuccessState .State .Loading -> CodeCircularProgressIndicator (
122+ modifier = modifier,
123+ progress = animatedProgress,
124+ strokeWidth = CodeTheme .dimens.grid.x1,
125+ color = Color .White ,
126+ backgroundColor = Color .White .copy(0.30f ),
127+ strokeCap = StrokeCap .Butt ,
128+ )
129+ LoadingSuccessState .State .Success -> Image (
130+ modifier = modifier,
131+ painter = painterResource(R .drawable.ic_circle_check_large),
132+ contentDescription = null ,
133+ )
134+ }
135+ }
136+ }
137+
138+ @Preview
139+ @Composable
140+ private fun ProcessingLoadingIndicatorPreview () {
141+ FlipcashPreview (showBackground = true ) {
142+ var state by remember { mutableStateOf(LoadingSuccessState ()) }
143+
144+ Column (
145+ modifier = Modifier
146+ .fillMaxSize()
147+ .padding(24 .dp),
148+ horizontalAlignment = Alignment .CenterHorizontally ,
149+ verticalArrangement = Arrangement .Center ,
150+ ) {
151+ ProcessingLoadingIndicator (
152+ processingState = state,
153+ modifier = Modifier .size(48 .dp),
154+ duration = 20 .seconds,
155+ )
156+
157+ Spacer (modifier = Modifier .height(32 .dp))
158+
159+ Row (horizontalArrangement = Arrangement .spacedBy(8 .dp)) {
160+ CodeButton (onClick = { state = LoadingSuccessState (loading = true ) }) {
161+ Text (" Start" )
162+ }
163+ CodeButton (onClick = { state = LoadingSuccessState (success = true ) }) {
164+ Text (" Success" )
165+ }
166+ CodeButton (onClick = { state = LoadingSuccessState (error = true ) }) {
167+ Text (" Error" )
168+ }
169+ CodeButton (onClick = { state = LoadingSuccessState () }) {
170+ Text (" Reset" )
171+ }
172+ }
173+ }
174+ }
175+ }
0 commit comments