Skip to content

Commit

Permalink
issue boostcampwm-2022#171 feat: calendar compose migration
Browse files Browse the repository at this point in the history
Co-authored-by: junhyeongleeee <[email protected]>
  • Loading branch information
junhyeongleeee committed Jul 25, 2023
1 parent 6b5b20f commit c270295
Show file tree
Hide file tree
Showing 6 changed files with 437 additions and 4 deletions.
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 }
)
}

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

0 comments on commit c270295

Please sign in to comment.