diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/DateViewModel.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/DateViewModel.kt index 18a79a2a..b08e2190 100644 --- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/DateViewModel.kt +++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/DateViewModel.kt @@ -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 @@ -27,6 +28,12 @@ class DateViewModel @Inject constructor( val calendar = savedStateHandle.get(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 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) diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/adpater/CalendarPagerAdapter.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/adpater/CalendarPagerAdapter.kt index 7e720462..45a8414d 100644 --- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/adpater/CalendarPagerAdapter.kt +++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/adpater/CalendarPagerAdapter.kt @@ -2,8 +2,9 @@ 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 @@ -11,13 +12,13 @@ class CalendarPagerAdapter( 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() diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/compose/CalendarView.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/compose/CalendarView.kt new file mode 100644 index 00000000..4c916869 --- /dev/null +++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/compose/CalendarView.kt @@ -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) -> 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?, + reloadList: List, + dateState: DateState, + currentCalendar: Calendar, + todayCalendar: Calendar, + reloadCalendar: (List) -> Unit, + resetFilm: (List) -> 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, + 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 +) \ No newline at end of file diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/compose/DateComposeFragment.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/compose/DateComposeFragment.kt new file mode 100644 index 00000000..33220737 --- /dev/null +++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/compose/DateComposeFragment.kt @@ -0,0 +1,111 @@ +package com.boostcamp.dailyfilm.presentation.calendar.compose + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.util.Log +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import com.boostcamp.dailyfilm.R +import com.boostcamp.dailyfilm.databinding.FragmentDateComposeBinding +import com.boostcamp.dailyfilm.presentation.BaseFragment +import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity.Companion.KEY_FILM_ARRAY +import com.boostcamp.dailyfilm.presentation.calendar.CalendarViewModel +import com.boostcamp.dailyfilm.presentation.calendar.DateViewModel +import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel +import com.boostcamp.dailyfilm.presentation.playfilm.PlayFilmActivity +import com.boostcamp.dailyfilm.presentation.playfilm.PlayFilmFragment.Companion.KEY_DATE_MODEL +import com.boostcamp.dailyfilm.presentation.ui.theme.DailyFilmTheme +import dagger.hilt.android.AndroidEntryPoint +import java.util.* + +@AndroidEntryPoint +class DateComposeFragment : + BaseFragment(R.layout.fragment_date_compose) { + + private val viewModel: DateViewModel by viewModels() + private val activityViewModel: CalendarViewModel by activityViewModels() + + private val startForResult: ActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK && result.data != null) { + val calendarIndex = result.data?.getIntExtra(KEY_CALENDAR_INDEX, -1) + ?: return@registerForActivityResult + val dateModel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + result.data?.getParcelableExtra(KEY_DATE_MODEL, DateModel::class.java) + } else { + result.data?.getParcelableExtra(KEY_DATE_MODEL) + } + dateModel ?: return@registerForActivityResult + viewModel.setVideo(calendarIndex, dateModel) + } + } + + @SuppressLint("ClickableViewAccessibility") + override fun initView() { + initSync() + initBinding() + } + + private fun initBinding() { + binding.composeView.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + DailyFilmTheme( + requireActivity() + ) { + CalendarView( + viewModel = viewModel, + resetFilm = { + activityViewModel.emitFilm(it) + }, + imgClick = { idx, dateModel -> + Log.d("CalendarView", "${ArrayList(activityViewModel.filmFlow.value)}") + startForResult.launch( + Intent(requireContext(), PlayFilmActivity::class.java).apply { + putExtra( + KEY_CALENDAR_INDEX, + idx + ) + putExtra( + KEY_DATE_MODEL_INDEX, + activityViewModel.filmFlow.value.indexOf(dateModel) + ) + putParcelableArrayListExtra( + KEY_FILM_ARRAY, + ArrayList(activityViewModel.filmFlow.value) + ) + } + ) + } + ) + } + } + } + } + + private fun initSync() { + if (viewModel.isSynced(viewModel.calendar.get(Calendar.YEAR)).not()) { + viewModel.syncFilmItem() + } + } + + companion object { + const val KEY_CALENDAR = "calendar" + const val KEY_DATE_MODEL_INDEX = "dateModelIndex" + const val KEY_CALENDAR_INDEX = "calendarIndex" + fun newInstance(calendar: Calendar): DateComposeFragment { + return DateComposeFragment().apply { + arguments = Bundle().apply { + putSerializable(KEY_CALENDAR, calendar) + } + } + } + } +} diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/compose/DateState.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/compose/DateState.kt new file mode 100644 index 00000000..335602c0 --- /dev/null +++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/compose/DateState.kt @@ -0,0 +1,20 @@ +package com.boostcamp.dailyfilm.presentation.calendar.compose + +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color + + +class DateState { + var isCurrentMonth: Boolean = true + var selectedDay by mutableStateOf(null) + + val alpha: Float get() = if (isCurrentMonth) 0.3f else 1f + + @Composable + fun isSelected(idx: Int) = + if (selectedDay == idx) Color.Gray else MaterialTheme.colors.background +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_date_compose.xml b/app/src/main/res/layout/fragment_date_compose.xml new file mode 100644 index 00000000..22681e19 --- /dev/null +++ b/app/src/main/res/layout/fragment_date_compose.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + \ No newline at end of file