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

[UI/YAF-68] 알람 설정 시 요일 반복 설정 #34

Merged
merged 17 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b184d86
[UI/#29] AlarmCheckItem 컴포넌트 구현
DongChyeon Jan 12, 2025
8833258
[UI/#29] AlarmDayButton 컴포넌트 구현
DongChyeon Jan 12, 2025
5750419
[REFACTOR/#29] OrbitPickerItem 상태를 부모 composable에서 관리하도록 변경
DongChyeon Jan 13, 2025
6417db4
[FEAT/#29] 알람 설정 화면 시간 변경 로직
DongChyeon Jan 13, 2025
06be2ec
[REFACTOR/#29] currentState를 통해 뷰모델의 상태값을 가져올 수 있도록 함
DongChyeon Jan 13, 2025
e7d2a97
[FEAT/#29] 알람 요일 반복 선택 로직 구현
DongChyeon Jan 13, 2025
9ea3531
[DEL/#29] MyPageNavigation padding 파라미터 삭제
DongChyeon Jan 13, 2025
420768e
[FEAT/#29] 알람 설정 화면 네비게이션 연결
DongChyeon Jan 13, 2025
340ab00
[FIX/#29] 요일 순서 수정
DongChyeon Jan 13, 2025
ca7710e
[FEAT/#29] OrbitPicker: HourPickerItem 한 바퀴 회전 시 AM/PM 자동 토글
DongChyeon Jan 13, 2025
f185480
[FEAT/#29] 알람 설정 초기 시각 오전 6시로 설정
DongChyeon Jan 13, 2025
8ee7aa8
[REFACTOR/#29] ktlint formatting
DongChyeon Jan 13, 2025
92ece50
[FIX/#29] 요일 선택 버튼 최초 클릭이 씹히는 버그 수정
DongChyeon Jan 13, 2025
d15028d
[FIX/#35] Layout Inspector 사용 불가 문제 해결
DongChyeon Jan 14, 2025
97d5a27
[REFACTOR/#35] 람다식을 통한 상태 주입 및 Recomposition 횟수 최적화
DongChyeon Jan 14, 2025
ae8c804
[REFACTOR/#35] toMutableSet() 사용 없이 요일 선택 업데이트
DongChyeon Jan 14, 2025
b06dfda
[REFACTOR/#35] remember를 활용하여 AlarmDayButton 색상 상태 관리
DongChyeon Jan 14, 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
14 changes: 14 additions & 0 deletions core/designsystem/src/main/res/drawable/ic_holiday.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="12"
android:viewportHeight="12">
<group>
<clip-path
android:pathData="M0,0h12v12h-12z"/>
<path
android:pathData="M7.852,2.606C7.653,2.464 7.442,2.381 7.237,2.351C6.861,2.296 6.457,2.413 6.107,2.77C5.914,2.967 5.598,2.971 5.4,2.778C5.203,2.585 5.199,2.268 5.392,2.071C5.954,1.497 6.674,1.257 7.382,1.361C7.778,1.419 8.153,1.581 8.485,1.831C8.453,1.547 8.312,1.306 8.097,1.183C7.858,1.045 7.776,0.739 7.914,0.5C8.052,0.26 8.358,0.179 8.597,0.317C9.384,0.771 9.602,1.651 9.435,2.426C10.589,2.73 11.5,3.778 11.5,5.088C11.5,5.364 11.276,5.588 11,5.588C10.724,5.588 10.5,5.364 10.5,5.088C10.5,4.465 10.172,3.931 9.696,3.622C9.849,4.334 9.679,5.109 9.152,5.678C8.964,5.88 8.648,5.892 8.445,5.704C8.243,5.517 8.231,5.2 8.418,4.998C8.823,4.561 8.857,3.862 8.519,3.351C8.248,3.506 7.978,3.797 7.738,4.24C7.417,4.833 7.19,5.641 7.126,6.56C7.969,6.666 8.67,6.911 9.238,7.282C10.023,7.795 10.512,8.522 10.769,9.352C10.851,9.616 10.703,9.896 10.44,9.978C10.176,10.059 9.896,9.912 9.814,9.648C9.617,9.012 9.257,8.489 8.691,8.119C8.118,7.744 7.292,7.5 6.125,7.5C4.958,7.5 4.09,7.744 3.466,8.127C2.847,8.506 2.441,9.037 2.216,9.668C2.123,9.928 1.837,10.064 1.577,9.971C1.317,9.878 1.181,9.592 1.274,9.332C1.572,8.497 2.121,7.778 2.944,7.274C3.363,7.017 3.845,6.82 4.392,6.689C4.561,4.709 5.977,3.085 7.852,2.606ZM4.33,10.751C3.697,10.751 3.17,10.328 3.159,10.319C2.877,10.083 2.479,10.084 2.198,10.281C2.144,10.313 1.943,10.429 1.674,10.538C1.382,10.656 1.048,10.751 0.75,10.751C0.474,10.751 0.25,10.975 0.25,11.251C0.25,11.527 0.474,11.751 0.75,11.751H11.25C11.526,11.751 11.75,11.527 11.75,11.251C11.75,10.975 11.526,10.751 11.25,10.751C10.952,10.751 10.618,10.656 10.326,10.538C10.057,10.429 9.856,10.313 9.802,10.281C9.521,10.084 9.123,10.083 8.841,10.319C8.83,10.328 8.303,10.751 7.67,10.751C7.038,10.751 6.511,10.328 6.5,10.319C6.212,10.078 5.788,10.078 5.5,10.319C5.489,10.328 4.962,10.751 4.33,10.751ZM9.807,10.285C9.807,10.285 9.807,10.285 9.807,10.285V10.285Z"
android:fillColor="#7B8696"
android:fillType="evenOdd"/>
</group>
</vector>
2 changes: 2 additions & 0 deletions core/ui/src/main/java/com/yapp/ui/base/BaseViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ abstract class BaseViewModel<UI_STATE : UiState, SIDE_EFFECT : SideEffect>(
) : ViewModel(), ContainerHost<UI_STATE, SIDE_EFFECT> {

override val container = container<UI_STATE, SIDE_EFFECT>(initialState)
val currentState: UI_STATE
get() = container.stateFlow.value

/**
* UI 상태 업데이트
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,31 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.yapp.designsystem.theme.OrbitTheme
import kotlinx.coroutines.launch
import java.util.Locale

@Composable
fun OrbitPicker(
modifier: Modifier = Modifier,
itemSpacing: Dp = 2.dp,
selectedAmPm: String = "오후",
selectedHour: Int = 0,
selectedMinute: Int = 0,
amPmStartIndex: Int = 0,
hourStartIndex: Int = 5,
minuteStartIndex: Int = 0,
onValueChange: (String, Int, Int) -> Unit,
) {
Surface(
Expand All @@ -45,9 +54,11 @@ fun OrbitPicker(
val hourItems = remember { (1..12).map { it.toString() } }
val minuteItems = remember { (0..59).map { String.format(Locale.ROOT, "%02d", it) } }

val amPmPickerState = rememberPickerState()
val hourPickerState = rememberPickerState()
val minutePickerState = rememberPickerState()
val amPmListState = rememberLazyListState()
val hourListState = rememberLazyListState()
val minuteListState = rememberLazyListState()

val scope = rememberCoroutineScope()

Box(modifier = Modifier.fillMaxWidth()) {
Box(
Expand All @@ -66,59 +77,57 @@ fun OrbitPicker(
verticalAlignment = Alignment.CenterVertically,
) {
OrbitPickerItem(
state = amPmPickerState,
items = amPmItems,
listState = amPmListState,
visibleItemsCount = 3,
itemSpacing = itemSpacing,
textStyle = OrbitTheme.typography.title2Medium,
modifier = Modifier.weight(1f),
textModifier = Modifier.padding(8.dp),
startIndex = amPmStartIndex,
infiniteScroll = false,
onValueChange = {
onPickerValueChange(
amPmPickerState,
hourPickerState,
minutePickerState,
onValueChange,
)
selectedItem = selectedAmPm,
onSelectedItemChange = { amPm ->
onValueChange(amPm, selectedHour, selectedMinute)
},
)

OrbitPickerItem(
state = hourPickerState,
items = hourItems,
listState = hourListState,
visibleItemsCount = 5,
itemSpacing = itemSpacing,
textStyle = OrbitTheme.typography.title2Medium,
modifier = Modifier.weight(1f),
textModifier = Modifier.padding(8.dp),
startIndex = hourStartIndex,
infiniteScroll = true,
onValueChange = {
onPickerValueChange(
amPmPickerState,
hourPickerState,
minutePickerState,
onValueChange,
)
selectedItem = selectedHour.toString(),
onSelectedItemChange = { hour ->
onValueChange(selectedAmPm, hour.toInt(), selectedMinute)
},
onScrollCompleted = {
scope.launch {
val currentIndex = amPmListState.firstVisibleItemIndex % amPmItems.size
val nextIndex = (currentIndex + 1) % amPmItems.size
amPmListState.animateScrollToItem(nextIndex)
}
},
)

OrbitPickerItem(
state = minutePickerState,
items = minuteItems,
listState = minuteListState,
visibleItemsCount = 5,
itemSpacing = itemSpacing,
textStyle = OrbitTheme.typography.title2Medium,
modifier = Modifier.weight(1f),
textModifier = Modifier.padding(8.dp),
startIndex = minuteStartIndex,
infiniteScroll = true,
onValueChange = {
onPickerValueChange(
amPmPickerState,
hourPickerState,
minutePickerState,
onValueChange,
)
selectedItem = selectedMinute.toString(),
onSelectedItemChange = { minute ->
onValueChange(selectedAmPm, selectedHour, minute.toInt())
},
)
}
Expand All @@ -127,18 +136,6 @@ fun OrbitPicker(
}
}

private fun onPickerValueChange(
amPmState: PickerState,
hourState: PickerState,
minuteState: PickerState,
onValueChange: (String, Int, Int) -> Unit,
) {
val amPm = amPmState.selectedItem
val hour = hourState.selectedItem.toIntOrNull() ?: 0
val minute = minuteState.selectedItem.toIntOrNull() ?: 0
onValueChange(amPm, hour, minute)
}

@Preview(showBackground = true)
@Composable
fun BottomSheetPickerPreview() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -27,21 +31,22 @@ import androidx.compose.ui.unit.dp
import com.yapp.designsystem.theme.OrbitTheme
import com.yapp.ui.utils.toPx
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlin.math.abs

@Composable
fun OrbitPickerItem(
modifier: Modifier = Modifier,
items: List<String>,
state: PickerState = rememberPickerState(),
selectedItem: String,
onSelectedItemChange: (String) -> Unit,
onScrollCompleted: (() -> Unit)? = null,
listState: LazyListState = rememberLazyListState(),
startIndex: Int = 0,
visibleItemsCount: Int,
textModifier: Modifier = Modifier,
infiniteScroll: Boolean = true,
textStyle: TextStyle,
itemSpacing: Dp,
onValueChange: (String) -> Unit,
) {
val visibleItemsMiddle = visibleItemsCount / 2
val listScrollCount = if (infiniteScroll) Int.MAX_VALUE else items.size + visibleItemsMiddle * 2
Expand All @@ -53,31 +58,36 @@ fun OrbitPickerItem(
visibleItemsMiddle,
startIndex,
)
val listState = rememberLazyListState(initialFirstVisibleItemIndex = listStartIndex)
val flingBehavior = rememberSnapFlingBehavior(lazyListState = listState)
val itemHeightPixels = remember { mutableIntStateOf(0) }
val itemHeightDp = with(LocalDensity.current) { itemHeightPixels.intValue.toDp() }

LaunchedEffect(listState) {
snapshotFlow { listState.layoutInfo }
.map { layoutInfo ->
val centerOffset = layoutInfo.viewportStartOffset + (layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset) / 2

layoutInfo.visibleItemsInfo.minByOrNull { item ->
val itemCenter = item.offset + (item.size / 2)
abs(itemCenter - centerOffset)
}?.index
}
if (listState.firstVisibleItemIndex != listStartIndex) {
listState.scrollToItem(listStartIndex) // 초기 스크롤 위치 설정
}
}

LaunchedEffect(listState) {
var previousAdjustedIndex = calculateCenterIndex(listState) // 초기값 설정

snapshotFlow { calculateCenterIndex(listState) }
.distinctUntilChanged()
.collect { centerIndex ->
if (centerIndex != null) {
val adjustedIndex = centerIndex % items.size
val newValue = items[adjustedIndex]
if (newValue != state.selectedItem) {
state.selectedItem = newValue
onValueChange(newValue)
}
val adjustedIndex = centerIndex % items.size

// 끝에서 끝으로 이동 감지
if (previousAdjustedIndex == items.size - 1 && adjustedIndex == 0) {
onScrollCompleted?.invoke()
} else if (previousAdjustedIndex == 0 && adjustedIndex == items.size - 1) {
onScrollCompleted?.invoke()
}

if (adjustedIndex != previousAdjustedIndex) {
onSelectedItemChange(items[adjustedIndex])
}

previousAdjustedIndex = adjustedIndex
}
}

Expand Down Expand Up @@ -105,7 +115,7 @@ fun OrbitPickerItem(
val maxDistance = totalItemHeight.toPx() * visibleItemsMiddle
val alpha = ((maxDistance - distanceFromCenter) / maxDistance).coerceIn(0.2f, 1f)
val scaleY = 1f - (0.4f * (distanceFromCenter / maxDistance)).coerceIn(0f, 0.4f)
val isSelected = getItemForIndex(index, items, infiniteScroll, visibleItemsMiddle) == state.selectedItem
val isSelected = getItemForIndex(index, items, infiniteScroll, visibleItemsMiddle) == selectedItem

Text(
text = getItemForIndex(index, items, infiniteScroll, visibleItemsMiddle),
Expand Down Expand Up @@ -140,6 +150,19 @@ private fun calculateStartIndex(
}
}

/**
* 중심 인덱스를 계산합니다.
*/
private fun calculateCenterIndex(listState: LazyListState): Int {
val layoutInfo = listState.layoutInfo
val centerOffset = layoutInfo.viewportStartOffset + (layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset) / 2

return layoutInfo.visibleItemsInfo.minByOrNull { item ->
val itemCenter = item.offset + (item.size / 2)
abs(itemCenter - centerOffset)
}?.index ?: 0
}

/**
* 주어진 인덱스에 해당하는 항목을 반환합니다.
* 무한 스크롤과 보이는 항목의 개수를 고려합니다.
Expand All @@ -156,13 +179,15 @@ private fun getItemForIndex(index: Int, items: List<String>, infiniteScroll: Boo
@Preview
fun OrbitPickerPreview() {
OrbitTheme {
var selectedItem by remember { mutableStateOf("0") }

OrbitPickerItem(
items = (0..100).map { it.toString() },
state = rememberPickerState(),
selectedItem = selectedItem,
onSelectedItemChange = { selectedItem = it },
visibleItemsCount = 5,
textStyle = TextStyle.Default,
itemSpacing = 8.dp,
onValueChange = {},
)
}
}
Loading
Loading