diff --git a/app/src/main/java/org/sopt/stamp/designsystem/component/button/SoptampIconButton.kt b/app/src/main/java/org/sopt/stamp/designsystem/component/button/SoptampIconButton.kt new file mode 100644 index 0000000..a3b82da --- /dev/null +++ b/app/src/main/java/org/sopt/stamp/designsystem/component/button/SoptampIconButton.kt @@ -0,0 +1,59 @@ +package org.sopt.stamp.designsystem.component.button + +import androidx.compose.foundation.layout.Row +import androidx.compose.material.Icon +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import org.sopt.stamp.R +import org.sopt.stamp.designsystem.component.util.noRippleClickable +import org.sopt.stamp.designsystem.style.SoptTheme + +/** + * 앱 디자인 시스템 Icon Button 입니다. + * Icon에 들어갈 ImageVector 는 32*32로 통일된 크기를 가정합니다. + * + * @param imageVector [ImageVector] to draw inside this Icon + * + * @param contentDescription text used by accessibility services to describe what this icon + * represents. This should always be provided unless this icon is used for decorative purposes, + * and does not represent a meaningful action that a user can take. This text should be + * localized, such as by using [androidx.compose.ui.res.stringResource] or similar + * + * @param tint tint to be applied to [imageVector]. If [Color.Unspecified] is provided, then no + * tint is applied + * + * @author jinsu4755 + * */ +@Composable +fun SoptampIconButton( + imageVector: ImageVector, + contentDescription: String? = null, + tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current), + onClick: () -> Unit = {} +) { + Row( + modifier = Modifier.noRippleClickable { onClick() } + ) { + Icon( + imageVector = imageVector, + contentDescription = contentDescription, + tint = tint + ) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewTopBarIconButton() { + SoptTheme { + SoptampIconButton( + imageVector = ImageVector.vectorResource(id = R.drawable.up_expand) + ) + } +} diff --git a/app/src/main/java/org/sopt/stamp/designsystem/component/mission/CompletedStamp.kt b/app/src/main/java/org/sopt/stamp/designsystem/component/mission/CompletedStamp.kt new file mode 100644 index 0000000..03a2fbb --- /dev/null +++ b/app/src/main/java/org/sopt/stamp/designsystem/component/mission/CompletedStamp.kt @@ -0,0 +1,20 @@ +package org.sopt.stamp.designsystem.component.mission + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.rememberLottieComposition + +@Composable +fun CompletedStamp( + stamp: Stamp, + modifier: Modifier +) { + val completedStamp by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(stamp.lottie)) + LottieAnimation( + composition = completedStamp, + modifier = modifier + ) +} diff --git a/app/src/main/java/org/sopt/stamp/designsystem/component/mission/LevelOfMission.kt b/app/src/main/java/org/sopt/stamp/designsystem/component/mission/LevelOfMission.kt new file mode 100644 index 0000000..1a1281b --- /dev/null +++ b/app/src/main/java/org/sopt/stamp/designsystem/component/mission/LevelOfMission.kt @@ -0,0 +1,35 @@ +package org.sopt.stamp.designsystem.component.mission + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import org.sopt.stamp.R +import org.sopt.stamp.domain.MissionLevel + +@Composable +fun LevelOfMission(stamp: Stamp, spaceSize: Dp) { + Row( + horizontalArrangement = Arrangement.spacedBy(spaceSize) + ) { + MissionLevelOfStar(stamp = stamp) + } +} + +@Composable +private fun MissionLevelOfStar(stamp: Stamp) { + repeat(MissionLevel.MAXIMUM_LEVEL) { + val starColor = if (it <= stamp.missionLevel.value) { + stamp.starColor + } else { + Stamp.defaultStarColor + } + Icon( + painter = painterResource(id = R.drawable.level_star), + contentDescription = "Star Of Mission Level", + tint = starColor + ) + } +} diff --git a/app/src/main/java/org/sopt/stamp/designsystem/component/mission/MissionComponent.kt b/app/src/main/java/org/sopt/stamp/designsystem/component/mission/MissionComponent.kt new file mode 100644 index 0000000..f497cd4 --- /dev/null +++ b/app/src/main/java/org/sopt/stamp/designsystem/component/mission/MissionComponent.kt @@ -0,0 +1,90 @@ +package org.sopt.stamp.designsystem.component.mission + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.stamp.designsystem.component.mission.model.MissionUiModel +import org.sopt.stamp.designsystem.component.util.noRippleClickable +import org.sopt.stamp.designsystem.style.SoptTheme +import org.sopt.stamp.domain.MissionLevel + +@Composable +fun MissionComponent( + mission: MissionUiModel, + modifier: Modifier = Modifier, + onClick: (() -> Unit) = {} +) { + val shape = MissionShape.DEFAULT_WAVE + val stamp = Stamp.findStampByLevel(mission.level) + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Column( + modifier = modifier.defaultMinSize(160.dp, 200.dp).background( + color = if (mission.isCompleted) stamp.background else SoptTheme.colors.onSurface5, + shape = shape + ).noRippleClickable { onClick() }, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + if (mission.isCompleted) { + CompletedStamp( + stamp = stamp, + modifier = Modifier.size(104.dp) + ) + Spacer(modifier = Modifier.size(8.dp)) + } else { + LevelOfMission(stamp = stamp, spaceSize = 10.dp) + Spacer(modifier = Modifier.size(16.dp)) + } + TitleOfMission(missionTitle = mission.title) + } + } +} + +@Composable +private fun TitleOfMission(missionTitle: String) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = missionTitle, + style = SoptTheme.typography.sub3, + textAlign = TextAlign.Center, + maxLines = 2, + modifier = Modifier.fillMaxWidth(0.8875f) + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xff000000) +@Composable +fun PreviewMissionComponent() { + SoptTheme { + val previewMission = MissionUiModel( + id = 1, + title = "일이삼사오육칠팔구십일일이삼사오육칠팔구십일", + level = MissionLevel.of(1), + isCompleted = !false + ) + MissionComponent( + modifier = Modifier.width(160.dp), + mission = previewMission + ) + } +} diff --git a/app/src/main/java/org/sopt/stamp/designsystem/component/mission/MissionShape.kt b/app/src/main/java/org/sopt/stamp/designsystem/component/mission/MissionShape.kt new file mode 100644 index 0000000..0fabe23 --- /dev/null +++ b/app/src/main/java/org/sopt/stamp/designsystem/component/mission/MissionShape.kt @@ -0,0 +1,155 @@ +package org.sopt.stamp.designsystem.component.mission + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.sopt.stamp.designsystem.component.mission.model.MissionPattern +import org.sopt.stamp.designsystem.style.SoptTheme + +internal class MissionShape( + private val patternCount: Int +) : Shape { + + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline = Outline.Generic(drawMissionPatternPath(size)) + + private fun drawMissionPatternPath(size: Size): Path { + val pattern = MissionPattern(calculatePatternLength(size)) + return drawMissionPatternPath(size, pattern) + } + + private fun calculatePatternLength(size: Size): Float { + return size.width * TOTAL_PATTERN_LENGTH / patternCount + } + + private fun drawMissionPatternPath(size: Size, pattern: MissionPattern): Path = Path().apply { + val sideSize = size.width * SIDE_SIZE_RATIO + reset() + moveTo(0f, 0f) + lineTo(sideSize, 0f) + drawTopMissionPattern(size, pattern) + lineTo(size.width, 0f) + lineTo(size.width, size.height) + lineTo(size.width - sideSize, size.height) + drawBottomMissionPattern(size, pattern) + lineTo(0f, size.height) + lineTo(0f, 0f) + close() + } + + private fun Path.drawTopMissionPattern(size: Size, pattern: MissionPattern) { + val sideSize = size.width * SIDE_SIZE_RATIO + val rectHeight = pattern.diameter / 2 + for (i in 1..patternCount) { + lineTo(sideSize + pattern.length * (i - 1) + pattern.gap, 0f) + arcTo( + rect = Rect( + topLeft = Offset( + x = sideSize + pattern.length * (i - 1) + pattern.gap, + y = -rectHeight + ), + bottomRight = Offset( + x = sideSize + pattern.length * (i - 1) + pattern.gap + pattern.diameter, + y = rectHeight + ) + ), + startAngleDegrees = 180f, + sweepAngleDegrees = -180f, + forceMoveTo = false + ) + lineTo(sideSize + pattern.length * i, 0f) + } + } + + private fun Path.drawBottomMissionPattern(size: Size, pattern: MissionPattern) { + val sideSize = size.width * SIDE_SIZE_RATIO + val rectHeight = pattern.diameter / 2 + for (i in 1..patternCount) { + lineTo(size.width - (sideSize + pattern.length * (i - 1) + pattern.gap), size.height) + arcTo( + rect = Rect( + topLeft = Offset( + x = size.width - (sideSize + pattern.gap + pattern.length * (i - 1) + pattern.diameter), + y = size.height - rectHeight + ), + bottomRight = Offset( + x = size.width - (sideSize + pattern.gap + pattern.length * (i - 1)), + y = size.height + rectHeight + ) + ), + startAngleDegrees = 0f, + sweepAngleDegrees = -180f, + forceMoveTo = false + ) + lineTo(size.width - (sideSize + pattern.length * i), size.height) + } + } + + companion object { + val DEFAULT_WAVE: MissionShape = MissionShape(6) + private const val TOTAL_PATTERN_LENGTH = 0.9f + private const val SIDE_SIZE_RATIO: Float = 0.05f + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewMissionShape() { + SoptTheme { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + val shape = MissionShape.DEFAULT_WAVE + Card( + modifier = Modifier + .fillMaxWidth(0.8f) + .fillMaxHeight(0.6f), + shape = shape + ) { + Column( + modifier = Modifier + .fillMaxSize() + .background(shape = shape, color = Color.Green), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Ticket Price : 20$", + modifier = Modifier + .padding(30.dp), + color = Color.Black, + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + } + } + } + } +} diff --git a/app/src/main/java/org/sopt/stamp/designsystem/component/mission/Stamp.kt b/app/src/main/java/org/sopt/stamp/designsystem/component/mission/Stamp.kt new file mode 100644 index 0000000..b38a8a7 --- /dev/null +++ b/app/src/main/java/org/sopt/stamp/designsystem/component/mission/Stamp.kt @@ -0,0 +1,45 @@ +package org.sopt.stamp.designsystem.component.mission + +import androidx.annotation.RawRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import org.sopt.stamp.R +import org.sopt.stamp.designsystem.style.SoptTheme +import org.sopt.stamp.domain.MissionLevel + +enum class Stamp( + val missionLevel: MissionLevel, + @RawRes val lottie: Int +) { + LEVEL1(MissionLevel.of(1), R.raw.pinkstamps), LEVEL2(MissionLevel.of(2), R.raw.purplestamp), LEVEL3( + MissionLevel.of(3), + R.raw.greenstamp + ); + + val starColor: Color + @Composable get() = when (this) { + LEVEL1 -> SoptTheme.colors.pink300 + LEVEL2 -> SoptTheme.colors.purple300 + LEVEL3 -> SoptTheme.colors.mint300 + } + + val background: Color + @Composable get() = when (this) { + LEVEL1 -> SoptTheme.colors.pink100 + LEVEL2 -> SoptTheme.colors.purple100 + LEVEL3 -> SoptTheme.colors.mint100 + } + + fun hasStampLevel(level: MissionLevel): Boolean { + return this.missionLevel == level + } + + companion object { + val defaultStarColor: Color + @Composable get() = SoptTheme.colors.onSurface30 + + fun findStampByLevel(level: MissionLevel): Stamp = values().find { + it.hasStampLevel(level) + } ?: throw IllegalArgumentException("$level 에 해당하는 Stamp 가 없습니다.") + } +} diff --git a/app/src/main/java/org/sopt/stamp/designsystem/component/mission/model/MissionPattern.kt b/app/src/main/java/org/sopt/stamp/designsystem/component/mission/model/MissionPattern.kt new file mode 100644 index 0000000..b283b73 --- /dev/null +++ b/app/src/main/java/org/sopt/stamp/designsystem/component/mission/model/MissionPattern.kt @@ -0,0 +1,20 @@ +package org.sopt.stamp.designsystem.component.mission.model + +internal class MissionPattern private constructor( + val length: Float, + val diameter: Float, + val gap: Float +) { + companion object { + private const val DIAMETER_RATIO: Float = 0.58f + private const val GAP_RATIO: Float = 0.21f + + operator fun invoke(length: Float): MissionPattern { + return MissionPattern( + length = length, + diameter = length * DIAMETER_RATIO, + gap = length * GAP_RATIO + ) + } + } +} diff --git a/app/src/main/java/org/sopt/stamp/designsystem/component/mission/model/MissionUiModel.kt b/app/src/main/java/org/sopt/stamp/designsystem/component/mission/model/MissionUiModel.kt new file mode 100644 index 0000000..908d147 --- /dev/null +++ b/app/src/main/java/org/sopt/stamp/designsystem/component/mission/model/MissionUiModel.kt @@ -0,0 +1,10 @@ +package org.sopt.stamp.designsystem.component.mission.model + +import org.sopt.stamp.domain.MissionLevel + +data class MissionUiModel( + val id: Int, + val title: String, + val level: MissionLevel, + val isCompleted: Boolean +) diff --git a/app/src/main/java/org/sopt/stamp/designsystem/component/topappbar/SoptAppBarDefault.kt b/app/src/main/java/org/sopt/stamp/designsystem/component/topappbar/SoptAppBarDefault.kt new file mode 100644 index 0000000..4abf331 --- /dev/null +++ b/app/src/main/java/org/sopt/stamp/designsystem/component/topappbar/SoptAppBarDefault.kt @@ -0,0 +1,9 @@ +package org.sopt.stamp.designsystem.component.topappbar + +import androidx.compose.ui.unit.dp + +object SoptAppBarDefault { + val height = 56.dp + val appBarDefaultHorizontalPadding = 20.dp + val appBarDefaultVerticalPadding = 12.dp +} diff --git a/app/src/main/java/org/sopt/stamp/designsystem/component/topappbar/SoptTopAppBar.kt b/app/src/main/java/org/sopt/stamp/designsystem/component/topappbar/SoptTopAppBar.kt new file mode 100644 index 0000000..3e2fc6e --- /dev/null +++ b/app/src/main/java/org/sopt/stamp/designsystem/component/topappbar/SoptTopAppBar.kt @@ -0,0 +1,171 @@ +package org.sopt.stamp.designsystem.component.topappbar + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +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.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.sopt.stamp.R +import org.sopt.stamp.designsystem.component.button.SoptampIconButton +import org.sopt.stamp.designsystem.style.SoptTheme + +/** + * 기본적인 TopAppBar 를 디자인에 맞춰 커스텀 하였습니다. + * 일반적인 TopAppBar 와 다르지 않게 사용할 수 있습니다. + * + * @param title 앱바에서 보일 Title 입니다. + * @param modifier 앱바 내부 content 의 Modifier 를 설정합니다. + * @param navigationIcon 앱바 내부 navigationIcon 을 설정합니다. + * @param actions 앱바에서 가능한 액션 버튼 들을 설정합니다. + * @param backgroundColor 앱바의 backgroundColor 를 설정합니다. + * @param elevation 앱바의 elevation 을 설정합니다. + * + * @author jinsu4755*/ + +@Composable +fun SoptTopAppBar( + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + navigationIcon: @Composable (RowScope.() -> Unit)? = null, + dropDownButton: @Composable (RowScope.() -> Unit)? = null, + actions: @Composable (RowScope.() -> Unit) = {}, + backgroundColor: Color = Color.Transparent, + elevation: Dp = 0.dp +) { + SoptAppBar( + backgroundColor = backgroundColor, + elevation = elevation, + modifier = modifier.padding( + horizontal = SoptAppBarDefault.appBarDefaultHorizontalPadding, + vertical = SoptAppBarDefault.appBarDefaultVerticalPadding + ) + ) { + if (navigationIcon != null) { + navigationIcon() + Spacer(modifier = Modifier.size(2.dp)) + } + Row( + modifier = Modifier.fillMaxWidth() + .weight(1f), + verticalAlignment = Alignment.CenterVertically + ) { + title() + if (dropDownButton != null) { + Spacer(modifier = Modifier.size(2.dp)) + Row(content = dropDownButton) + } + } + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + content = actions + ) + } +} + +@Composable +fun SoptAppBar( + backgroundColor: Color, + elevation: Dp, + modifier: Modifier, + content: @Composable RowScope.() -> Unit +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .height(SoptAppBarDefault.height), + color = backgroundColor, + elevation = elevation + ) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + content = content + ) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewSoptTopBarOnlyMissionTitleAndDropDownMenu() { + SoptTheme { + SoptTopAppBar( + title = { Text(text = "hello") }, + dropDownButton = { + SoptampIconButton(imageVector = ImageVector.vectorResource(id = R.drawable.setting)) + } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewSoptTopBarTitleWithNavigationButton() { + SoptTheme { + SoptTopAppBar( + title = { Text(text = "hello") }, + navigationIcon = { + SoptampIconButton(imageVector = ImageVector.vectorResource(id = R.drawable.setting)) + } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewSoptTopBarTitleWithNavigationButtonAndActions() { + SoptTheme { + SoptTopAppBar( + title = { Text(text = "hello") }, + navigationIcon = { + SoptampIconButton( + imageVector = ImageVector.vectorResource(id = R.drawable.setting) + ) + }, + actions = { + SoptampIconButton( + imageVector = ImageVector.vectorResource(id = R.drawable.setting) + ) + } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewSoptTopBarTitleWithNavigationButtonAndActionText() { + SoptTheme { + SoptTopAppBar( + title = { Text(text = "hello") }, + navigationIcon = { + SoptampIconButton( + imageVector = ImageVector.vectorResource(id = R.drawable.setting) + ) + }, + actions = { + Text( + modifier = Modifier.clickable {}, + text = "취소" + ) + Spacer(modifier = Modifier.width(8.dp)) + } + ) + } +} diff --git a/app/src/main/java/org/sopt/stamp/designsystem/component/util/NoRippleClickable.kt b/app/src/main/java/org/sopt/stamp/designsystem/component/util/NoRippleClickable.kt new file mode 100644 index 0000000..c0844cc --- /dev/null +++ b/app/src/main/java/org/sopt/stamp/designsystem/component/util/NoRippleClickable.kt @@ -0,0 +1,16 @@ +package org.sopt.stamp.designsystem.component.util + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed + +inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed { + clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onClick() + } +} diff --git a/app/src/main/java/org/sopt/stamp/domain/MissionLevel.kt b/app/src/main/java/org/sopt/stamp/domain/MissionLevel.kt new file mode 100644 index 0000000..25f7e38 --- /dev/null +++ b/app/src/main/java/org/sopt/stamp/domain/MissionLevel.kt @@ -0,0 +1,40 @@ +package org.sopt.stamp.domain + +class MissionLevel private constructor( + val value: Int +) { + override fun toString(): String { + return "$value" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MissionLevel + + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + return value + } + + companion object { + const val MINIMUM_LEVEL = 1 + const val MAXIMUM_LEVEL = 3 + + fun of(level: Int): MissionLevel { + validateMissionLevel(level) + return MissionLevel(level) + } + + private fun validateMissionLevel(level: Int) { + require(level in MINIMUM_LEVEL..MAXIMUM_LEVEL) { + "Mission Level 은 $MINIMUM_LEVEL ~ $MAXIMUM_LEVEL 사이 값이어야 합니다." + } + } + } +} diff --git a/app/src/main/res/drawable/level_star.xml b/app/src/main/res/drawable/level_star.xml new file mode 100644 index 0000000..b865c8e --- /dev/null +++ b/app/src/main/res/drawable/level_star.xml @@ -0,0 +1,9 @@ + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2e9d14d..61c6d0f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -38,6 +38,7 @@ google-services = "4.3.14" crashlytics = "2.9.2" firebase = "31.1.1" ktlint = "11.0.0" +lottie = "5.2.0" [libraries] agp = { module = "com.android.tools.build:gradle", version.ref = "gradleplugin" } @@ -71,6 +72,7 @@ compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" compose-destination-core = { module = "io.github.raamcosta.compose-destinations:core", version.ref = "compose-destination" } compose-destination-ksp = { module = "io.github.raamcosta.compose-destinations:ksp", version.ref = "compose-destination" } compose-hilt-navigation = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "dagger-hilt-navigation-compose" } +compose-lottie = {module="com.airbnb.android:lottie-compose", version.ref = "lottie"} accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } @@ -123,7 +125,8 @@ compose = [ "compose-runtime", "compose-ui-tooling-preview", "compose-hilt-navigation", - "compose-material-three" + "compose-material-three", + "compose-lottie" ] compose-test = ["compose-junit"] compose-android-test = ["compose-ui-test"]