Skip to content

Commit b5d6f72

Browse files
committed
WTA #71 added interactivity for stats chip, can click to flip the chip to show other stats
1 parent 043c5d8 commit b5d6f72

File tree

5 files changed

+186
-96
lines changed

5 files changed

+186
-96
lines changed

core/models/src/main/java/com/jacob/wakatimeapp/core/models/Streak.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,36 @@ import kotlinx.datetime.Instant
55
import kotlinx.datetime.LocalDate
66
import kotlinx.datetime.TimeZone
77
import kotlinx.datetime.daysUntil
8+
import kotlinx.datetime.format
9+
import kotlinx.datetime.format.DateTimeFormat
10+
import kotlinx.datetime.format.char
811
import kotlinx.datetime.minus
912
import kotlinx.datetime.plus
1013
import kotlinx.datetime.toLocalDateTime
1114
import kotlinx.serialization.Serializable
1215
import timber.log.Timber
1316
import kotlin.collections.Map.Entry
1417

18+
private const val EpochYear = 1970
19+
1520
@Serializable
1621
data class Streak(
1722
val start: LocalDate,
1823
val end: LocalDate,
1924
) : Comparable<Streak> {
2025
val days: Int = if (this == ZERO) 0 else start.daysUntil(end) + 1
2126

27+
private val streakFormat = LocalDate.Format {
28+
dayOfMonth()
29+
char('/')
30+
monthNumber()
31+
char('/')
32+
yearTwoDigits(EpochYear)
33+
}
34+
35+
fun formattedPrintRange(format: DateTimeFormat<LocalDate>? = null): String =
36+
"${start.format(format ?: streakFormat)} to ${end.format(format ?: streakFormat)}"
37+
2238
operator fun plus(other: Streak) = when {
2339
this == ZERO -> other
2440
other == ZERO -> this

core/ui/src/main/java/com/jacob/wakatimeapp/core/ui/components/cards/StatsChip.kt

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.jacob.wakatimeapp.core.ui.components.cards
22

33
import androidx.annotation.DrawableRes
4+
import androidx.compose.animation.core.animateFloatAsState
45
import androidx.compose.foundation.Image
56
import androidx.compose.foundation.background
67
import androidx.compose.foundation.clickable
@@ -16,11 +17,16 @@ import androidx.compose.foundation.shape.RoundedCornerShape
1617
import androidx.compose.material3.MaterialTheme
1718
import androidx.compose.material3.Surface
1819
import androidx.compose.runtime.Composable
20+
import androidx.compose.runtime.getValue
21+
import androidx.compose.runtime.mutableStateOf
22+
import androidx.compose.runtime.remember
23+
import androidx.compose.runtime.setValue
1924
import androidx.compose.ui.Alignment
2025
import androidx.compose.ui.Modifier
2126
import androidx.compose.ui.draw.clip
2227
import androidx.compose.ui.graphics.Brush
2328
import androidx.compose.ui.graphics.ColorFilter
29+
import androidx.compose.ui.graphics.graphicsLayer
2430
import androidx.compose.ui.res.painterResource
2531
import androidx.compose.ui.unit.dp
2632
import com.jacob.wakatimeapp.core.ui.WtaPreviews
@@ -37,6 +43,7 @@ fun StatsChip(
3743
roundedCornerPercent: Int,
3844
modifier: Modifier = Modifier,
3945
onClick: () -> Unit = {},
46+
rotation: Float = 0f,
4047
chipContent: @Composable ColumnScope.() -> Unit,
4148
) {
4249
val gradientBrush = Brush.horizontalGradient(gradient.colorList)
@@ -59,19 +66,61 @@ fun StatsChip(
5966
bottom = MaterialTheme.spacing.extraSmall,
6067
)
6168
.size(size = 50.dp)
62-
.align(Alignment.BottomEnd),
69+
.align(Alignment.BottomEnd)
70+
.graphicsLayer {
71+
this.rotationX = rotation
72+
},
6373
)
6474
Column(
6575
modifier = Modifier
6676
.padding(
67-
horizontal = MaterialTheme.spacing.medium,
77+
horizontal = MaterialTheme.spacing.small,
6878
vertical = MaterialTheme.spacing.small,
69-
),
79+
)
80+
.graphicsLayer { this.rotationX = rotation },
7081
content = chipContent,
7182
)
7283
}
7384
}
7485

86+
/**
87+
* [Rotating on Axis in 3D](https://www.youtube.com/watch?v=WdQUDHOwlgE&t=148s)
88+
* [Resetting Animation for each click](https://stackoverflow.com/questions/78620347/repeat-animation-when-user-clicks-jetpack-compose-android)
89+
* [Showing the back of the card correctly](https://medium.com/bilue/card-flip-animation-with-jetpack-compose-f60aaaad4ac9)
90+
*/
91+
@Composable
92+
fun InteractableStatsChip(
93+
modifier: Modifier,
94+
gradient: Gradient,
95+
iconId: Int = MaterialTheme.assets.icons.time,
96+
frontContent: @Composable ColumnScope.() -> Unit = {},
97+
backContent: @Composable ColumnScope.() -> Unit = {},
98+
) {
99+
var isFlipped by remember { mutableStateOf(false) }
100+
val rotationXAnimation = animateFloatAsState(targetValue = if (isFlipped) 180f else 0f)
101+
102+
val contentToShow = remember(rotationXAnimation.value) {
103+
if (rotationXAnimation.value < 90f) frontContent else backContent
104+
}
105+
106+
val innerRotation = remember(rotationXAnimation.value) {
107+
if (rotationXAnimation.value < 90f) 0f else 180f
108+
}
109+
110+
StatsChip(
111+
gradient = gradient,
112+
iconId = iconId,
113+
roundedCornerPercent = 15,
114+
onClick = { isFlipped = !isFlipped },
115+
modifier = modifier.graphicsLayer {
116+
this.rotationX = rotationXAnimation.value
117+
this.cameraDistance = 8 * this.density
118+
},
119+
rotation = innerRotation,
120+
chipContent = { contentToShow() },
121+
)
122+
}
123+
75124
@WtaPreviews
76125
@Composable
77126
private fun StatsChipPreview() = WakaTimeAppTheme {

details/src/main/java/com/jacob/wakatimeapp/details/ui/components/CardAndChipConten.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import androidx.compose.material3.Text
1212
import androidx.compose.runtime.Composable
1313
import androidx.compose.ui.Alignment
1414
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.graphics.graphicsLayer
1516
import androidx.compose.ui.text.TextStyle
1617
import androidx.compose.ui.text.font.FontWeight
1718
import androidx.compose.ui.text.style.TextAlign
@@ -36,18 +37,26 @@ internal fun ColumnScope.ChipContent(
3637
cardSubHeading: String,
3738
cardHeading: String,
3839
gradient: Gradient,
40+
rotationX: Float = 0f,
3941
statValueTextStyle: TextStyle = MaterialTheme.typography.displayLarge,
4042
) {
4143
Text(
4244
text = cardHeading,
4345
color = gradient.onStartColor,
4446
style = MaterialTheme.typography.titleLarge,
45-
modifier = Modifier.removeFontPadding(statValueTextStyle),
47+
modifier = Modifier
48+
.removeFontPadding(statValueTextStyle)
49+
.graphicsLayer {
50+
this.rotationX = rotationX
51+
},
4652
)
4753
Text(
4854
text = cardSubHeading,
4955
color = gradient.onStartColor,
5056
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Light),
57+
modifier = Modifier.graphicsLayer {
58+
this.rotationX = rotationX
59+
},
5160
)
5261
}
5362

@@ -59,7 +68,8 @@ internal fun BoxScope.CardContent(
5968
) {
6069
Column(
6170
horizontalAlignment = Alignment.Start,
62-
modifier = Modifier.fillMaxWidth()
71+
modifier = Modifier
72+
.fillMaxWidth()
6373
.padding(horizontal = MaterialTheme.spacing.medium)
6474
.padding(bottom = MaterialTheme.spacing.extraSmall),
6575
) {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.jacob.wakatimeapp.details.ui.components
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Row
5+
import androidx.compose.foundation.layout.fillMaxWidth
6+
import androidx.compose.material3.MaterialTheme
7+
import androidx.compose.material3.Text
8+
import androidx.compose.runtime.Composable
9+
import androidx.compose.ui.Modifier
10+
import androidx.compose.ui.text.font.FontWeight
11+
import com.jacob.wakatimeapp.core.models.Streak
12+
import com.jacob.wakatimeapp.core.ui.components.cards.InteractableStatsChip
13+
import com.jacob.wakatimeapp.core.ui.theme.gradients
14+
import com.jacob.wakatimeapp.core.ui.theme.spacing
15+
import com.jacob.wakatimeapp.details.ui.DetailsPageViewState
16+
17+
@Composable
18+
internal fun ProjectStreakChips(detailsPageData: DetailsPageViewState.Loaded, modifier: Modifier = Modifier) {
19+
Row(
20+
modifier = modifier.fillMaxWidth(),
21+
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.small),
22+
) {
23+
InteractableStatsChip(
24+
modifier = Modifier.weight(1f),
25+
gradient = MaterialTheme.gradients.shifter,
26+
frontContent = {
27+
ChipContent(
28+
cardHeading = "${detailsPageData.longestStreakInProject.days} Days",
29+
cardSubHeading = "Longest Streak",
30+
gradient = MaterialTheme.gradients.shifter,
31+
statValueTextStyle = MaterialTheme.typography.titleMedium,
32+
)
33+
},
34+
backContent = { StreakRangeDisplay(detailsPageData.longestStreakInProject) },
35+
)
36+
37+
InteractableStatsChip(
38+
modifier = Modifier.weight(1f),
39+
gradient = MaterialTheme.gradients.shifter,
40+
frontContent = {
41+
ChipContent(
42+
cardHeading = "${detailsPageData.currentStreakInProject.days} Days",
43+
cardSubHeading = "Current Streak",
44+
gradient = MaterialTheme.gradients.shifter,
45+
statValueTextStyle = MaterialTheme.typography.titleMedium,
46+
)
47+
},
48+
backContent = { StreakRangeDisplay(detailsPageData.currentStreakInProject) },
49+
)
50+
}
51+
}
52+
53+
@Composable
54+
private fun StreakRangeDisplay(streak: Streak) {
55+
val gradient = MaterialTheme.gradients.shifter
56+
Text(
57+
text = streak.formattedPrintRange(),
58+
color = gradient.onStartColor,
59+
style = MaterialTheme.typography.bodyLarge,
60+
)
61+
Text(
62+
text = "Streak Range",
63+
color = gradient.onStartColor,
64+
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Light),
65+
)
66+
}

0 commit comments

Comments
 (0)