diff --git a/app/src/main/java/com/cornellappdev/score/components/ScoreBox.kt b/app/src/main/java/com/cornellappdev/score/components/ScoreBox.kt index 088e0f6..e6a7951 100644 --- a/app/src/main/java/com/cornellappdev/score/components/ScoreBox.kt +++ b/app/src/main/java/com/cornellappdev/score/components/ScoreBox.kt @@ -2,186 +2,408 @@ package com.cornellappdev.score.components import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import com.cornellappdev.score.model.GameData +import com.cornellappdev.score.model.ScoresByPeriod import com.cornellappdev.score.model.TeamScore import com.cornellappdev.score.theme.CrimsonPrimary import com.cornellappdev.score.theme.GrayMedium import com.cornellappdev.score.theme.GrayPrimary -import com.cornellappdev.score.theme.Style.bodyNormal -import com.cornellappdev.score.theme.Style.labelsNormal +import com.cornellappdev.score.theme.Style.metricNormal +import com.cornellappdev.score.theme.Style.metricSmallNormal import com.cornellappdev.score.theme.saturatedGreen import com.cornellappdev.score.util.emptyGameData +import com.cornellappdev.score.util.extraLongGameData import com.cornellappdev.score.util.gameData import com.cornellappdev.score.util.longGameData import com.cornellappdev.score.util.mediumGameData +import com.cornellappdev.score.util.shortGameData + +private val HEADER_HEIGHT = 35.dp +private const val SMALL_BOX_SIZE = 4 +private const val LARGE_BOX_SIZE = 10 @Composable -fun BoxScore(gameData: GameData) { - val maxPeriods = maxOf( - gameData.teamScores.first.scoresByPeriod.size, - gameData.teamScores.second.scoresByPeriod.size - ) - val rowTextStyle = if (maxPeriods > 4) labelsNormal else bodyNormal +fun BoxScore( + gameData: GameData, + modifier: Modifier = Modifier +) { + Surface( + modifier = modifier + //set height required for CompleteLazyTableData case + .height(160.dp) + .fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + border = (BorderStroke(width = 1.dp, color = CrimsonPrimary)), + shadowElevation = 8.dp, + color = Color.White + ) { + Row( + modifier = Modifier + .fillMaxWidth() + ) { + val rowTextStyle = if (gameData.maxPeriods > SMALL_BOX_SIZE) { + metricSmallNormal + } else { + metricNormal + } + TeamNameColumn( + gameData.teamScores.first.team.name, + gameData.teamScores.second.team.name, + rowTextStyle, + gameData.maxPeriods + ) + if (gameData.maxPeriods > LARGE_BOX_SIZE) { + CompleteLazyTableData( + gameData = gameData, + rowTextStyle = rowTextStyle + ) + } else { + CompleteTableData( + gameData = gameData, + rowTextStyle = rowTextStyle, + maxPeriods = gameData.maxPeriods + ) + } + } + } +} + +@Composable +private fun TeamNameColumn( + teamOneName: String, + teamTwoName: String, + rowTextStyle: TextStyle, + maxPeriods: Int, + modifier: Modifier = Modifier +) { Column( - modifier = Modifier - .fillMaxWidth() - .clip(shape = RoundedCornerShape(8.dp)) - .background(color = Color.White, shape = RoundedCornerShape(8.dp)) - .border(BorderStroke(width = 1.dp, color = CrimsonPrimary)) + modifier = if (maxPeriods > 4) { + //set to approximation of minimum width for "Cornell" + modifier.widthIn(max = 70.dp) + } else { + modifier.widthIn(max = 100.dp) + } + ) { + Spacer( + modifier = Modifier + .fillMaxWidth() + .background(color = CrimsonPrimary) + .height(HEADER_HEIGHT) + .padding(top = 6.dp, bottom = 4.dp), + ) + Row( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(top = 8.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = teamOneName, + style = rowTextStyle, + color = GrayPrimary, + modifier = Modifier + .weight(1f, fill = true) + .padding(10.dp), + textAlign = TextAlign.Left, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + HorizontalDivider(thickness = 1.dp, color = CrimsonPrimary) + Row( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(top = 8.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = teamTwoName, + style = rowTextStyle, + color = GrayPrimary, + modifier = Modifier + .weight(1f, fill = true) + .padding(10.dp), + textAlign = TextAlign.Left, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Composable +private fun TableDataColumn( + header: Int, + teamOneScore: String, + teamTwoScore: String, + rowTextStyle: TextStyle, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxWidth() ) { Row( modifier = Modifier .fillMaxWidth() .background(color = CrimsonPrimary) - .padding(top = 6.dp, bottom = 4.dp, end = 8.dp), + .height(HEADER_HEIGHT), + horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Text( - text = "", - modifier = Modifier.weight(1f), + text = header.toString(), color = Color.White, + style = rowTextStyle, textAlign = TextAlign.Center ) - repeat(maxPeriods) { period -> - Text( - text = "${period + 1}", - modifier = Modifier.weight(1f), - color = Color.White, - style = rowTextStyle, - textAlign = TextAlign.Center - ) - } + } + Row( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { Text( - text = "Total", - modifier = Modifier.weight(1f), + text = teamOneScore, style = rowTextStyle, - color = Color.White, textAlign = TextAlign.Center ) } - TeamScoreRow( - teamScore = gameData.teamScores.first, - totalTextColor = saturatedGreen, - maxPeriods, - rowTextStyle - ) HorizontalDivider(thickness = 1.dp, color = CrimsonPrimary) + Row( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = teamTwoScore, + style = rowTextStyle, + textAlign = TextAlign.Center + ) + } + } +} - TeamScoreRow( - teamScore = gameData.teamScores.second, - totalTextColor = GrayMedium, - maxPeriods, - rowTextStyle - ) +@Composable +private fun CompleteLazyTableData( + gameData: GameData, + rowTextStyle: TextStyle, + modifier: Modifier = Modifier +) { + val periodScores = gameData.mapToPeriodScores() + + if (periodScores.isNotEmpty()) { + Row( + modifier = modifier + ) { + LazyRow( + modifier = Modifier.weight(1f) + ) { + items(periodScores) { periodScore -> + TableDataColumn( + header = periodScore.header, + teamOneScore = periodScore.teamOneScore, + teamTwoScore = periodScore.teamTwoScore, + rowTextStyle = rowTextStyle, + modifier = Modifier.width(35.dp) + ) + } + } + TotalsColumn( + teamOneScores = gameData.teamScores.first, + teamTwoScores = gameData.teamScores.second, + rowTextStyle = rowTextStyle + ) + } + } +} + +@Composable +private fun CompleteTableData( + gameData: GameData, + rowTextStyle: TextStyle, + maxPeriods: Int, + modifier: Modifier = Modifier +) { + val periodScores = gameData.mapToPeriodScores() + + Row( + modifier = modifier, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + if (periodScores.isNotEmpty()) { + periodScores.map { periodScore -> + TableDataColumn( + header = periodScore.header, + teamOneScore = periodScore.teamOneScore, + teamTwoScore = periodScore.teamTwoScore, + rowTextStyle = rowTextStyle, + modifier = Modifier.weight(1f) + ) + } + } else { + for (i in 1..4) { + TableDataColumn( + header = i, + teamOneScore = "-", + teamTwoScore = "-", + rowTextStyle = rowTextStyle, + modifier = Modifier.weight(1f) + ) + } + } + TotalsColumn( + teamOneScores = gameData.teamScores.first, + teamTwoScores = gameData.teamScores.second, + //if maxPeriods > 8, "Totals" header will wrap to two lines. In this case, don't weight so that space is allocated to TotalsColumn first +//otherwise, TotalsColumn will fit without wrapping and can be allocated equal width as the data columns + modifier = if (maxPeriods < 4) { + Modifier.weight(1f, fill = true) + } else { + Modifier + }, + rowTextStyle = rowTextStyle + ) } } @Composable -fun TeamScoreRow( +private fun TotalScoreCell( teamScore: TeamScore, totalTextColor: Color, - maxPeriods: Int, - rowTextStyle: TextStyle + rowTextStyle: TextStyle, + modifier: Modifier = Modifier ) { val showEmpty = teamScore.scoresByPeriod.isEmpty() - Row( - modifier = Modifier + modifier = modifier .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = 10.dp), - horizontalArrangement = Arrangement.SpaceBetween, + .padding(top = 8.dp, bottom = 8.dp, end = 8.dp), + horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Text( - text = teamScore.team.name, + text = if (showEmpty) "-" else teamScore.totalScore.toString(), style = rowTextStyle, - color = GrayPrimary, - modifier = Modifier.weight(1f), + color = if (showEmpty) Color.Gray else totalTextColor, + fontWeight = if (showEmpty) FontWeight.Normal else FontWeight.Bold, textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Ellipsis + modifier = Modifier.fillMaxWidth() ) + } +} - teamScore.scoresByPeriod.forEach { score -> +@Composable +private fun TotalsColumn( + teamOneScores: TeamScore, + teamTwoScores: TeamScore, + rowTextStyle: TextStyle, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .width(IntrinsicSize.Min) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(color = CrimsonPrimary) + .padding(end = 8.dp) + .height(HEADER_HEIGHT), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { Text( - text = if (showEmpty) "-" else score.toString(), - modifier = Modifier.weight(1f), + text = "Total", style = rowTextStyle, - color = GrayPrimary, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + color = Color.White ) } - repeat(maxPeriods - teamScore.scoresByPeriod.size) { - Text( - text = "-", - modifier = Modifier.weight(1f), - style = rowTextStyle, - color = GrayPrimary, - textAlign = TextAlign.Center - ) - } + val totalOne = teamOneScores.totalScore + val totalTwo = teamTwoScores.totalScore - Text( - text = if (showEmpty) "-" else teamScore.totalScore.toString(), - modifier = Modifier.weight(1f), - style = rowTextStyle, - color = if (showEmpty) Color.Gray else totalTextColor, - fontWeight = if (showEmpty) FontWeight.Normal else FontWeight.Bold, - textAlign = TextAlign.Center + TotalScoreCell( + teamScore = teamOneScores, + totalTextColor = if (totalOne > totalTwo) { + saturatedGreen + } else { + GrayMedium + }, + rowTextStyle = rowTextStyle, + modifier = Modifier.weight(1f) + ) + HorizontalDivider(thickness = 1.dp, color = CrimsonPrimary) + TotalScoreCell( + teamScore = teamTwoScores, + totalTextColor = if (totalTwo > totalOne) { + saturatedGreen + } else { + GrayMedium + }, + rowTextStyle = rowTextStyle, + modifier = Modifier.weight(1f) ) } } - -@Preview -@Composable -private fun PreviewBoxScore() = ScorePreview { - BoxScore(gameData = gameData) -} - -@Preview -@Composable -private fun PreviewBoxScoreForLongGame() = ScorePreview { - BoxScore(longGameData) -} - -@Preview -@Composable -private fun PreviewBoxScoreForMedGame() = ScorePreview { - BoxScore(mediumGameData) +class GameDataPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + gameData, + longGameData, + mediumGameData, + shortGameData, + extraLongGameData, + emptyGameData() + ) } -@Preview +@Preview(showBackground = true) @Composable -private fun PreviewBoxScoreEmpty() = ScorePreview { - BoxScore(gameData = emptyGameData()) -} - - -@Preview -@Composable -private fun PreviewTeamScoreRow() = ScorePreview { - TeamScoreRow(gameData.teamScores.first, GrayMedium, 4, bodyNormal) +private fun BoxScoreParameterizedPreview( + @PreviewParameter(GameDataPreviewProvider::class) sample: GameData +) { + ScorePreview { + BoxScore( + gameData = sample, + modifier = Modifier.padding(start = 20.dp, end = 20.dp) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/model/Game.kt b/app/src/main/java/com/cornellappdev/score/model/Game.kt index 18ed1a2..d6c93d7 100644 --- a/app/src/main/java/com/cornellappdev/score/model/Game.kt +++ b/app/src/main/java/com/cornellappdev/score/model/Game.kt @@ -129,6 +129,13 @@ data class DetailsCardData( val oppScore: Int ) +// Scoring information by round of a game, used in the box score +data class ScoresByPeriod( + val header: Int, + val teamOneScore: String, + val teamTwoScore: String +) + // Scoring information for a specific team, used in the box score data class TeamScore( val team: TeamBoxScore, @@ -139,7 +146,31 @@ data class TeamScore( // Aggregated game data showing scores for both teams data class GameData( val teamScores: Pair -) +){ + val maxPeriods: Int + get() = + maxOf( + teamScores.first.scoresByPeriod.size, + teamScores.second.scoresByPeriod.size + ) + + fun mapToPeriodScores(): List { + val teamOneScores = this.teamScores.first.scoresByPeriod + val teamTwoScores = this.teamScores.second.scoresByPeriod + + val maxPeriods = maxOf(teamOneScores.size, teamTwoScores.size) + + return (0 until maxPeriods).map { i -> + ScoresByPeriod( + header = i + 1, + teamOneScore = teamOneScores.getOrNull(i)?.toString() ?: "-", + teamTwoScore = teamTwoScores.getOrNull(i)?.toString() ?: "-" + + ) + + } + } +} /** * Represents a scoring event in a game. diff --git a/app/src/main/java/com/cornellappdev/score/model/Sport.kt b/app/src/main/java/com/cornellappdev/score/model/Sport.kt index 5e8fd07..bcf68cb 100644 --- a/app/src/main/java/com/cornellappdev/score/model/Sport.kt +++ b/app/src/main/java/com/cornellappdev/score/model/Sport.kt @@ -87,8 +87,8 @@ enum class Sport( LACROSSE( displayName = "Lacrosse", gender = GenderDivision.ALL, - emptyIcon = R.drawable.ic_squash, - filledIcon = R.drawable.ic_squash_filled + emptyIcon = R.drawable.ic_lacrosse, + filledIcon = R.drawable.ic_lacrosse_filled ), ROWING_HEAVYWEIGHT( displayName = "Heavyweight Rowing", diff --git a/app/src/main/java/com/cornellappdev/score/theme/TextStyle.kt b/app/src/main/java/com/cornellappdev/score/theme/TextStyle.kt index a09e045..c2fd46e 100644 --- a/app/src/main/java/com/cornellappdev/score/theme/TextStyle.kt +++ b/app/src/main/java/com/cornellappdev/score/theme/TextStyle.kt @@ -196,6 +196,12 @@ object Style { fontWeight = FontWeight(500) ) + val metricSmallNormal = TextStyle( + fontSize = 14.sp, + fontFamily = poppinsFamily, + fontWeight = FontWeight(400) + ) + val metricNormal = TextStyle( fontSize = 18.sp, fontFamily = poppinsFamily, diff --git a/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt b/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt index 382ffc6..6f5cd35 100644 --- a/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt +++ b/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt @@ -55,7 +55,7 @@ val gameList = listOf( ) val team1 = TeamBoxScore(name = "Cornell") -val team2 = TeamBoxScore(name = "Yale") +val team2 = TeamBoxScore(name = "Yale University") val teamScore1 = TeamScore( team = team1, @@ -68,16 +68,28 @@ val teamScore2 = TeamScore( totalScore = 23 ) -val mediumGameTeamScore1 = TeamScore( +val shortGameTeamScore1 = TeamScore( team = team1, - scoresByPeriod = listOf(13, 14, 6, 14, 13, 2), - totalScore = 62 + scoresByPeriod = listOf(13, 14), + totalScore = 27 ) -val mediumGameTeamScore2 = TeamScore( + +val shortGameTeamScore2 = TeamScore( + team = team2, + scoresByPeriod = listOf(7, 7), + totalScore = 14 +) + +val mediumGameTeamScore1 = TeamScore( team = team1, scoresByPeriod = listOf(7, 7, 9, 0, 7, 7), totalScore = 37 ) +val mediumGameTeamScore2 = TeamScore( + team = team2, + scoresByPeriod = listOf(13, 14, 6, 14, 13, 2), + totalScore = 62 +) val longGameTeamScore1 = TeamScore( team = team1, @@ -89,18 +101,35 @@ val longGameTeamScore2 = TeamScore( scoresByPeriod = listOf(7, 7, 9, 0, 7, 7, 9, 0, 7, 2), totalScore = 47 ) + +val extraLongGameTeamScore1 = TeamScore( + team = team1, + scoresByPeriod = listOf(13, 14, 6, 14, 13, 2, 4, 6, 2, 2, 3, 4), + totalScore = 54 +) + +val extraLongGameTeamScore2 = TeamScore( + team = team1, + scoresByPeriod = listOf(7, 7, 9, 0, 7, 7, 9, 0, 7, 2, 1, 1), + totalScore = 49 +) + val gameData = GameData(teamScores = Pair(teamScore1, teamScore2)) +val shortGameData = GameData(teamScores = shortGameTeamScore1 to shortGameTeamScore2) + val mediumGameData = GameData(teamScores = mediumGameTeamScore1 to mediumGameTeamScore2) val longGameData = GameData(teamScores = longGameTeamScore1 to longGameTeamScore2) +val extraLongGameData = GameData(teamScores = extraLongGameTeamScore1 to extraLongGameTeamScore2) + val team3 = TeamGameSummary( name = "Cornell", "https://cornellbigred.com/images/logos/penn_200x200.png?width=80&height=80&mode=max" ) val team4 = TeamGameSummary( - name = "Yale", + name = "Yale University", "https://cornellbigred.com/images/logos/penn_200x200.png?width=80&height=80&mode=max" ) val scoreEvents1 = listOf(