diff --git a/app/src/main/java/com/boostcamp/dailyfilm/data/selectvideo/GalleryVideoRepository.kt b/app/src/main/java/com/boostcamp/dailyfilm/data/selectvideo/GalleryVideoRepository.kt index 1d6f1726..37d82e49 100644 --- a/app/src/main/java/com/boostcamp/dailyfilm/data/selectvideo/GalleryVideoRepository.kt +++ b/app/src/main/java/com/boostcamp/dailyfilm/data/selectvideo/GalleryVideoRepository.kt @@ -13,9 +13,9 @@ interface GalleryVideoRepository { class GalleryVideoRepositoryImpl @Inject constructor( private val contentResolver: ContentResolver ) : GalleryVideoRepository { - override fun loadVideo(): Flow> { - return Pager(config = PagingConfig(pageSize = GalleryPagingSource.PAGING_SIZE)) { - GalleryPagingSource(contentResolver) - }.flow - } + + override fun loadVideo(): Flow> = Pager(config = PagingConfig(pageSize = GalleryPagingSource.PAGING_SIZE)) { + GalleryPagingSource(contentResolver) + }.flow + } \ No newline at end of file diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/SelectVideoActivity.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/SelectVideoActivity.kt index 9dc6a1c6..2d17beba 100644 --- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/SelectVideoActivity.kt +++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/SelectVideoActivity.kt @@ -18,7 +18,6 @@ import androidx.lifecycle.repeatOnLifecycle import com.boostcamp.dailyfilm.R import com.boostcamp.dailyfilm.databinding.ActivitySelectVideoBinding import com.boostcamp.dailyfilm.presentation.BaseActivity -import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity.Companion.FLAG_FROM_VIEW import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity.Companion.KEY_DATE_MODEL import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity.Companion.KEY_EDIT_STATE @@ -42,6 +41,11 @@ class SelectVideoActivity : override fun initView() { binding.viewModel = viewModel + binding.mediaListCompose.setContent { + VideoLists( + viewModel = viewModel + ) + } checkPermission() setObserveUserEvent() } diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/SelectVideoBindingAdapter.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/SelectVideoBindingAdapter.kt index 8668557a..705516ce 100644 --- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/SelectVideoBindingAdapter.kt +++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/SelectVideoBindingAdapter.kt @@ -3,18 +3,8 @@ package com.boostcamp.dailyfilm.presentation.selectvideo import android.net.Uri import android.widget.ImageView import android.widget.TextView -import androidx.constraintlayout.widget.ConstraintLayout import androidx.databinding.BindingAdapter import androidx.lifecycle.* -import androidx.paging.PagingData -import androidx.recyclerview.widget.ConcatAdapter -import androidx.recyclerview.widget.ConcatAdapter.Config -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.boostcamp.dailyfilm.data.model.VideoItem -import com.boostcamp.dailyfilm.presentation.selectvideo.adapter.SelectVideoAdapter -import com.boostcamp.dailyfilm.presentation.selectvideo.adapter.VideoLoadStateAdapter -import com.boostcamp.dailyfilm.presentation.selectvideo.adapter.VideoSelectListener import com.bumptech.glide.Glide import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.MediaItem @@ -24,29 +14,6 @@ import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -@BindingAdapter(value = ["thisItem", "selectedVideo"], requireAll = false) -fun ConstraintLayout.chooseVideoAndUpdateAlpha( - videoItem: VideoItem, - clickListener: VideoSelectListener -) { - with(clickListener) { - viewTreeLifecycleScope.launch { - selectedVideo.collect { - alpha = if (videoItem.uri == it?.uri) - 0.5f - else - 1.0f - } - } - - if (selectedVideo.value == null) { - alpha = 0.5f - clickListener.chooseVideo(videoItem) - } - - } -} - @BindingAdapter("onUploaded") fun TextView.showResultOnSnackBar(uploadResult: SharedFlow) { findViewTreeLifecycleOwner()?.lifecycleScope?.launch { @@ -76,44 +43,6 @@ fun StyledPlayerView.playVideo(uri: Uri?) { } } -@BindingAdapter(value = ["setVideoSelectListener", "updateAdapter"], requireAll = true) -fun RecyclerView.updateAdapter( - videoClickListener: VideoSelectListener, - videosState: StateFlow> -) { - if (adapter == null) { - itemAnimator = null - val newAdapter = SelectVideoAdapter(videoClickListener) - adapter = newAdapter.withLoadStateFooter( - footer = VideoLoadStateAdapter(newAdapter::retry) - ).apply { - Config.Builder().apply { - setIsolateViewTypes(false) - }.build() - } - - this.layoutManager = GridLayoutManager(this.context, 3).apply { - spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { - override fun getSpanSize(position: Int): Int { - return when ((adapter as ConcatAdapter).getItemViewType(position)) { - 1 -> { - 3 - } - else -> { - 1 - } - } - } - } - } - } - findViewTreeLifecycleOwner()?.lifecycleScope?.launch { - findViewTreeLifecycleOwner()?.repeatOnLifecycle(Lifecycle.State.STARTED) { - ((adapter as ConcatAdapter).adapters[0] as SelectVideoAdapter).submitData(videosState.value) - } - } -} - @BindingAdapter("updateThumbnails") fun ImageView.updateThumbnails(uri: Uri?) { uri?.let { diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/SelectVideoCompose.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/SelectVideoCompose.kt new file mode 100644 index 00000000..5b9f58a0 --- /dev/null +++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/SelectVideoCompose.kt @@ -0,0 +1,105 @@ +package com.boostcamp.dailyfilm.presentation.selectvideo + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.LoadState +import androidx.paging.compose.collectAsLazyPagingItems +import com.boostcamp.dailyfilm.R +import com.boostcamp.dailyfilm.data.model.VideoItem +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage + + +@Composable +fun VideoLists( + modifier: Modifier = Modifier, + viewModel: SelectVideoViewModel +) { + val selectedVideo = viewModel.selectedVideo.collectAsStateWithLifecycle() + val nestedScrollInterop = rememberNestedScrollInteropConnection() + val lazyPagingItems = viewModel.videoItems.collectAsLazyPagingItems() + + if (lazyPagingItems.itemCount > 0) { + LaunchedEffect(selectedVideo.value == null) { + viewModel.chooseVideo(lazyPagingItems[0]) + } + } + + // RecyclerView 역할 (GridLayout) + LazyVerticalGrid( + modifier = modifier + .padding(top = 3.dp, start = 3.dp, end = 3.dp) + .nestedScroll(nestedScrollInterop), + verticalArrangement = Arrangement.spacedBy(3.dp), + horizontalArrangement = Arrangement.spacedBy(3.dp), + columns = GridCells.Adaptive(minSize = 120.dp), + ) { + + items(lazyPagingItems.itemCount) { idx -> + + val isSelected = selectedVideo.value == lazyPagingItems[idx] + val alpha by animateFloatAsState(if (isSelected) 0.5f else 1f) + + VideoGrid( + modifier = Modifier + .clickable { viewModel.chooseVideo(lazyPagingItems[idx]) } + .alpha(alpha), + videoItem = lazyPagingItems[idx], + ) + + } + + // Loading시 Indicator 보이기 + if (lazyPagingItems.loadState.append == LoadState.Loading) { + item { + CircularProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally), + color = colorResource(id = R.color.Primary) + ) + } + } + } + +} + +@OptIn(ExperimentalGlideComposeApi::class) +@Composable +fun VideoGrid( + modifier: Modifier = Modifier, + videoItem: VideoItem? +) { + + if (videoItem != null) { + GlideImage( + modifier = modifier + .aspectRatio(1f), + contentScale = ContentScale.Crop, + model = videoItem.uri, + contentDescription = null + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/SelectVideoViewModel.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/SelectVideoViewModel.kt index 1b0c74a3..eb00283d 100644 --- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/SelectVideoViewModel.kt +++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/SelectVideoViewModel.kt @@ -3,6 +3,7 @@ package com.boostcamp.dailyfilm.presentation.selectvideo import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.Pager import androidx.paging.PagingData import androidx.paging.cachedIn import com.boostcamp.dailyfilm.data.model.VideoItem @@ -12,10 +13,8 @@ import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity.Companion. import com.boostcamp.dailyfilm.presentation.calendar.DateFragment import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel import com.boostcamp.dailyfilm.presentation.playfilm.model.EditState -import com.boostcamp.dailyfilm.presentation.selectvideo.adapter.VideoSelectListener import com.boostcamp.dailyfilm.presentation.uploadfilm.model.DateAndVideoModel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -24,26 +23,23 @@ import javax.inject.Inject class SelectVideoViewModel @Inject constructor( private val selectVideoRepository: GalleryVideoRepository, savedStateHandle: SavedStateHandle -) : ViewModel(), VideoSelectListener { +) : ViewModel() { val dateModel = savedStateHandle.get(CalendarActivity.KEY_DATE_MODEL) val calendarIndex = savedStateHandle.get(DateFragment.KEY_CALENDAR_INDEX) val editState = savedStateHandle.get(KEY_EDIT_STATE) - override val viewTreeLifecycleScope: CoroutineScope - get() = viewModelScope - - private val _videosState = MutableStateFlow>(PagingData.empty()) - val videosState: StateFlow> get() = _videosState - private val _selectedVideo = MutableStateFlow(null) - override val selectedVideo = _selectedVideo.asStateFlow() + val selectedVideo = _selectedVideo.asStateFlow() private var clickSound = false private val _eventFlow = MutableSharedFlow() val eventFlow: SharedFlow = _eventFlow.asSharedFlow() + private val _videoItems = MutableStateFlow>(PagingData.empty()) + val videoItems: StateFlow> get() = _videoItems + fun navigateToUpload() { viewModelScope.launch { selectedVideo.value?.let { selectedVideoItem -> @@ -72,7 +68,7 @@ class SelectVideoViewModel @Inject constructor( fun loadVideo() { selectVideoRepository.loadVideo().cachedIn(viewModelScope).onEach { pagingData -> - _videosState.value = pagingData + _videoItems.value = pagingData }.launchIn(viewModelScope) } @@ -82,7 +78,7 @@ class SelectVideoViewModel @Inject constructor( } } - override fun chooseVideo(videoItem: VideoItem?) { + fun chooseVideo(videoItem: VideoItem?) { viewModelScope.launch { _selectedVideo.emit(videoItem) } diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/SelectVideoAdapter.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/SelectVideoAdapter.kt deleted file mode 100644 index e154c350..00000000 --- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/SelectVideoAdapter.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.boostcamp.dailyfilm.presentation.selectvideo.adapter - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.databinding.DataBindingUtil -import androidx.recyclerview.widget.DiffUtil -import androidx.paging.PagingDataAdapter -import com.boostcamp.dailyfilm.R -import com.boostcamp.dailyfilm.data.model.VideoItem - -class SelectVideoAdapter( - private val videoSelectListener: VideoSelectListener -) : PagingDataAdapter(diffUtil) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SelectVideoViewHolder { - return SelectVideoViewHolder( - DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - R.layout.item_select_video, - parent, - false - ), - videoSelectListener - ) - } - - override fun onBindViewHolder(holder: SelectVideoViewHolder, position: Int) { - holder.bind(getItem(position) ?: return) - } - - companion object { - - val diffUtil = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: VideoItem, newItem: VideoItem): Boolean { - return oldItem.uri == newItem.uri - } - - override fun areContentsTheSame(oldItem: VideoItem, newItem: VideoItem): Boolean { - return oldItem == newItem - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/SelectVideoViewHolder.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/SelectVideoViewHolder.kt deleted file mode 100644 index fbdf8463..00000000 --- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/SelectVideoViewHolder.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.boostcamp.dailyfilm.presentation.selectvideo.adapter - -import androidx.core.view.doOnAttach -import androidx.core.view.doOnDetach -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.findViewTreeLifecycleOwner -import androidx.recyclerview.widget.RecyclerView.ViewHolder -import com.boostcamp.dailyfilm.databinding.ItemSelectVideoBinding -import com.boostcamp.dailyfilm.data.model.VideoItem - -class SelectVideoViewHolder( - private val binding:ItemSelectVideoBinding, - private val videoSelectListener: VideoSelectListener - ):ViewHolder(binding.root) { - - private var lifecycleOwner: LifecycleOwner? = null - - init { - - itemView.doOnAttach { - lifecycleOwner = itemView.findViewTreeLifecycleOwner() - } - - itemView.doOnDetach { - lifecycleOwner = null - binding.item = null - binding.clickListener = null - } - - } - - fun bind(item:VideoItem){ - binding.item = item - binding.lifecycleOwner = lifecycleOwner - binding.clickListener = videoSelectListener - binding.executePendingBindings() - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/VideoLoadStateAdapter.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/VideoLoadStateAdapter.kt deleted file mode 100644 index e9e95bc2..00000000 --- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/VideoLoadStateAdapter.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.boostcamp.dailyfilm.presentation.selectvideo.adapter - -import android.view.ViewGroup -import androidx.paging.LoadState -import androidx.paging.LoadStateAdapter - -class VideoLoadStateAdapter( - private val retry: () -> Unit -) : LoadStateAdapter() { - override fun onCreateViewHolder( - parent: ViewGroup, - loadState: LoadState - ) = VideoLoadStateViewHolder(parent, retry) - - override fun onBindViewHolder( - holder: VideoLoadStateViewHolder, - loadState: LoadState - ) = holder.bind(loadState) -} \ No newline at end of file diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/VideoLoadStateViewHolder.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/VideoLoadStateViewHolder.kt deleted file mode 100644 index 5dbbfc41..00000000 --- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/VideoLoadStateViewHolder.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.boostcamp.dailyfilm.presentation.selectvideo.adapter - -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.Button -import android.widget.ProgressBar -import android.widget.TextView -import androidx.core.view.isVisible -import androidx.paging.LoadState -import androidx.recyclerview.widget.RecyclerView -import com.boostcamp.dailyfilm.R -import com.boostcamp.dailyfilm.databinding.ItemVideoLoadStateBinding - -class VideoLoadStateViewHolder( - parent: ViewGroup, - retry: () -> Unit -) : RecyclerView.ViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_video_load_state, parent, false) -) { - private val binding = ItemVideoLoadStateBinding.bind(itemView) - private val progressBar: ProgressBar = binding.loadStateProgress - private val errorMsg: TextView = binding.loadStateErrorMessage - private val retry: Button = binding.loadStateRetry - .also { - it.setOnClickListener { retry() } - } - - fun bind(loadState: LoadState) { - if (loadState is LoadState.Error) { - errorMsg.text = loadState.error.localizedMessage - } - progressBar.isVisible = loadState is LoadState.Loading - retry.isVisible = loadState is LoadState.Error - errorMsg.isVisible = loadState is LoadState.Error - - } -} \ No newline at end of file diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/VideoSelectListener.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/VideoSelectListener.kt deleted file mode 100644 index e1f76392..00000000 --- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/selectvideo/adapter/VideoSelectListener.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.boostcamp.dailyfilm.presentation.selectvideo.adapter - -import com.boostcamp.dailyfilm.data.model.VideoItem -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.StateFlow - -interface VideoSelectListener { - - val selectedVideo: StateFlow - - val viewTreeLifecycleScope: CoroutineScope - - fun chooseVideo(videoItem: VideoItem?) - -} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_select_video.xml b/app/src/main/res/layout/activity_select_video.xml index f01f8ddb..bea3875d 100644 --- a/app/src/main/res/layout/activity_select_video.xml +++ b/app/src/main/res/layout/activity_select_video.xml @@ -100,14 +100,11 @@ - + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"/> + \ No newline at end of file diff --git a/app/src/main/res/layout/item_select_video.xml b/app/src/main/res/layout/item_select_video.xml deleted file mode 100644 index 3068a667..00000000 --- a/app/src/main/res/layout/item_select_video.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file