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

Calendar 컴포즈 구현 #173

Merged
merged 7 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.boostcamp.dailyfilm.data.calendar.CalendarRepository
import com.boostcamp.dailyfilm.data.model.DailyFilmItem
import com.boostcamp.dailyfilm.data.sync.SyncRepository
import com.boostcamp.dailyfilm.presentation.calendar.DateFragment.Companion.KEY_CALENDAR
import com.boostcamp.dailyfilm.presentation.calendar.compose.DateState
import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel
import com.boostcamp.dailyfilm.presentation.util.*
import dagger.hilt.android.lifecycle.HiltViewModel
Expand All @@ -27,6 +28,12 @@ class DateViewModel @Inject constructor(
val calendar = savedStateHandle.get<Calendar>(KEY_CALENDAR)
?: throw IllegalStateException("CalendarViewModel - calendar is null")

val todayCalendar = createCalendar(Locale.getDefault()).apply {
set(Calendar.HOUR_OF_DAY, 24)
}
private val _dateState = MutableStateFlow(DateState())
val dateState : StateFlow<DateState> get() = _dateState

private val dateFormat = SimpleDateFormat("yyyyMMdd", Locale.getDefault())
private val dayOfWeek = createCalendar(calendar, day = 1).dayOfWeek()
private val prevCalendar = createCalendar(calendar, month = calendar.month() - 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@ package com.boostcamp.dailyfilm.presentation.calendar.adpater

import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.boostcamp.dailyfilm.presentation.calendar.DateFragment
import java.util.*
import com.boostcamp.dailyfilm.presentation.calendar.compose.DateComposeFragment
import java.util.Calendar
import java.util.Locale

class CalendarPagerAdapter(
fragmentActivity: FragmentActivity
) : FragmentStateAdapter(fragmentActivity) {

override fun getItemCount(): Int = Int.MAX_VALUE

override fun createFragment(position: Int): DateFragment {
override fun createFragment(position: Int): DateComposeFragment {
val calendar = Calendar.getInstance(Locale.getDefault())
calendar.add(Calendar.MONTH, getItemId(position).toInt())
if (getItemId(position).toInt() != 0) {
calendar.set(Calendar.DAY_OF_MONTH, 1)
}
return DateFragment.newInstance(calendar)
return DateComposeFragment.newInstance(calendar)
}

override fun getItemId(position: Int): Long = (position - START_POSITION).toLong()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
package com.boostcamp.dailyfilm.presentation.calendar.compose

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.boostcamp.dailyfilm.data.model.DailyFilmItem
import com.boostcamp.dailyfilm.presentation.calendar.DateViewModel
import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel
import com.boostcamp.dailyfilm.presentation.util.compose.noRippleClickable
import com.boostcamp.dailyfilm.presentation.util.compose.rememberLifecycleEvent
import com.boostcamp.dailyfilm.presentation.util.createCalendar
import com.boostcamp.dailyfilm.presentation.util.month
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import com.bumptech.glide.integration.compose.GlideImage
import java.util.Calendar

@Composable
fun CalendarView(
viewModel: DateViewModel,
resetFilm: (List<DateModel>) -> Unit,
imgClick: (Int, DateModel) -> Unit,
) {
val lifecycleEvent = rememberLifecycleEvent()
val itemList by viewModel.itemFlow.collectAsStateWithLifecycle(initialValue = null)
val reloadList by viewModel.dateFlow.collectAsStateWithLifecycle(minActiveState = Lifecycle.State.RESUMED)
val dateState by viewModel.dateState.collectAsStateWithLifecycle()

CalendarView(
lifecycleEvent = lifecycleEvent,
itemList = itemList,
reloadList = reloadList,
dateState = dateState,
currentCalendar = viewModel.calendar,
todayCalendar = viewModel.todayCalendar,
reloadCalendar = {
viewModel.reloadCalendar(it)
},
resetFilm = resetFilm,
imgClick = imgClick
)
}

@Composable
fun CalendarView(
lifecycleEvent: Lifecycle.Event,
itemList: List<DailyFilmItem?>?,
reloadList: List<DateModel>,
dateState: DateState,
currentCalendar: Calendar,
todayCalendar: Calendar,
reloadCalendar: (List<DailyFilmItem?>) -> Unit,
resetFilm: (List<DateModel>) -> Unit,
imgClick: (Int, DateModel) -> Unit,
) {

val textSize = 12.sp
val textHeight = with(LocalDensity.current) {
textSize.toPx() + 10
}.toInt()

LaunchedEffect(lifecycleEvent) {
when (lifecycleEvent) {
Lifecycle.Event.ON_PAUSE -> {
dateState.selectedDay = null
}

Lifecycle.Event.ON_RESUME -> {
// onResume 에서 가 아닌 repeatOnLifecycle 의 RESUMED 상태로 받아도 됐었음.
// LaunchedEffect(key1 = reloadList) 가 안됨
resetFilm(
reloadList.filter { dateModel -> dateModel.videoUrl != null }
)
}

else -> {}
}
}
LaunchedEffect(key1 = reloadList) {
resetFilm(
reloadList.filter { dateModel -> dateModel.videoUrl != null }
)
}
Comment on lines +100 to +104
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아마 이거 작동하지 않는 이유가 MutableStateFlowList가 변하는 거 잘 인지 못해서 그런 거 같아
SharedFlow로 바꾸거나 MutableStateList 써보면 인식 잘 될 듯


LaunchedEffect(key1 = itemList) {
reloadCalendar(itemList ?: return@LaunchedEffect)
}

CustomCalendarView(
textHeight = textHeight,
textSize = textSize,
reloadList = reloadList,
currentCalendar = currentCalendar,
todayCalendar = todayCalendar,
dateState = dateState,
imgClick = imgClick
)
}

@OptIn(ExperimentalGlideComposeApi::class)
@Composable
private fun DateImage(background: Color, alpha: Float, url: String?, onClick: () -> Unit) {

GlideImage(
modifier = Modifier
.fillMaxSize()
.background(background)
.alpha(alpha)
.padding(2.dp)
.clip(RoundedCornerShape(5.dp))
.noRippleClickable(onClick = onClick),
contentScale = ContentScale.Crop,
model = url,
contentDescription = ""
)
}

@Composable
private fun CustomCalendarView(
textHeight: Int,
textSize: TextUnit,
reloadList: List<DateModel>,
currentCalendar: Calendar,
todayCalendar: Calendar,
dateState: DateState,
imgClick: (Int, DateModel) -> Unit,
) {

CustomCalendarView(
textHeight = textHeight
) {
reloadList.forEachIndexed { index, dateModel ->

val isNotCurrentMonth = isNotCurrentMonth(
dateModel,
currentCalendar.month(),
todayCalendar
)
dateState.isCurrentMonth = isNotCurrentMonth

Text(
modifier = Modifier
.alpha(dateState.alpha)
.background(dateState.isSelected(index)),
text = dateModel.day,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.primary,
fontSize = textSize
)
DateImage(
background = dateState.isSelected(index),
alpha = dateState.alpha,
url = dateModel.videoUrl
) {
if (!isNotCurrentMonth) {
dateState.apply {
if (dateModel.videoUrl != null) {
selectedDay = null
imgClick(index, dateModel)
} else {
selectedDay = index
}
}
}
}
}
}
}


@Composable
private fun CustomCalendarView(textHeight: Int, content: @Composable () -> Unit) {

val lineColor = MaterialTheme.colors.primary

Layout(
modifier = Modifier
.fillMaxSize()
.drawWithCache {
onDrawWithContent {
drawContent()
repeat(5) { idx ->
val y = (idx + 1) * size.height / 6
drawLine(
color = lineColor,
start = Offset(0f, y),
end = Offset(size.width, y),
strokeWidth = 2f
)
}
}
},
content = content,
) { measureables, constraints ->

val dayWidth = constraints.maxWidth / 7
val dayHeight = constraints.maxHeight / 6

val placeables = measureables.mapIndexed { idx, measurable ->
measurable.measure(
when (idx % 2) {
0 -> constraints.fixConstraints(width = dayWidth, height = textHeight)
1 -> constraints.fixConstraints(
width = dayWidth,
height = dayHeight - textHeight
)

else -> constraints
}
)
}

layout(constraints.maxWidth, constraints.maxHeight) {

placeables.forEachIndexed { index, placeable ->
val idx = index / 2

val verticalIdx = idx / 7
val horizontalIdx = idx % 7

val left = horizontalIdx * dayWidth
val top = verticalIdx * dayHeight

when (index % 2) {
0 -> placeable.placeRelative(x = left, y = top)
1 -> placeable.placeRelative(x = left, y = top + textHeight)
}
}
}
}
}

private fun isNotCurrentMonth(
dateModel: DateModel,
currentMonth: Int,
todayCalendar: Calendar
): Boolean {
val itemCalendar = with(dateModel) {
createCalendar(year.toInt(), month.toInt() - 1, day.toInt())
}
return dateModel.month.toInt() != currentMonth ||
itemCalendar.timeInMillis > todayCalendar.timeInMillis
}

private fun Constraints.fixConstraints(width: Int = maxWidth, height: Int = maxHeight) = copy(
minWidth = width,
maxWidth = width,
minHeight = height,
maxHeight = height
)
Loading