Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BottomSheet 구현 #30

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0ea6102
feat : 바텀시트 폴더 추가
leeeyubin Nov 21, 2024
87fda6a
feat : git pull from main
leeeyubin Dec 18, 2024
d943277
feat : bottom sheet UI
leeeyubin Jan 13, 2025
67dc425
feat : handle bar optional인 상태 삭제
leeeyubin Jan 17, 2025
9460ec3
feat : bottomSheetType Sealed class로 구현
leeeyubin Jan 17, 2025
167ed2a
feat : TwoButton 타입 UI 구현
leeeyubin Jan 19, 2025
97b0a9b
feat : preview 수정
leeeyubin Jan 19, 2025
e328e10
feat : BottomSheetDefaults 생성
leeeyubin Jan 19, 2025
ac17d98
feat : 불필요한 코드 삭제
leeeyubin Jan 19, 2025
cf6e412
feat : 바텀시트 data 추가
leeeyubin Jan 19, 2025
0d6f84e
feat : 드래그로 사라지는 애니메이션 구현
leeeyubin Jan 20, 2025
40a9c28
feat : sheet.show 상태 추가
leeeyubin Jan 20, 2025
8e679e0
feat : git pull from main
leeeyubin Jan 20, 2025
b625eb0
feat : conflict 해결
leeeyubin Jan 20, 2025
3212d94
feat : currentValue 업데이트 문제 해결
leeeyubin Jan 20, 2025
364668f
feat : show, hide 애니메이션 구현
leeeyubin Jan 21, 2025
f802d48
feat : 불필요한 코드 수정
leeeyubin Jan 21, 2025
4c2cef6
feat : currentValue 값 업데이트 되도록 수정
leeeyubin Jan 21, 2025
d1f3470
feat : targetValue 추가 및 배경 동기화 수정
leeeyubin Jan 21, 2025
f082ef8
feat : 드래그 후 onDismissRequest 요청할 수 있도록 수정
leeeyubin Jan 21, 2025
b2f71ed
feat : 불필요한 코드 삭제
leeeyubin Jan 21, 2025
6545c06
feat : surface에 shape 적용
leeeyubin Jan 21, 2025
19748ac
feat : 주석 작성 및 Color 추가
leeeyubin Jan 21, 2025
f408080
feat : 주석 수정 및 Color 토큰 추가
leeeyubin Jan 21, 2025
09cb87b
feat : 패딩값 상수화
leeeyubin Jan 21, 2025
d71a8c6
feat : 최소 사이즈 구현
leeeyubin Jan 21, 2025
b0f753f
feat : 프리뷰 바텀시트 보이도록 수정
leeeyubin Jan 21, 2025
3516c97
feat : 프리뷰 경우의 수 추가
leeeyubin Jan 21, 2025
6d55a6e
feat : NoButton 타입 삭제
leeeyubin Jan 21, 2025
781e039
feat : 주석 수정
leeeyubin Jan 21, 2025
3a2542b
feat : 프리뷰 업데이트
leeeyubin Jan 21, 2025
ca7ac13
feat : content 기본값 삭제
leeeyubin Jan 29, 2025
4b1ec41
feat : 줄바꿈 삭제
leeeyubin Feb 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

88 changes: 88 additions & 0 deletions app/src/main/kotlin/com/yourssu/handy/demo/BottomSheetPreview.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.yourssu.handy.demo

import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.yourssu.handy.compose.BottomSheet
import com.yourssu.handy.compose.BottomSheetType
import com.yourssu.handy.compose.HandyTheme
import com.yourssu.handy.compose.SheetValue
import com.yourssu.handy.compose.Text
import com.yourssu.handy.compose.rememberModalBottomSheetState

@Preview(showBackground = true)
@Composable
private fun OneShortBottomSheetPreview() {
HandyTheme {
val sheetState = rememberModalBottomSheetState(
initialValue = SheetValue.Expanded
)
BottomSheet(
onDismissRequest = {},
sheetState = sheetState,
bottomSheetType = BottomSheetType.OneButton(
buttonText = "TEXT"
)
) {
Text("one button")
}
}
}

@Preview(showBackground = true)
@Composable
private fun OneLongBottomSheetPreview() {
HandyTheme {
val sheetState = rememberModalBottomSheetState(
initialValue = SheetValue.Expanded
)
BottomSheet(
onDismissRequest = {},
sheetState = sheetState,
bottomSheetType = BottomSheetType.OneButton(
buttonText = "TEXT"
)
) {
Text("one button\none button\none button\none button\none button\none button\none button\none button")
}
}
}

@Preview(showBackground = true)
@Composable
private fun TwoShortBottomSheetPreview() {
HandyTheme {
val sheetState = rememberModalBottomSheetState(
initialValue = SheetValue.Expanded
)
BottomSheet(
onDismissRequest = {},
sheetState = sheetState,
bottomSheetType = BottomSheetType.TwoButton(
firstButtonText = "LEFT",
secondButtonText = "RIGHT"
)
) {
Text("two button")
}
}
}

@Preview(showBackground = true)
@Composable
private fun TwoLongBottomSheetPreview() {
HandyTheme {
val sheetState = rememberModalBottomSheetState(
initialValue = SheetValue.Expanded
)
BottomSheet(
onDismissRequest = {},
sheetState = sheetState,
bottomSheetType = BottomSheetType.TwoButton(
firstButtonText = "LEFT",
secondButtonText = "RIGHT"
)
) {
Text("two button\ntwo button\ntwo button\ntwo button\ntwo button\ntwo button\ntwo button\ntwo button")
}
}
}
263 changes: 263 additions & 0 deletions compose/src/main/kotlin/com/yourssu/handy/compose/BottomSheet.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
package com.yourssu.handy.compose

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
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.SideEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import com.yourssu.handy.compose.BottomSheetDefaults.DragHandle
import com.yourssu.handy.compose.BottomSheetDefaults.contentMinHeight
import com.yourssu.handy.compose.BottomSheetDefaults.contentPadding
import com.yourssu.handy.compose.BottomSheetDefaults.surfaceBottomPadding
import com.yourssu.handy.compose.BottomSheetDefaults.surfaceHorizontalPadding
import com.yourssu.handy.compose.BottomSheetType.OneButton
import com.yourssu.handy.compose.BottomSheetType.TwoButton
import com.yourssu.handy.compose.SheetValue.Hidden
import com.yourssu.handy.compose.button.BaseButton
import com.yourssu.handy.compose.button.ButtonColorState
import com.yourssu.handy.compose.foundation.ColorGray080
import com.yourssu.handy.compose.foundation.HandyTypography
import com.yourssu.handy.compose.foundation.Radius
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch

/**
* BottomSheet의 타입을 나타냅니다.
*
* [OneButton] : 하단에 버튼 한 개 표시
* [TwoButton] : 하단에 버튼 두 개 표시
*/
sealed class BottomSheetType {
data class OneButton(
val buttonText: String
) : BottomSheetType()

data class TwoButton(
val firstButtonText: String,
val secondButtonText: String
) : BottomSheetType()
}

/**
* 바텀시트의 UI를 나타내는 함수입니다.
*
* @param onDismissRequest 바텀시트가 닫힐 때 호출되는 함수
* @param bottomSheetType 바텀시트의 [BottomSheetType] 타입을 결정
* @param modifier Modifier 수정자
* @param sheetState 바텀시트의 상태를 나타내는 객체
* @param onOneButtonClick [BottomSheetType.OneButton]의 버튼 클릭 시 호출되는 함수
* @param onFirstButtonClick [BottomSheetType.TwoButton]의 첫 번째 버튼 클릭 시 호출되는 함수
* @param onSecondButtonClick [BottomSheetType.TwoButton]의 두 번째 버튼 클릭 시 호출되는 함수
* @param content 바텀시트 내부에 표시될 내용을 결정
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun BottomSheet(
onDismissRequest: () -> Unit,
bottomSheetType: BottomSheetType,
modifier: Modifier = Modifier,
sheetState: SheetState = rememberModalBottomSheetState(),
onOneButtonClick: () -> Unit = {},
onFirstButtonClick: () -> Unit = {},
onSecondButtonClick: () -> Unit = {},
content: @Composable () -> Unit
) {
val density = LocalDensity.current
SideEffect {
sheetState.density = density
}
val scope = rememberCoroutineScope()
val animateToDismiss: () -> Unit = {
scope.launch { sheetState.hide() }.invokeOnCompletion {
if (!sheetState.isVisible) onDismissRequest()
}
}

LaunchedEffect(sheetState) {
snapshotFlow { sheetState.currentValue }
.drop(1)
.distinctUntilChanged()
.filter { it == Hidden }
.collect {
scope.launch { sheetState.hide() }.invokeOnCompletion { onDismissRequest() }
}
}

Popup {
BoxWithConstraints(Modifier.fillMaxSize()) {
val fullHeight = constraints.maxHeight
Scrim(
color = ColorGray080,
onDismissRequest = animateToDismiss,
visible = sheetState.targetValue != Hidden
)
Surface(
modifier = modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.padding(horizontal = surfaceHorizontalPadding)
.padding(bottom = surfaceBottomPadding)
.offset {
IntOffset(
x = 0,
y = sheetState
.requireOffset()
.toInt()
)
}
.anchoredDraggable(
state = sheetState.anchoredDraggableState,
orientation = Orientation.Vertical,
)
.modalBottomSheetAnchors(
sheetState = sheetState,
fullHeight = fullHeight.toFloat()
),
shape = RoundedCornerShape(24.dp)
) {
Column(
modifier = Modifier
.background(HandyTheme.colors.bgBasicDefault)
.align(Alignment.BottomCenter)
.padding(horizontal = contentPadding)
.padding(bottom = contentPadding),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
modifier = Modifier
.defaultMinSize(minHeight = contentMinHeight),
horizontalAlignment = Alignment.CenterHorizontally
) {
DragHandle()
Spacer(modifier = Modifier.height(contentPadding))
content()
Spacer(modifier = Modifier.height(contentPadding))
}
Column {
when (bottomSheetType) {
is OneButton -> {
OneButtonBottomSheet(
buttonText = bottomSheetType.buttonText,
onClick = onOneButtonClick
)
}

is TwoButton -> {
TwoButtonBottomSheet(
firstButtonText = bottomSheetType.firstButtonText,
secondButtonText = bottomSheetType.secondButtonText,
onFirstButtonClick = onFirstButtonClick,
onSecondButtonClick = onSecondButtonClick
)
}
}
}
}
}
}
}
if (sheetState.hasExpandedState) {
LaunchedEffect(sheetState) {
sheetState.show()
}
}
}

/**
* 하단에 버튼이 하나만 표시될 때 사용하는 함수입니다.
*
* @param buttonText 버튼에 표시될 텍스트
* @param onClick 버튼 클릭 시 호출되는 함수
* @param modifier Modifier 수정자
*/
@Composable
private fun OneButtonBottomSheet(
buttonText: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
BaseButton(
onClick = onClick,
colors = ButtonColorState(bgColor = HandyTheme.colors.buttonFilledPrimaryEnabled),
modifier = modifier.fillMaxWidth(),
rounding = Radius.XL.dp
) {
Text(
text = buttonText,
style = HandyTypography.B1Sb16,
color = HandyTheme.colors.textBasicWhite
)
}
}

/**
* 하단에 버튼이 두 개일 때 표시될 때 사용하는 함수입니다.
*
* @param firstButtonText 첫 번째 버튼에 표시될 텍스트
* @param secondButtonText 두 번째 버튼에 표시될 텍스트
* @param onFirstButtonClick 첫 번째 버튼 클릭 시 호출되는 함수
* @param onSecondButtonClick 두 번째 버튼 클릭 시 호출되는 함수
* @param modifier Modifier 수정자
*/
@Composable
private fun TwoButtonBottomSheet(
firstButtonText: String,
secondButtonText: String,
onFirstButtonClick: () -> Unit,
onSecondButtonClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
BaseButton(
onClick = onFirstButtonClick,
colors = ButtonColorState(bgColor = HandyTheme.colors.buttonFilledSecondaryEnabled),
modifier = Modifier.weight(1f),
rounding = Radius.XL.dp
) {
Text(
text = firstButtonText,
style = HandyTypography.B1Sb16,
color = HandyTheme.colors.textBrandSecondary
)
}
BaseButton(
onClick = onSecondButtonClick,
colors = ButtonColorState(bgColor = HandyTheme.colors.buttonFilledPrimaryEnabled),
modifier = Modifier.weight(1f),
rounding = Radius.XL.dp
) {
Text(
text = secondButtonText,
style = HandyTypography.B1Sb16,
color = HandyTheme.colors.textBasicWhite
)
}
}
}
Loading