From 07ab728ae4bed8b70fff8f5e47314a33fba72e88 Mon Sep 17 00:00:00 2001 From: Gael-Android <84790707+Gael-Android@users.noreply.github.com> Date: Sun, 19 Jan 2025 19:59:27 +0900 Subject: [PATCH] =?UTF-8?q?Switch=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add : 스위치를 위한 기본 색상 추가 * add : 스위치 제작 * add : 스위치 프리뷰 추가 * chore : 스위치 에니메이션 변경 * chore : Preview 위치 변경 * chore : 스위치 주석 추가 및 포맷 통일 * chore : 주석추가 * chore : Preview 굳이 프리뷰파라미터로 만들 필요 없을거 같아서 Column으로 나타냄 * chore : figma 설정에 roundedCorner에 대한 dp 값이 없어져서 CIrcleShape로 변경함 * chore : SwitchPreview 업데이트. 간단하게 flatmap을 사용하는 방식으로 * chore : Switch Composable 파라미터 순서 변경: 필수 파라미터, modifier, 옵셔널 파라미터 순으로 * chore : SwitchTHumb에 대해서 Surface를 Box로 변경. 머티리얼 디자인을 따르지 않기 떄문 * chore : 불필요하게 상태와 ui값을 매핑해주는 함수 제거. sealed class를 전부 enum class로 변경 --- .idea/deploymentTargetSelector.xml | 4 +- .../com/yourssu/handy/demo/SwitchPreview.kt | 40 +++++ .../com/yourssu/handy/compose/Switch.kt | 151 ++++++++++++++++++ .../compose/foundation/SemanticColors.kt | 8 +- 4 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 app/src/main/kotlin/com/yourssu/handy/demo/SwitchPreview.kt create mode 100644 compose/src/main/kotlin/com/yourssu/handy/compose/Switch.kt diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index 5394540f..48ea7696 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -5,10 +5,10 @@ - + - + diff --git a/app/src/main/kotlin/com/yourssu/handy/demo/SwitchPreview.kt b/app/src/main/kotlin/com/yourssu/handy/demo/SwitchPreview.kt new file mode 100644 index 00000000..8fdb0209 --- /dev/null +++ b/app/src/main/kotlin/com/yourssu/handy/demo/SwitchPreview.kt @@ -0,0 +1,40 @@ +package com.yourssu.handy.demo + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.yourssu.handy.compose.Switch +import com.yourssu.handy.compose.SwitchSize +import com.yourssu.handy.compose.SwitchState +import com.yourssu.handy.compose.Text + + +data class SwitchPreviewParameter( + var switchState: SwitchState, + val switchSize: SwitchSize +) + +@Preview(showBackground = true) +@Composable +private fun PreviewMultipleSwitchState() { + val switchStates = + listOf(SwitchState.Unselected, SwitchState.Selected, SwitchState.Disabled) + val switchSizes = listOf(SwitchSize.Large, SwitchSize.Medium, SwitchSize.Small) + val samples = switchStates.flatMap { state -> + switchSizes.map { size -> SwitchPreviewParameter(state, size) } + } + + + Column { + samples.forEachIndexed { index, switchPreviewParameter -> + Text(text = "Size:${switchPreviewParameter.switchSize} State:${switchPreviewParameter.switchState}") + Switch( + switchState = switchPreviewParameter.switchState, + switchSize = switchPreviewParameter.switchSize, + onToggle = { + + } + ) + } + } +} diff --git a/compose/src/main/kotlin/com/yourssu/handy/compose/Switch.kt b/compose/src/main/kotlin/com/yourssu/handy/compose/Switch.kt new file mode 100644 index 00000000..002ead90 --- /dev/null +++ b/compose/src/main/kotlin/com/yourssu/handy/compose/Switch.kt @@ -0,0 +1,151 @@ +package com.yourssu.handy.compose + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.unit.Dp +import androidx.compose.ui.unit.dp + + +enum class SwitchSize( + val trackWidth: Dp, + val trackHeight: Dp, + val padding: Dp, + val thumbSize: Dp, +) { + Large(48.dp, 30.dp, 2.5.dp, 25.dp), + Medium(32.dp, 20.dp, 2.dp, 16.dp), + Small(24.dp, 16.dp, 1.5.dp, 13.dp), +} + +enum class SwitchState { + Unselected, + Selected, + Disabled, +} + +/** + * Switch를 그리는 함수입니다. + * + * 스위치는 특정 기능을 활성 또는 비활성의 상태로 만들 수 있도록 도와주는 요소입니다. + * 내부적으로 Track과 Thumb으로 구성되어 있습니다. + * + * @param switchState Switch의 상태. [SwitchState.Unselected], [SwitchState.Selected], [SwitchState.Disabled] 중 하나를 가질 수 있습니다. + * @param onToggle Switch의 상태를 변경하는 함수. [SwitchState]를 인자로 가집니다. + * @param switchSize Switch의 크기. [SwitchSize.Large], [SwitchSize.Medium], [SwitchSize.Small] 중 하나를 가질 수 있습니다. + * @param modifier Modifier + */ +@Composable +fun Switch( + onToggle: (SwitchState) -> Unit, + modifier: Modifier = Modifier, + switchState: SwitchState = SwitchState.Unselected, + switchSize: SwitchSize = SwitchSize.Large +) { + val trackWidth = switchSize.trackWidth + val trackHeight = switchSize.trackHeight + val trackPadding = switchSize.padding + val thumbSize = switchSize.thumbSize + + // Switch의 Track의 색상 변경을 에니메이션하기 위한 상태입니다. + val trackColor: Color by animateColorAsState(switchTrackColor(switchState)) + + val easeIn = SwitchAnimationEasing + val easeOut = SwitchAnimationEasing + + // Trainstion을 사용하면 Switch의 상태가 변경될 때 애니메이션을 적용할 수 있습니다. + val transition = updateTransition(targetState = switchState, label = "SwitchStateTransition") + + // Switch의 Thumb의 Offset을 에니메이션 하기위한 상태입니다. + val thumbOffset: Dp by transition.animateDp( + transitionSpec = { + // Switch의 상태가 변경될 때 애니메이션을 적용합니다. + // easeIn: Unselected -> Selected, easeOut: Selected -> Unselected + when { + SwitchState.Selected isTransitioningTo SwitchState.Unselected -> + tween(durationMillis = SwitchAnimationDuration, easing = easeOut) + + else -> + tween(durationMillis = SwitchAnimationDuration, easing = easeIn) + } + }, + label = "ThumbOffset" + ) { state -> + if (state == SwitchState.Selected) { + // Switch의 Thumb이 Track의 오른쪽 끝에 위치하도록 하는 Offset입니다. + trackWidth - thumbSize - trackPadding + } else { + // Switch의 Thumb이 Track의 왼쪽 끝에 위치하도록 하는 Offset입니다. + trackPadding + } + } + + Surface( + checked = switchState == SwitchState.Selected, + onCheckedChange = { onToggle(if (switchState == SwitchState.Selected) SwitchState.Unselected else SwitchState.Selected) }, + modifier = modifier + .padding(trackPadding) + .width(trackWidth) + .height(trackHeight), + enabled = switchState != SwitchState.Disabled, + shape = CircleShape, + backgroundColor = trackColor, + ) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.CenterStart) { + // Offset을 사용하여 Switch의 Thumb을 이동시킵니다. + SwitchThumb( + switchSize = switchSize, + modifier = Modifier + .offset(x = thumbOffset) + ) + } + } +} + +// Switch의 상태가 변경될 때 애니메이션을 적용하기 위한 상수입니다.(ms) +private const val SwitchAnimationDuration = 150 + +// Switch의 애니메이션 Easing입니다. +private val SwitchAnimationEasing = CubicBezierEasing(0.25f, 0.1f, 0.25f, 1f) + +/** + * Switch의 Thumb을 그리는 함수입니다. + * @param switchSize Switch의 크기 + */ +@Composable +private fun SwitchThumb(switchSize: SwitchSize, modifier: Modifier) { + Box( + modifier = modifier + .background(HandyTheme.colors.switchThumb, CircleShape) + .clip(CircleShape) + .size(switchSize.thumbSize), + ) +} + +/** + * Switch의 Track의 색상을 반환하는 함수입니다. + * @param switchState Switch의 상태 + */ +@Composable +private fun switchTrackColor(switchState: SwitchState): Color = when (switchState) { + SwitchState.Unselected -> HandyTheme.colors.switchUnselected + SwitchState.Selected -> HandyTheme.colors.switchSelected + SwitchState.Disabled -> HandyTheme.colors.switchDisabled +} diff --git a/compose/src/main/kotlin/com/yourssu/handy/compose/foundation/SemanticColors.kt b/compose/src/main/kotlin/com/yourssu/handy/compose/foundation/SemanticColors.kt index e04a0bed..7b2ce281 100644 --- a/compose/src/main/kotlin/com/yourssu/handy/compose/foundation/SemanticColors.kt +++ b/compose/src/main/kotlin/com/yourssu/handy/compose/foundation/SemanticColors.kt @@ -112,7 +112,13 @@ data class ColorScheme( // Pagination / Basic val paginationBasicSelected: Color = ColorNeutralBlack, - val paginationBasicUnselected: Color = ColorGray500 + val paginationBasicUnselected: Color = ColorGray500, + + // Switch + val switchUnselected: Color = ColorGray300, + val switchSelected: Color = ColorViolet500, + val switchDisabled: Color = ColorGray200, + val switchThumb: Color = ColorNeutralWhite, ) val lightColorScheme = ColorScheme()