From 298f689459296965f4cd3f84fe42213df9d9239e Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Fri, 20 Sep 2024 02:50:35 +0900 Subject: [PATCH 01/15] feat : make snackbar file --- .idea/inspectionProfiles/Project_Default.xml | 24 ++ .idea/other.xml | 318 ------------------ .../com/yourssu/handy/compose/SnackBar.kt | 11 + 3 files changed, 35 insertions(+), 318 deletions(-) delete mode 100644 .idea/other.xml create mode 100644 compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 44ca2d9..b67486e 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,30 @@ diff --git a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt index 4804e49..0d72954 100644 --- a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt +++ b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt @@ -58,21 +58,21 @@ fun InfoSnackBarItem( @Composable fun InfoSnackBar( text: String, - delay: Long = DURATION, - onDismiss: () -> Unit + onDismiss: () -> Unit, + duration: Long = SNACK_BAR_DURATION, ) { var visible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { visible = true - delay(delay) + delay(duration) visible = false delay(FADE_OUT_DURATION) onDismiss() } Popup( - alignment = Alignment.BottomCenter + alignment = Alignment.BottomCenter, ) { AnimatedVisibility( visible = visible, @@ -83,10 +83,8 @@ fun InfoSnackBar( ), exit = fadeOut( animationSpec = tween(durationMillis = 300) - ) + shrinkVertically( - shrinkTowards = Alignment.Bottom ) + slideOutVertically( - targetOffsetY = { fullHeight -> fullHeight } + targetOffsetY = { fullHeight -> fullHeight }, ) ) { InfoSnackBarItem( @@ -99,8 +97,8 @@ fun InfoSnackBar( @Composable fun ErrorSnackBarItem( text: String, + onClick: () -> Unit, modifier: Modifier = Modifier, - onClick: () -> Unit ) { Row( modifier = modifier @@ -161,10 +159,9 @@ fun ErrorSnackBar( ) + expandVertically( expandFrom = Alignment.Top ), - exit = fadeOut( + exit = shrinkVertically( + shrinkTowards = Alignment.Bottom, animationSpec = tween(durationMillis = 300) - ) + shrinkVertically( - shrinkTowards = Alignment.Bottom ) + slideOutVertically( targetOffsetY = { fullHeight -> fullHeight } ) @@ -177,5 +174,5 @@ fun ErrorSnackBar( } } -private const val DURATION = 5000L +private const val SNACK_BAR_DURATION = 5000L private const val FADE_OUT_DURATION = 300L \ No newline at end of file From dc8b1435ba59e28b1dcc0b7084fee973389ca9e5 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Sun, 22 Sep 2024 11:39:56 +0900 Subject: [PATCH 09/15] =?UTF-8?q?feat=20:=20=EC=A0=95=EB=B3=B4=EC=84=B1=20?= =?UTF-8?q?=EC=8A=A4=EB=82=B5=EB=B0=94=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/yourssu/handy/compose/SnackBar.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt index 0d72954..7003619 100644 --- a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt +++ b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt @@ -33,6 +33,12 @@ import com.yourssu.handy.compose.foundation.ColorStatusRedSub import com.yourssu.handy.compose.foundation.HandyTypography import kotlinx.coroutines.delay +/** + * 정보성 스낵바의 UI를 그린 함수입니다. + * + * @param text 스낵바의 문구를 나타내는 텍스트, 최대 두 줄까지 입력 가능 + * @param modifier Modifier + */ @Composable fun InfoSnackBarItem( text: String, @@ -55,6 +61,16 @@ fun InfoSnackBarItem( } } +/** + * 정보성 스낵바를 구현한 함수입니다. + * + * 유저의 행동에 대한 단순 결과를 나타낼 때 사용합니다. + * 특정 시간 노출 후에 사라집니다. + * + * @param text 스낵바의 문구를 나타내는 텍스트, 최대 두 줄까지 입력 가능 + * @param onDismiss 스낵바가 사라질 때 호출되는 함수 + * @param duration 스낵바가 지속되어 있는 시간 (기본값은 5초) + */ @Composable fun InfoSnackBar( text: String, From ccd35a84f78f4cae7e8de4d1690a2ba5be65ab8c Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Mon, 23 Sep 2024 12:35:08 +0900 Subject: [PATCH 10/15] =?UTF-8?q?feat=20:=20=EC=97=90=EB=9F=AC=20=EC=8A=A4?= =?UTF-8?q?=EB=82=B5=EB=B0=94=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/yourssu/handy/compose/SnackBar.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt index 7003619..40496f9 100644 --- a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt +++ b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt @@ -110,6 +110,13 @@ fun InfoSnackBar( } } +/** + * 에러 스낵바의 UI를 그린 함수입니다. + * + * @param text 스낵바의 문구를 나타내는 텍스트, 최대 두 줄까지 입력 가능 + * @param onClick 스낵바의 X 버튼을 눌렀을 때 호출되는 함수 + * @param modifier Modifier + */ @Composable fun ErrorSnackBarItem( text: String, @@ -147,6 +154,17 @@ fun ErrorSnackBarItem( } } +/** + * 에러 스낵바를 구현한 함수입니다. + * + * 사용자의 수행 과정에 부정적인 결과가 발생하거나 + * 정보성 스낵바보다 강조해야 할 메시지를 담아야 할 때 사용합니다. + * + * X 버튼을 눌러야만 사라집니다. + * + * @param text 스낵바의 문구를 나타내는 텍스트, 최대 두 줄까지 입력 가능 + * @param onDismiss 스낵바가 사라질 때 호출되는 함수 + */ @Composable fun ErrorSnackBar( text: String, From a7c3700f7d00a6283d3db152058950732b16d44f Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Mon, 23 Sep 2024 12:39:30 +0900 Subject: [PATCH 11/15] =?UTF-8?q?feat=20:=20=EC=97=90=EB=9F=AC=20=EC=8A=A4?= =?UTF-8?q?=EB=82=B5=EB=B0=94=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt index 40496f9..d0e09e1 100644 --- a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt +++ b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt @@ -135,7 +135,8 @@ fun ErrorSnackBarItem( ) { Icon( painter = painterResource(id = R.drawable.ic_alert_triangle), - tint = HandyTheme.colors.bgStatusNegative + tint = HandyTheme.colors.bgStatusNegative, + modifier = Modifier.align(Alignment.Top) ) Text( text = text, From e65c0638746a205129ec1a61fc3fea1a9c470b46 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Mon, 23 Sep 2024 14:06:43 +0900 Subject: [PATCH 12/15] =?UTF-8?q?feat=20:=20=EC=8A=A4=EB=82=B5=EB=B0=94=20?= =?UTF-8?q?=EC=A7=80=EC=86=8D=EC=8B=9C=EA=B0=84=20=EC=83=81=EC=88=98?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/yourssu/handy/compose/SnackBar.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt index d0e09e1..a10141e 100644 --- a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt +++ b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt @@ -83,7 +83,7 @@ fun InfoSnackBar( visible = true delay(duration) visible = false - delay(FADE_OUT_DURATION) + delay(FADE_OUT_DELAY) onDismiss() } @@ -93,12 +93,12 @@ fun InfoSnackBar( AnimatedVisibility( visible = visible, enter = fadeIn( - animationSpec = tween(durationMillis = 500) + animationSpec = tween(durationMillis = FADE_IN_DURATION) ) + expandVertically( expandFrom = Alignment.Top ), exit = fadeOut( - animationSpec = tween(durationMillis = 300) + animationSpec = tween(durationMillis = FADE_OUT_DURATION) ) + slideOutVertically( targetOffsetY = { fullHeight -> fullHeight }, ) @@ -179,7 +179,7 @@ fun ErrorSnackBar( LaunchedEffect(visible) { if (!visible) { - delay(FADE_OUT_DURATION) + delay(FADE_OUT_DELAY) onDismiss() } } @@ -190,13 +190,13 @@ fun ErrorSnackBar( AnimatedVisibility( visible = visible, enter = fadeIn( - animationSpec = tween(durationMillis = 500) + animationSpec = tween(durationMillis = FADE_IN_DURATION) ) + expandVertically( expandFrom = Alignment.Top ), exit = shrinkVertically( shrinkTowards = Alignment.Bottom, - animationSpec = tween(durationMillis = 300) + animationSpec = tween(durationMillis = FADE_OUT_DURATION) ) + slideOutVertically( targetOffsetY = { fullHeight -> fullHeight } ) @@ -210,4 +210,6 @@ fun ErrorSnackBar( } private const val SNACK_BAR_DURATION = 5000L -private const val FADE_OUT_DURATION = 300L \ No newline at end of file +private const val FADE_OUT_DELAY = 300L +private const val FADE_IN_DURATION = 500 +private const val FADE_OUT_DURATION = 300 From 8f96b18350b7b15e509efdc411bea69621e29891 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Fri, 27 Sep 2024 14:29:17 +0900 Subject: [PATCH 13/15] =?UTF-8?q?fix=20:=20=ED=95=B8=EB=94=94=20=EC=8A=A4?= =?UTF-8?q?=EB=82=B5=EB=B0=94=20=EC=BB=AC=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt | 4 ++-- .../yourssu/handy/compose/foundation/SemanticColors.kt | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt index a10141e..93f6023 100644 --- a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt +++ b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt @@ -49,7 +49,7 @@ fun InfoSnackBarItem( .padding(horizontal = 16.dp) .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) - .background(ColorGray800) + .background(HandyTheme.colors.snackBarInfo) .padding(16.dp) ) { Text( @@ -128,7 +128,7 @@ fun ErrorSnackBarItem( .padding(horizontal = 16.dp) .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) - .background(ColorStatusRedSub) + .background(HandyTheme.colors.snackBarError) .padding(16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically 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..e0d3bd9 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, + + + // SnackBar + val snackBarInfo: Color = ColorGray800, + val snackBarError: Color = ColorStatusRedSub + ) val lightColorScheme = ColorScheme() From 3955b5dbbbf65a24e1fed21fd6c60aad9be98e6d Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Thu, 3 Oct 2024 09:45:32 +0900 Subject: [PATCH 14/15] fix : modify to Handy Icon --- .../main/kotlin/com/yourssu/handy/compose/SnackBar.kt | 10 +++++----- compose/src/main/res/drawable/ic_alert_triangle.xml | 10 ---------- compose/src/main/res/drawable/ic_cancel.xml | 9 --------- 3 files changed, 5 insertions(+), 24 deletions(-) delete mode 100644 compose/src/main/res/drawable/ic_alert_triangle.xml delete mode 100644 compose/src/main/res/drawable/ic_cancel.xml diff --git a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt index 93f6023..e3da93b 100644 --- a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt +++ b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt @@ -25,12 +25,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup -import com.yourssu.handy.compose.foundation.ColorGray800 -import com.yourssu.handy.compose.foundation.ColorStatusRedSub import com.yourssu.handy.compose.foundation.HandyTypography +import com.yourssu.handy.compose.icons.HandyIcons +import com.yourssu.handy.compose.icons.filled.AlertTriangle +import com.yourssu.handy.compose.icons.line.Close import kotlinx.coroutines.delay /** @@ -134,7 +134,7 @@ fun ErrorSnackBarItem( verticalAlignment = Alignment.CenterVertically ) { Icon( - painter = painterResource(id = R.drawable.ic_alert_triangle), + imageVector = HandyIcons.Filled.AlertTriangle, tint = HandyTheme.colors.bgStatusNegative, modifier = Modifier.align(Alignment.Top) ) @@ -146,7 +146,7 @@ fun ErrorSnackBarItem( ) Spacer(modifier = Modifier.weight(1f)) Icon( - painter = painterResource(id = R.drawable.ic_cancel), + imageVector = HandyIcons.Line.Close, tint = HandyTheme.colors.textBasicTertiary, modifier = Modifier .clickable(onClick = onClick) diff --git a/compose/src/main/res/drawable/ic_alert_triangle.xml b/compose/src/main/res/drawable/ic_alert_triangle.xml deleted file mode 100644 index ca08709..0000000 --- a/compose/src/main/res/drawable/ic_alert_triangle.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/compose/src/main/res/drawable/ic_cancel.xml b/compose/src/main/res/drawable/ic_cancel.xml deleted file mode 100644 index 724ce1b..0000000 --- a/compose/src/main/res/drawable/ic_cancel.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - From 7ab14c56acb2da181eb3ace8ebea15ca3f44d0f8 Mon Sep 17 00:00:00 2001 From: LEE YOU BIN Date: Fri, 4 Oct 2024 03:35:48 +0900 Subject: [PATCH 15/15] refactor : refactor snackBar code --- .../com/yourssu/handy/demo/SnackBarPreview.kt | 12 +- .../com/yourssu/handy/compose/SnackBar.kt | 139 ++---------------- .../handy/compose/SnackBarAnimation.kt | 138 +++++++++++++++++ .../com/yourssu/handy/compose/SnackData.kt | 87 +++++++++++ 4 files changed, 247 insertions(+), 129 deletions(-) create mode 100644 compose/src/main/kotlin/com/yourssu/handy/compose/SnackBarAnimation.kt create mode 100644 compose/src/main/kotlin/com/yourssu/handy/compose/SnackData.kt diff --git a/app/src/main/kotlin/com/yourssu/handy/demo/SnackBarPreview.kt b/app/src/main/kotlin/com/yourssu/handy/demo/SnackBarPreview.kt index 383450f..c02b395 100644 --- a/app/src/main/kotlin/com/yourssu/handy/demo/SnackBarPreview.kt +++ b/app/src/main/kotlin/com/yourssu/handy/demo/SnackBarPreview.kt @@ -8,9 +8,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.yourssu.handy.compose.ErrorSnackBarItem +import com.yourssu.handy.compose.ErrorSnackBar import com.yourssu.handy.compose.HandyTheme -import com.yourssu.handy.compose.InfoSnackBarItem +import com.yourssu.handy.compose.InfoSnackBar @Preview(showBackground = true) @Composable @@ -19,11 +19,11 @@ fun InfoSnackBarPreview() { Column( modifier = Modifier.padding(10.dp) ) { - InfoSnackBarItem( + InfoSnackBar( text = "한 줄짜리 정보성 메세지가 들어갑니다." ) Spacer(modifier = Modifier.height(10.dp)) - InfoSnackBarItem( + InfoSnackBar( text = "줄 수가 두 줄 이상이 되는 스낵바 메시지입니다.\n좌측 정렬을 해주세요." ) } @@ -37,12 +37,12 @@ fun ErrorSnackBarPreview() { Column( modifier = Modifier.padding(10.dp) ) { - ErrorSnackBarItem( + ErrorSnackBar( text = "에러 메세지가 들어갑니다", onClick = {} ) Spacer(modifier = Modifier.height(10.dp)) - ErrorSnackBarItem( + ErrorSnackBar( text = "두 줄 이상의 에러 메세지가 들어갈 경우\n 아이콘은 모두 위로 정렬해주세요.", onClick = {} ) diff --git a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt index e3da93b..6f854f9 100644 --- a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt +++ b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBar.kt @@ -1,12 +1,5 @@ package com.yourssu.handy.compose -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -17,30 +10,26 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -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 import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Popup import com.yourssu.handy.compose.foundation.HandyTypography import com.yourssu.handy.compose.icons.HandyIcons import com.yourssu.handy.compose.icons.filled.AlertTriangle import com.yourssu.handy.compose.icons.line.Close -import kotlinx.coroutines.delay /** * 정보성 스낵바의 UI를 그린 함수입니다. * + * 유저의 행동에 대한 단순 결과를 나타낼 때 사용합니다. + * 특정 시간 노출 후에 사라집니다. + * * @param text 스낵바의 문구를 나타내는 텍스트, 최대 두 줄까지 입력 가능 * @param modifier Modifier */ @Composable -fun InfoSnackBarItem( +fun InfoSnackBar( text: String, modifier: Modifier = Modifier, ) { @@ -62,63 +51,19 @@ fun InfoSnackBarItem( } /** - * 정보성 스낵바를 구현한 함수입니다. + * 에러 스낵바의 UI를 그린 함수입니다. * - * 유저의 행동에 대한 단순 결과를 나타낼 때 사용합니다. - * 특정 시간 노출 후에 사라집니다. + * 사용자의 수행 과정에 부정적인 결과가 발생하거나 + * 정보성 스낵바보다 강조해야 할 메시지를 담아야 할 때 사용합니다. * - * @param text 스낵바의 문구를 나타내는 텍스트, 최대 두 줄까지 입력 가능 - * @param onDismiss 스낵바가 사라질 때 호출되는 함수 - * @param duration 스낵바가 지속되어 있는 시간 (기본값은 5초) - */ -@Composable -fun InfoSnackBar( - text: String, - onDismiss: () -> Unit, - duration: Long = SNACK_BAR_DURATION, -) { - var visible by remember { mutableStateOf(false) } - - LaunchedEffect(Unit) { - visible = true - delay(duration) - visible = false - delay(FADE_OUT_DELAY) - onDismiss() - } - - Popup( - alignment = Alignment.BottomCenter, - ) { - AnimatedVisibility( - visible = visible, - enter = fadeIn( - animationSpec = tween(durationMillis = FADE_IN_DURATION) - ) + expandVertically( - expandFrom = Alignment.Top - ), - exit = fadeOut( - animationSpec = tween(durationMillis = FADE_OUT_DURATION) - ) + slideOutVertically( - targetOffsetY = { fullHeight -> fullHeight }, - ) - ) { - InfoSnackBarItem( - text = text, - ) - } - } -} - -/** - * 에러 스낵바의 UI를 그린 함수입니다. + * X 버튼을 눌러야만 사라집니다. * * @param text 스낵바의 문구를 나타내는 텍스트, 최대 두 줄까지 입력 가능 * @param onClick 스낵바의 X 버튼을 눌렀을 때 호출되는 함수 * @param modifier Modifier */ @Composable -fun ErrorSnackBarItem( +fun ErrorSnackBar( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, @@ -146,7 +91,7 @@ fun ErrorSnackBarItem( ) Spacer(modifier = Modifier.weight(1f)) Icon( - imageVector = HandyIcons.Line.Close, + imageVector = HandyIcons.Line.Close, tint = HandyTheme.colors.textBasicTertiary, modifier = Modifier .clickable(onClick = onClick) @@ -155,61 +100,9 @@ fun ErrorSnackBarItem( } } -/** - * 에러 스낵바를 구현한 함수입니다. - * - * 사용자의 수행 과정에 부정적인 결과가 발생하거나 - * 정보성 스낵바보다 강조해야 할 메시지를 담아야 할 때 사용합니다. - * - * X 버튼을 눌러야만 사라집니다. - * - * @param text 스낵바의 문구를 나타내는 텍스트, 최대 두 줄까지 입력 가능 - * @param onDismiss 스낵바가 사라질 때 호출되는 함수 - */ -@Composable -fun ErrorSnackBar( - text: String, - onDismiss: () -> Unit -) { - var visible by remember { mutableStateOf(false) } - - LaunchedEffect(Unit) { - visible = true - } - - LaunchedEffect(visible) { - if (!visible) { - delay(FADE_OUT_DELAY) - onDismiss() - } - } - - Popup( - alignment = Alignment.BottomCenter - ) { - AnimatedVisibility( - visible = visible, - enter = fadeIn( - animationSpec = tween(durationMillis = FADE_IN_DURATION) - ) + expandVertically( - expandFrom = Alignment.Top - ), - exit = shrinkVertically( - shrinkTowards = Alignment.Bottom, - animationSpec = tween(durationMillis = FADE_OUT_DURATION) - ) + slideOutVertically( - targetOffsetY = { fullHeight -> fullHeight } - ) - ) { - ErrorSnackBarItem( - text = text, - onClick = { visible = false } - ) - } - } -} - -private const val SNACK_BAR_DURATION = 5000L -private const val FADE_OUT_DELAY = 300L -private const val FADE_IN_DURATION = 500 -private const val FADE_OUT_DURATION = 300 +object SnackBarDefaults { + const val SNACK_BAR_DURATION = 5000L + const val FADE_IN_DURATION = 500 + const val FADE_OUT_DURATION = 300 + const val TARGET_VALUE = -16f +} \ No newline at end of file diff --git a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBarAnimation.kt b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBarAnimation.kt new file mode 100644 index 0000000..8a0ab91 --- /dev/null +++ b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackBarAnimation.kt @@ -0,0 +1,138 @@ +package com.yourssu.handy.compose + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.EaseOut +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.RecomposeScope +import androidx.compose.runtime.State +import androidx.compose.runtime.currentRecomposeScope +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.unit.dp +import com.yourssu.handy.compose.SnackBarDefaults.FADE_IN_DURATION +import com.yourssu.handy.compose.SnackBarDefaults.FADE_OUT_DURATION +import com.yourssu.handy.compose.SnackBarDefaults.TARGET_VALUE + +data class SnackBarTransitionItem( + val snackBarData: SnackBarData?, + val opacityTransition: OpacityTransition +) + +typealias OpacityTransition = @Composable (snackBar: @Composable () -> Unit) -> Unit + +@Composable +fun FadeInFadeOut( + newSnackBarData: SnackBarData?, + modifier: Modifier = Modifier, + snackBar: @Composable (SnackBarData) -> Unit +) { + var scheduledSnackBarData by remember { mutableStateOf(null) } + val snackBarTransitions = remember { mutableListOf() } + var scope by remember { mutableStateOf(null) } + + if (newSnackBarData != scheduledSnackBarData) { + scheduledSnackBarData = newSnackBarData + + val snackBarDataList = snackBarTransitions.map { it.snackBarData }.toMutableList() + + snackBarDataList.add(newSnackBarData) + + snackBarTransitions.clear() + + snackBarDataList.filterNotNull() + .mapTo(destination = snackBarTransitions) { appearedSnackBarData -> + SnackBarTransitionItem(appearedSnackBarData) { snackBar -> + val isVisible = appearedSnackBarData == newSnackBarData + val animateInSpec: AnimationSpec = + tween(durationMillis = FADE_IN_DURATION) + val animateOutSpec: AnimationSpec = + tween(durationMillis = FADE_OUT_DURATION, easing = EaseOut) + + val opacity = animatedOpacity( + visible = isVisible, + animateInSpec = animateInSpec, + animateOutSpec = animateOutSpec + ) + + val offsetY = animatedOffset( + visible = isVisible, + animateInSpec = animateInSpec, + animateOutSpec = animateOutSpec + ) + + Box( + modifier = Modifier + .offset(y = offsetY.value.dp) + .alpha(opacity.value) + ) { + snackBar() + } + } + } + } + + Box( + modifier = modifier + ) { + scope = currentRecomposeScope + snackBarTransitions.forEach { (snackBarData, opacity) -> + key(snackBarData) { + opacity { + snackBar(snackBarData ?: return@opacity) + } + } + } + } +} + +@Composable +private fun animatedOpacity( + visible: Boolean, + animateInSpec: AnimationSpec, + animateOutSpec: AnimationSpec, +): State { + val alpha = remember { Animatable(0f) } + + LaunchedEffect(visible) { + alpha.animateTo( + if (visible) 1f else 0f, + animationSpec = if (visible) animateInSpec else animateOutSpec + ) + } + return alpha.asState() +} + +@Composable +private fun animatedOffset( + visible: Boolean, + animateInSpec: AnimationSpec, + animateOutSpec: AnimationSpec, +): State { + val offsetY = remember { Animatable(0f) } + + LaunchedEffect(visible) { + if (visible) { + offsetY.animateTo( + targetValue = TARGET_VALUE, + animationSpec = animateInSpec + ) + } else { + offsetY.animateTo( + targetValue = 0f, + animationSpec = animateOutSpec + ) + } + } + + return offsetY.asState() +} diff --git a/compose/src/main/kotlin/com/yourssu/handy/compose/SnackData.kt b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackData.kt new file mode 100644 index 0000000..38644f6 --- /dev/null +++ b/compose/src/main/kotlin/com/yourssu/handy/compose/SnackData.kt @@ -0,0 +1,87 @@ +package com.yourssu.handy.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import com.yourssu.handy.compose.SnackBarDefaults.SNACK_BAR_DURATION +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.delay +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlin.coroutines.resume + +interface SnackBarData { + val message: String + + fun dismiss() +} + +enum class SnackBarResult { + Dismissed, +} + +@Stable +class SnackBarHostState { + private val mutex = Mutex() + + internal var currentSnackBarData by mutableStateOf(null) + private set + + suspend fun showToast( + message: String, + ): SnackBarResult = mutex.withLock { + try { + return suspendCancellableCoroutine { continuation -> + currentSnackBarData = SnackBarDataImpl(message, continuation) + } + } finally { + currentSnackBarData = null + } + } +} + +@Composable +fun rememberSnackBarHostState(): SnackBarHostState = remember { SnackBarHostState() } + +@Stable +private class SnackBarDataImpl( + override val message: String, + private val continuation: CancellableContinuation +) : SnackBarData { + + override fun dismiss() { + if (continuation.isActive) { + continuation.resume(SnackBarResult.Dismissed) + } + } +} + +@Composable +fun SnackBarHost( + snackBarHostState: SnackBarHostState, + modifier: Modifier = Modifier, + snackBar: @Composable (SnackBarData) -> Unit = { + InfoSnackBar( + text = snackBarHostState.currentSnackBarData?.message ?: "" + ) + } +) { + val currentSnackBarData = snackBarHostState.currentSnackBarData + LaunchedEffect(currentSnackBarData) { + if (currentSnackBarData != null) { + delay(SNACK_BAR_DURATION) + currentSnackBarData.dismiss() + } + } + FadeInFadeOut( + newSnackBarData = snackBarHostState.currentSnackBarData, + modifier = modifier, + snackBar = snackBar + ) +}