-
Notifications
You must be signed in to change notification settings - Fork 1
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/#9] 월간 캘린더 화면 #18
Changes from 18 commits
00754bd
b26784d
6cd55a1
c71a9f8
45df9d9
a0c8541
35070bb
ed0a12e
6177a9e
cac95b8
51cb189
d9f82fd
b3df5e2
a7f65c8
74ac735
914f9cd
7dfa4df
110230c
c9680c0
a86695a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.terning.core.extension | ||
|
||
import java.time.LocalDate | ||
|
||
fun LocalDate.getStringAsTitle(): String = | ||
"${year}년 ${monthValue.toString().padStart(2, '0')}월" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package com.terning.feature.calendar | ||
|
||
import androidx.compose.foundation.background | ||
import androidx.compose.foundation.clickable | ||
import androidx.compose.foundation.interaction.MutableInteractionSource | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.Row | ||
import androidx.compose.foundation.layout.size | ||
import androidx.compose.foundation.shape.CircleShape | ||
import androidx.compose.material3.Text | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.tooling.preview.Preview | ||
import androidx.compose.ui.unit.dp | ||
import com.terning.core.designsystem.theme.Black | ||
import com.terning.core.designsystem.theme.Grey150 | ||
import com.terning.core.designsystem.theme.Grey200 | ||
import com.terning.core.designsystem.theme.TerningMain | ||
import com.terning.core.designsystem.theme.TerningTheme | ||
import com.terning.core.designsystem.theme.White | ||
import com.terning.feature.calendar.models.DayModel | ||
import java.time.LocalDate | ||
|
||
@Composable | ||
fun CalendarDay( | ||
modifier: Modifier = Modifier, | ||
dayData: DayModel, | ||
isSelected: Boolean, | ||
isToday: Boolean, | ||
onDateSelected: (LocalDate) -> Unit | ||
) { | ||
val backgroundColor = | ||
if (isSelected) TerningMain else if (isToday) Grey200 else Color.Transparent | ||
val textColor = | ||
if (dayData.isOutDate) { | ||
Grey150 | ||
} else { | ||
if (isSelected) | ||
White | ||
else | ||
Black | ||
} | ||
|
||
Box( | ||
modifier = modifier, | ||
contentAlignment = Alignment.Center | ||
) { | ||
Box( | ||
modifier = Modifier | ||
.size(22.dp) | ||
.clickable( | ||
interactionSource = remember { MutableInteractionSource() }, | ||
indication = null, | ||
onClick = { | ||
if (!dayData.isOutDate) { | ||
onDateSelected(dayData.date) | ||
} | ||
} | ||
) | ||
.background( | ||
color = backgroundColor, | ||
shape = CircleShape | ||
), | ||
contentAlignment = Alignment.Center | ||
) { | ||
Text( | ||
text = dayData.date.dayOfMonth.toString(), | ||
color = textColor, | ||
style = TerningTheme.typography.calendar | ||
) | ||
} | ||
} | ||
} | ||
|
||
@Preview(showBackground = true) | ||
@Composable | ||
fun CalendarDayPreview() { | ||
TerningTheme { | ||
Row { | ||
CalendarDay( | ||
dayData = DayModel(LocalDate.now(), false), | ||
isSelected = true, | ||
isToday = true, | ||
onDateSelected = {} | ||
) | ||
CalendarDay( | ||
dayData = DayModel(LocalDate.now(), false), | ||
isSelected = false, | ||
isToday = true, | ||
onDateSelected = {} | ||
) | ||
CalendarDay( | ||
dayData = DayModel(LocalDate.now(), false), | ||
isSelected = false, | ||
isToday = false, | ||
onDateSelected = {} | ||
) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package com.terning.feature.calendar | ||
|
||
import androidx.compose.foundation.background | ||
import androidx.compose.foundation.layout.Column | ||
import androidx.compose.foundation.layout.Row | ||
import androidx.compose.foundation.layout.Spacer | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.fillMaxWidth | ||
import androidx.compose.foundation.layout.height | ||
import androidx.compose.foundation.layout.padding | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.tooling.preview.Preview | ||
import androidx.compose.ui.unit.dp | ||
import com.terning.core.designsystem.theme.Grey150 | ||
import com.terning.core.designsystem.theme.TerningTheme | ||
import com.terning.feature.calendar.models.MonthData | ||
import com.terning.feature.calendar.models.Scrap | ||
import java.time.LocalDate | ||
import java.time.YearMonth | ||
|
||
@Composable | ||
fun CalendarMonth( | ||
modifier: Modifier = Modifier, | ||
monthData: MonthData, | ||
onDateSelected: (LocalDate) -> Unit, | ||
selectedDate: LocalDate, | ||
scrapLists: List<List<Scrap>> = listOf() | ||
) { | ||
Column( | ||
modifier = modifier | ||
.fillMaxSize() | ||
.padding(horizontal = 20.dp), | ||
) { | ||
val month = monthData.calendarMonth.weekDays | ||
for (week in month) { | ||
Row( | ||
modifier = Modifier.weight(1f), | ||
) { | ||
for (day in week) { | ||
Column( | ||
modifier = Modifier | ||
.weight(1f) | ||
.padding(top = 15.dp), | ||
horizontalAlignment = Alignment.CenterHorizontally | ||
) { | ||
CalendarDay( | ||
dayData = day, | ||
isSelected = selectedDate == day.date, | ||
isToday = day.date == LocalDate.now(), | ||
onDateSelected = onDateSelected | ||
) | ||
if(!day.isOutDate) { | ||
val index = day.date.dayOfWeek.value - 1 | ||
CalendarScrap( | ||
scrapList = scrapLists[index] | ||
) | ||
} | ||
} | ||
} | ||
} | ||
if(month.indexOf(week) != month.lastIndex) { | ||
Spacer( | ||
modifier = Modifier | ||
.fillMaxWidth() | ||
.height(1.dp) | ||
.background(color = Grey150) | ||
) | ||
} | ||
} | ||
} | ||
} | ||
|
||
@Preview(showBackground = true) | ||
@Composable | ||
fun CalendarMonthPreview() { | ||
TerningTheme { | ||
CalendarMonth( | ||
monthData = MonthData(YearMonth.now()), | ||
selectedDate = LocalDate.now(), | ||
onDateSelected = {}, | ||
scrapLists = listOf() | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package com.terning.feature.calendar | ||
|
||
import androidx.compose.foundation.background | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.lazy.LazyListState | ||
import androidx.compose.foundation.lazy.LazyRow | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Modifier | ||
import com.terning.core.designsystem.theme.White | ||
import com.terning.feature.calendar.models.CalendarDefaults.flingBehavior | ||
import com.terning.feature.calendar.models.CalendarState.Companion.getDateByPage | ||
import com.terning.feature.calendar.models.MonthData | ||
import com.terning.feature.calendar.models.Scrap | ||
import java.time.LocalDate | ||
import java.time.YearMonth | ||
|
||
@Composable | ||
fun CalendarMonths( | ||
modifier: Modifier = Modifier, | ||
listState: LazyListState, | ||
onDateSelected: (LocalDate) -> Unit, | ||
pages: Int, | ||
selectedDate: LocalDate, | ||
scrapLists: List<List<Scrap>> | ||
) { | ||
LazyRow( | ||
modifier = modifier | ||
.background(White), | ||
state = listState, | ||
userScrollEnabled = true, | ||
flingBehavior = flingBehavior( | ||
state = listState | ||
) | ||
) { | ||
items(pages) { page -> | ||
val date = getDateByPage(page) | ||
val monthData = MonthData(YearMonth.of(date.year, date.month)) | ||
|
||
CalendarMonth( | ||
modifier = Modifier.fillParentMaxSize(), | ||
selectedDate = selectedDate, | ||
onDateSelected = onDateSelected, | ||
monthData = monthData, | ||
scrapLists = scrapLists | ||
) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
package com.terning.feature.calendar | ||
|
||
import androidx.activity.compose.BackHandler | ||
import androidx.compose.foundation.background | ||
import androidx.compose.foundation.layout.Column | ||
import androidx.compose.foundation.layout.Spacer | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.fillMaxWidth | ||
import androidx.compose.foundation.layout.height | ||
import androidx.compose.foundation.layout.padding | ||
import androidx.compose.foundation.lazy.rememberLazyListState | ||
import androidx.compose.material3.Scaffold | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.LaunchedEffect | ||
import androidx.compose.runtime.collectAsState | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.mutableIntStateOf | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.runtime.rememberCoroutineScope | ||
import androidx.compose.runtime.setValue | ||
import androidx.compose.runtime.snapshotFlow | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.unit.dp | ||
import androidx.hilt.navigation.compose.hiltViewModel | ||
import com.terning.core.designsystem.theme.Grey200 | ||
import com.terning.feature.calendar.models.CalendarState | ||
import kotlinx.coroutines.flow.distinctUntilChanged | ||
import kotlinx.coroutines.launch | ||
|
||
@Composable | ||
fun CalendarRoute() { | ||
CalendarScreen() | ||
} | ||
|
||
@Composable | ||
fun CalendarScreen( | ||
modifier: Modifier = Modifier, | ||
viewModel: CalendarViewModel = hiltViewModel() | ||
) { | ||
val selectedDate by viewModel.selectedDate.collectAsState() | ||
val state by remember{ mutableStateOf(CalendarState()) } | ||
|
||
val listState = rememberLazyListState( | ||
initialFirstVisibleItemIndex = state.getInitialPage() | ||
) | ||
|
||
var currentDate by remember { mutableStateOf(selectedDate) } | ||
var currentPage by remember { mutableIntStateOf(listState.firstVisibleItemIndex)} | ||
|
||
var isListExpanded by remember { mutableStateOf(false) } | ||
var isWeekEnabled by remember { mutableStateOf(false) } | ||
|
||
Comment on lines
+50
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 줄바꿈도 너무 깔끔해요,,,,,,, |
||
LaunchedEffect(key1 = listState) { | ||
snapshotFlow { listState.firstVisibleItemIndex } | ||
.distinctUntilChanged() | ||
.collect{ | ||
val swipeDirection = (listState.firstVisibleItemIndex - currentPage).coerceIn(-1, 1).toLong() | ||
Comment on lines
+56
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
currentDate = currentDate.plusMonths(swipeDirection) | ||
currentPage = listState.firstVisibleItemIndex | ||
} | ||
} | ||
|
||
LaunchedEffect(key1 = selectedDate) { | ||
isWeekEnabled = true | ||
} | ||
|
||
BackHandler { | ||
isWeekEnabled = false | ||
} | ||
|
||
Scaffold( | ||
modifier = modifier, | ||
topBar = { | ||
val coroutineScope = rememberCoroutineScope() | ||
CalendarTopBar( | ||
date = currentDate, | ||
isListExpanded = isListExpanded, | ||
onListButtonClicked = { isListExpanded = !isListExpanded }, | ||
onMonthNavigationButtonClicked = { direction -> | ||
coroutineScope.launch { | ||
listState.animateScrollToItem( | ||
index = listState.firstVisibleItemIndex + direction, | ||
) | ||
} | ||
Comment on lines
+82
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. animateScrollToItem으로 매끄럽게 전환할 수 있근여 🥹..! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LazyListState에서 사용하는 전환 방법이라고 하네요 :D There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오호!! 이건 처음보는건데!! 좋네요😊 |
||
} | ||
) | ||
} | ||
) { paddingValues -> | ||
Column( | ||
modifier = Modifier | ||
.fillMaxWidth() | ||
.padding(top = paddingValues.calculateTopPadding()) | ||
) { | ||
WeekDaysHeader() | ||
Spacer(modifier = Modifier | ||
.fillMaxWidth() | ||
.height(1.dp) | ||
.background(color = Grey200) | ||
) | ||
CalendarMonths( | ||
modifier = Modifier.fillMaxSize(), | ||
Comment on lines
+103
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CalendarMonths안에서 Column에 fillMaxSize이 적용되고 있는 것 같은데 여기서도 fillMaxSize를 넘겨주는 이유가 뭔가용?! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 클래스 이름이랑 제 코딩 방식 때문에 혼동이 생긴 것 같네요,,, |
||
selectedDate = selectedDate, | ||
onDateSelected = { | ||
viewModel.updateSelectedDate(it) | ||
}, | ||
listState = listState, | ||
pages = state.getPageCount(), | ||
scrapLists = viewModel.mockScrapList | ||
) | ||
} | ||
|
||
} | ||
} |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CalendarMonths
하고CalendarMonth
의 네이밍이 좀 더 구분이 됐으면 좋겠는데 마땅한 네이밍이 떠오르지 않네욤,,,,흠,,There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
혹시라도 나중에 좋은 아이디어 있으면 추천 부탁드려여~!!