diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index b268ef3..48ea769 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -5,6 +5,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/git_toolbox_blame.xml b/.idea/git_toolbox_blame.xml
new file mode 100644
index 0000000..7dc1249
--- /dev/null
+++ b/.idea/git_toolbox_blame.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
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 0000000..62a62b0
--- /dev/null
+++ b/app/src/main/kotlin/com/yourssu/handy/demo/SwitchPreview.kt
@@ -0,0 +1,55 @@
+package com.yourssu.handy.demo
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.remember
+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 samples = remember { mutableStateListOf() }
+
+ LaunchedEffect(Unit) {// SwitchState와 SwitchSize의 모든 조합 생성
+ val switchStates =
+ listOf(SwitchState.Unselected, SwitchState.Selected, SwitchState.Disabled)
+ val switchSizes = listOf(SwitchSize.Large, SwitchSize.Medium, SwitchSize.Small)
+
+ switchStates.forEach { state ->
+ switchSizes.forEach { size ->
+ samples.add(
+ SwitchPreviewParameter(
+ switchState = state,
+ switchSize = size
+ )
+ )
+ }
+ }
+ }
+
+
+ Column {
+ samples.forEachIndexed { index, switchPreviewParameter ->
+ Text(text = "Size:${switchPreviewParameter.switchSize} State:${switchPreviewParameter.switchState}")
+ Switch(
+ switchState = switchPreviewParameter.switchState,
+ switchSize = switchPreviewParameter.switchSize,
+ onToggle = {
+ samples[index] = samples[index].copy(switchState = it)
+ }
+ )
+ }
+ }
+}
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 0000000..774d3e0
--- /dev/null
+++ b/compose/src/main/kotlin/com/yourssu/handy/compose/Switch.kt
@@ -0,0 +1,197 @@
+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.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.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+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
+
+/**
+ * Switch의 크기를 나타내는 Sealed Class입니다.
+ * [SwitchSize.Large], [SwitchSize.Medium], [SwitchSize.Small] 중 하나를 가질 수 있습니다.
+ */
+sealed class SwitchSize {
+ data object Large : SwitchSize()
+ data object Medium : SwitchSize()
+ data object Small : SwitchSize()
+}
+
+/**
+ * Switch의 상태를 나타내는 Sealed Class입니다.
+ * [SwitchState.Unselected], [SwitchState.Selected], [SwitchState.Disabled] 중 하나를 가질 수 있습니다.
+ */
+sealed class SwitchState {
+ data object Unselected : SwitchState()
+ data object Selected : SwitchState()
+ data object Disabled : SwitchState()
+}
+
+/**
+ * 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(
+ switchState: SwitchState = SwitchState.Unselected,
+ onToggle: (SwitchState) -> Unit,
+ switchSize: SwitchSize = SwitchSize.Large,
+ modifier: Modifier = Modifier
+) {
+ val switchTrackWidth = remember(switchSize) { switchTrackWidth(switchSize) }
+ val switchTrackHeight = remember(switchSize) { switchTrackHeight(switchSize) }
+ val switchThumbSize = remember(switchSize) { switchThumbSize(switchSize) }
+ val switchPadding = remember(switchSize) { switchPadding(switchSize) }
+
+ // 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입니다.
+ switchTrackWidth - switchThumbSize - switchPadding
+ } else {
+ // Switch의 Thumb이 Track의 왼쪽 끝에 위치하도록 하는 Offset입니다.
+ switchPadding
+ }
+ }
+
+ Surface(
+ checked = switchState == SwitchState.Selected,
+ onCheckedChange = { onToggle(if (switchState == SwitchState.Selected) SwitchState.Unselected else SwitchState.Selected) },
+ modifier = modifier
+ .padding(switchPadding)
+ .width(switchTrackWidth)
+ .height(switchTrackHeight),
+ enabled = switchState != SwitchState.Disabled,
+ shape = RoundedCornerShape(size = SwitchTrackRoundedCorner.dp),
+ 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의 Track의 모서리를 둥글게 만들기 위한 상수입니다.
+private const val SwitchTrackRoundedCorner = 999
+
+// 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) {
+ Surface(
+ modifier = modifier
+ .clip(CircleShape)
+ .size(switchThumbSize(switchSize)),
+ contentColor = HandyTheme.colors.switchThumb,
+ ) {}
+}
+
+/**
+ * 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
+}
+
+/**
+ * Switch의 Track의 높이를 반환하는 함수입니다.
+ * @param switchSize Switch의 크기
+ */
+private fun switchTrackHeight(switchSize: SwitchSize): Dp = when (switchSize) {
+ SwitchSize.Large -> 30.dp
+ SwitchSize.Medium -> 20.dp
+ SwitchSize.Small -> 16.dp
+}
+
+/**
+ * Switch의 Track의 너비를 반환하는 함수입니다.
+ * @param switchSize Switch의 크기
+ */
+private fun switchTrackWidth(switchSize: SwitchSize): Dp = when (switchSize) {
+ SwitchSize.Large -> 48.dp
+ SwitchSize.Medium -> 32.dp
+ SwitchSize.Small -> 24.dp
+}
+
+/**
+ * Switch의 Padding을 반환하는 함수입니다.
+ * @param switchSize Switch의 크기
+ */
+fun switchPadding(switchSize: SwitchSize): Dp = when (switchSize) {
+ SwitchSize.Large -> 2.5.dp
+ SwitchSize.Medium -> 2.dp
+ SwitchSize.Small -> 1.5.dp
+}
+
+/**
+ * Switch의 Thumb의 크기를 반환하는 함수입니다.
+ * @param switchSize Switch의 크기
+ */
+private fun switchThumbSize(switchSize: SwitchSize): Dp = when (switchSize) {
+ SwitchSize.Large -> 25.dp
+ SwitchSize.Medium -> 16.dp
+ SwitchSize.Small -> 13.dp
+}
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 d668f4f..916df52 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()