Skip to content

Commit

Permalink
WTA #71 moved logic to flip card/chip to separate component and added…
Browse files Browse the repository at this point in the history
… for card as well
  • Loading branch information
Jacob3075 committed Jan 15, 2025
1 parent 0bf4159 commit a893eed
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 71 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.jacob.wakatimeapp.core.ui.components

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember

/**
* [Rotating on Axis in 3D](https://www.youtube.com/watch?v=WdQUDHOwlgE&t=148s)
* [Resetting Animation for each click](https://stackoverflow.com/questions/78620347/repeat-animation-when-user-clicks-jetpack-compose-android)
* [Showing the back of the card correctly](https://medium.com/bilue/card-flip-animation-with-jetpack-compose-f60aaaad4ac9)
*/
@Composable
fun <T> rememberFlippableState(
frontContent: @Composable T.() -> Unit = {},
backContent: @Composable T.() -> Unit = {},
): FlippableElementData<T> {
val isFlipped = remember { mutableStateOf(false) }
val rotationXAnimation = animateFloatAsState(targetValue = if (isFlipped.value) 180f else 0f)

val contentToShow = remember(rotationXAnimation.value) {
if (rotationXAnimation.value < 90f) frontContent else backContent
}

val innerRotation = remember(rotationXAnimation.value) {
if (rotationXAnimation.value < 90f) 0f else 180f
}

return FlippableElementData(
isFlipped = isFlipped,
rotationAnimationValue = rotationXAnimation,
contentToShow = contentToShow,
innerRotation = innerRotation,
)
}

class FlippableElementData<T>(
val isFlipped: MutableState<Boolean>,
val rotationAnimationValue: State<Float>,
val contentToShow: @Composable T.() -> Unit,
val innerRotation: Float,
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,32 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.jacob.wakatimeapp.core.ui.components.rememberFlippableState
import com.jacob.wakatimeapp.core.ui.theme.assets
import com.jacob.wakatimeapp.core.ui.theme.colors.Gradient

@Composable
fun StatsCard(
gradient: Gradient,
onClick: () -> Unit,
roundedCornerPercent: Int = 25,
@DrawableRes iconId: Int,
modifier: Modifier = Modifier,
roundedCornerPercent: Int = 25,
iconOffset: Dp = 50.dp,
iconSize: Dp = 80.dp,
rotation: Float = 0f,
cardContent: @Composable BoxScope.() -> Unit = {},
) {
val cardGradient = Brush.horizontalGradient(
Expand All @@ -40,22 +47,49 @@ fun StatsCard(
val cardShape = RoundedCornerShape(roundedCornerPercent)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
modifier = modifier
.shadow(elevation = 10.dp, shape = cardShape)
.clickable(onClick = onClick)
.fillMaxWidth()
.background(cardGradient, cardShape),
content = {

Image(
painter = painterResource(id = iconId),
contentDescription = "",
contentScale = ContentScale.FillBounds,
colorFilter = ColorFilter.tint(gradient.onEndColor),
alpha = 0.15f,
modifier = Modifier
.padding(start = iconOffset)
.size(iconSize),
.size(iconSize)
.graphicsLayer { this.rotationX = rotation },
)
cardContent()
Box(Modifier.graphicsLayer { this.rotationX = rotation }, content = cardContent)
},
)
}

@Composable
fun FlippableStatsCard(
gradient: Gradient,
modifier: Modifier = Modifier,
iconId: Int = MaterialTheme.assets.icons.time,
roundedCornerPercent: Int = 25,
frontContent: @Composable BoxScope.() -> Unit = {},
backContent: @Composable BoxScope.() -> Unit = {},
) {
val flippableState = rememberFlippableState(frontContent, backContent)

StatsCard(
gradient = gradient,
iconId = iconId,
roundedCornerPercent = roundedCornerPercent,
onClick = { flippableState.isFlipped.value = !flippableState.isFlipped.value },
modifier = modifier.graphicsLayer {
this.rotationX = flippableState.rotationAnimationValue.value
this.cameraDistance = 8 * this.density
},
rotation = flippableState.innerRotation,
cardContent = flippableState.contentToShow,
)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.jacob.wakatimeapp.core.ui.components.cards

import androidx.annotation.DrawableRes
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
Expand All @@ -17,10 +16,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
Expand All @@ -30,6 +25,7 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.jacob.wakatimeapp.core.ui.WtaPreviews
import com.jacob.wakatimeapp.core.ui.components.rememberFlippableState
import com.jacob.wakatimeapp.core.ui.theme.WakaTimeAppTheme
import com.jacob.wakatimeapp.core.ui.theme.assets
import com.jacob.wakatimeapp.core.ui.theme.colors.Gradient
Expand Down Expand Up @@ -60,6 +56,7 @@ fun StatsChip(
painter = painterResource(iconId),
contentDescription = null,
colorFilter = ColorFilter.tint(gradient.onEndColor),
alpha = 0.2f,
modifier = Modifier
.padding(
end = MaterialTheme.spacing.small,
Expand All @@ -83,41 +80,27 @@ fun StatsChip(
}
}

/**
* [Rotating on Axis in 3D](https://www.youtube.com/watch?v=WdQUDHOwlgE&t=148s)
* [Resetting Animation for each click](https://stackoverflow.com/questions/78620347/repeat-animation-when-user-clicks-jetpack-compose-android)
* [Showing the back of the card correctly](https://medium.com/bilue/card-flip-animation-with-jetpack-compose-f60aaaad4ac9)
*/
@Composable
fun InteractableStatsChip(
fun FlippableStatsChip(
modifier: Modifier,
gradient: Gradient,
iconId: Int = MaterialTheme.assets.icons.time,
frontContent: @Composable ColumnScope.() -> Unit = {},
backContent: @Composable ColumnScope.() -> Unit = {},
) {
var isFlipped by remember { mutableStateOf(false) }
val rotationXAnimation = animateFloatAsState(targetValue = if (isFlipped) 180f else 0f)

val contentToShow = remember(rotationXAnimation.value) {
if (rotationXAnimation.value < 90f) frontContent else backContent
}

val innerRotation = remember(rotationXAnimation.value) {
if (rotationXAnimation.value < 90f) 0f else 180f
}
val flippableState = rememberFlippableState(frontContent, backContent)

StatsChip(
gradient = gradient,
iconId = iconId,
roundedCornerPercent = 15,
onClick = { isFlipped = !isFlipped },
onClick = { flippableState.isFlipped.value = !flippableState.isFlipped.value },
modifier = modifier.graphicsLayer {
this.rotationX = rotationXAnimation.value
this.rotationX = flippableState.rotationAnimationValue.value
this.cameraDistance = 8 * this.density
},
rotation = innerRotation,
chipContent = { contentToShow() },
rotation = flippableState.innerRotation,
chipContent = flippableState.contentToShow,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import com.jacob.wakatimeapp.core.models.Streak
import com.jacob.wakatimeapp.core.ui.components.cards.InteractableStatsChip
import com.jacob.wakatimeapp.core.ui.components.cards.FlippableStatsChip
import com.jacob.wakatimeapp.core.ui.theme.assets
import com.jacob.wakatimeapp.core.ui.theme.colors.Gradient
import com.jacob.wakatimeapp.core.ui.theme.gradients
import com.jacob.wakatimeapp.core.ui.theme.spacing
import com.jacob.wakatimeapp.details.ui.DetailsPageViewState
Expand All @@ -20,9 +22,10 @@ internal fun ProjectStreakChips(detailsPageData: DetailsPageViewState.Loaded, mo
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.small),
) {
InteractableStatsChip(
FlippableStatsChip(
modifier = Modifier.weight(1f),
gradient = MaterialTheme.gradients.shifter,
iconId = MaterialTheme.assets.icons.longestStreak,
frontContent = {
ChipContent(
cardHeading = "${detailsPageData.longestStreakInProject.days} Days",
Expand All @@ -31,28 +34,33 @@ internal fun ProjectStreakChips(detailsPageData: DetailsPageViewState.Loaded, mo
statValueTextStyle = MaterialTheme.typography.titleMedium,
)
},
backContent = { StreakRangeDisplay(detailsPageData.longestStreakInProject) },
backContent = {
StreakRangeDisplay(
detailsPageData.longestStreakInProject,
MaterialTheme.gradients.shifter,
)
},
)

InteractableStatsChip(
FlippableStatsChip(
modifier = Modifier.weight(1f),
gradient = MaterialTheme.gradients.shifter,
gradient = MaterialTheme.gradients.reef,
iconId = MaterialTheme.assets.icons.streak,
frontContent = {
ChipContent(
cardHeading = "${detailsPageData.currentStreakInProject.days} Days",
cardSubHeading = "Current Streak",
gradient = MaterialTheme.gradients.shifter,
gradient = MaterialTheme.gradients.reef,
statValueTextStyle = MaterialTheme.typography.titleMedium,
)
},
backContent = { StreakRangeDisplay(detailsPageData.currentStreakInProject) },
backContent = { StreakRangeDisplay(detailsPageData.currentStreakInProject, MaterialTheme.gradients.reef) },
)
}
}

@Composable
private fun StreakRangeDisplay(streak: Streak) {
val gradient = MaterialTheme.gradients.shifter
private fun StreakRangeDisplay(streak: Streak, gradient: Gradient) {
Text(
text = streak.formattedPrintRange(),
color = gradient.onStartColor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.jacob.wakatimeapp.core.models.Time
Expand All @@ -16,8 +15,8 @@ import com.jacob.wakatimeapp.core.models.secondarystats.Languages
import com.jacob.wakatimeapp.core.models.secondarystats.Machines
import com.jacob.wakatimeapp.core.models.secondarystats.OperatingSystems
import com.jacob.wakatimeapp.core.ui.WtaPreviews
import com.jacob.wakatimeapp.core.ui.components.cards.InteractableStatsChip
import com.jacob.wakatimeapp.core.ui.components.cards.StatsCard
import com.jacob.wakatimeapp.core.ui.components.cards.FlippableStatsCard
import com.jacob.wakatimeapp.core.ui.components.cards.FlippableStatsChip
import com.jacob.wakatimeapp.core.ui.theme.WakaTimeAppTheme
import com.jacob.wakatimeapp.core.ui.theme.assets
import com.jacob.wakatimeapp.core.ui.theme.gradients
Expand Down Expand Up @@ -49,40 +48,33 @@ internal fun QuickStatsCards(detailsPageData: DetailsPageViewState.Loaded) {
Modifier.fillMaxWidth(),
Arrangement.spacedBy(MaterialTheme.spacing.sMedium),
) {
// TODO: MERGE BELOW 2 CARDS INTO 1 AND MAKE THEN INTERACTABLE, CARDS SWAP WHEN CLICKED
StatsCard(
gradient = MaterialTheme.gradients.amin,
roundedCornerPercent = 15,
iconId = MaterialTheme.assets.icons.time,
onClick = {},
) {
CardContent(
cardSubHeading = "Total Time on Project",
cardHeading = detailsPageData.totalTime.formattedPrint(),
gradient = MaterialTheme.gradients.amin,
)
}

StatsCard(
FlippableStatsCard(
gradient = MaterialTheme.gradients.purpink,
roundedCornerPercent = 15,
iconId = MaterialTheme.assets.icons.time,
onClick = {},
) {
CardContent(
cardSubHeading = "Average Time",
cardHeading = detailsPageData.averageTime.formattedPrint(),
gradient = MaterialTheme.gradients.purpink,
)
}
frontContent = {
CardContent(
cardSubHeading = "Total Time on Project",
cardHeading = detailsPageData.totalTime.formattedPrint(),
gradient = MaterialTheme.gradients.purpink,
)
},
backContent = {
CardContent(
cardSubHeading = "Average Time",
cardHeading = detailsPageData.averageTime.formattedPrint(),
gradient = MaterialTheme.gradients.purpink,
)
},
)

ProjectStreakChips(detailsPageData)

Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.small),
) {
InteractableStatsChip(
FlippableStatsChip(
modifier = Modifier.weight(1f),
gradient = MaterialTheme.gradients.quepal,
frontContent = {
Expand All @@ -102,7 +94,7 @@ internal fun QuickStatsCards(detailsPageData: DetailsPageViewState.Loaded) {
)
},
)
InteractableStatsChip(
FlippableStatsChip(
modifier = Modifier.weight(1f),
gradient = MaterialTheme.gradients.flare,
frontContent = {
Expand Down Expand Up @@ -135,10 +127,10 @@ internal fun QuickStatsCards(detailsPageData: DetailsPageViewState.Loaded) {
// after clicking/expanding
// 🟥🟥
// 🟥🟥
Text("Most used language")
Text("Most used editor")
Text("Most used os")
Text("Most used machine")
// Text("Most used language")
// Text("Most used editor")
// Text("Most used os")
// Text("Most used machine")
}
}
}
Expand Down

0 comments on commit a893eed

Please sign in to comment.