HomeFragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.tabs.TabLayoutMediator
import com.nbc.shownect.R
import com.nbc.shownect.databinding.FragmentHomeBinding
import com.nbc.shownect.fetch.network.retrofit.RetrofitClient.fetch
import com.nbc.shownect.fetch.repository.impl.FetchRepositoryImpl
import com.nbc.shownect.presentation.home.adapter.GenreAdapter
import com.nbc.shownect.presentation.home.adapter.KidShowAdapter
import com.nbc.shownect.presentation.home.adapter.PosterClickListener
import com.nbc.shownect.presentation.home.adapter.TopRankAdapter
import com.nbc.shownect.presentation.home.adapter.UpcomingShowAdapter
import com.nbc.shownect.presentation.main.MainViewModel
import com.nbc.shownect.presentation.main.MainViewModelFactory
import com.nbc.shownect.presentation.ticket.TicketDialogFragment
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class HomeFragment : Fragment(), PosterClickListener {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
private val viewModel: HomeViewModel by viewModels {
HomeViewModelFactory(
fetchRemoteRepository = FetchRepositoryImpl(fetch),
)
}
private val sharedViewModel: MainViewModel by activityViewModels<MainViewModel> {
MainViewModelFactory(
fetchRemoteRepository = FetchRepositoryImpl(
fetch
)
)
}
private val upComingShowAdapter: UpcomingShowAdapter by lazy { UpcomingShowAdapter(this) }
private val topRankAdapter: TopRankAdapter by lazy { TopRankAdapter(this) }
private val genreAdapter: GenreAdapter by lazy { GenreAdapter(this) }
private val kidShowAdapter: KidShowAdapter by lazy { KidShowAdapter(this) }
private var isPaging = false
private var pagingJob: Job? = null
private val onPageChangeCallback: OnPageChangeCallback = object : OnPageChangeCallback() {
//페이지 선택 될 때마다 호출
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
binding.tvPageIndicator.text = "${position + 1} / 10"
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
initViews()
setUpObserve()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(viewModel) {
//공연 예정작
fetchUpcoming()
//TOP 10 공연
fetchTopRank()
//장르 스피너 선택
binding.spinnerHomeGenre.setOnSpinnerItemSelectedListener<String> { _, _, newIndex, _ ->
fetchGenre(newIndex)
}
//어린이 관람 가능 공연 목록
fetchKidShow()
}
}
//화면 초기 설정
private fun initViews() {
//어뎁터 초기화
upComingShowAdapter
topRankAdapter
kidShowAdapter
initRecyclerView()
//viewPager 연결
with(binding.viewPager) {
adapter = upComingShowAdapter
//viewPager PageTransformer 세팅
offscreenPageLimit = 1
setPageTransformer(SliderTransformer(requireContext()))
val itemDecoration = HorizontalMarginItemDecoration(
requireContext(),
R.dimen.viewpager_current_item_horizontal_margin
)
addItemDecoration(itemDecoration)
registerOnPageChangeCallback(onPageChangeCallback)
//tab 연결
TabLayoutMediator(binding.tabPosterIndicator, this) { tab, position ->
currentItem = tab.position
}.attach()
}
//장르 연극 초기화
viewModel.fetchGenre(0)
}
//옵저브 세팅
private fun setUpObserve() {
with(viewModel) {
showList.observe(viewLifecycleOwner) {
upComingShowAdapter.submitList(it)
if (!isPaging) startPaging()
}
topRank.observe(viewLifecycleOwner) {
topRankAdapter.submitList(it?.take(10))
}
genre.observe(viewLifecycleOwner) {
genreAdapter.submitList(it)
}
kidShow.observe(viewLifecycleOwner) {
kidShowAdapter.submitList(it)
}
//로딩 화면 처리
isLoadingGenre.observe(viewLifecycleOwner) {
binding.skeletonGenreLoading.isVisible = !it
}
isLoadingRecommend.observe(viewLifecycleOwner) {
binding.skeletonTopRankLoading.isVisible = !it
}
isLoadingKid.observe(viewLifecycleOwner) {
binding.skeletonKidLoading.isVisible = !it
}
}
}
//리사이클러뷰 초기화
private fun initRecyclerView() {
with(binding) {
//HOT 추천 리사이클러뷰
rvHomeTopRank.apply {
adapter = topRankAdapter
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
}
//장르별 리사이클러뷰
rvHomeGenre.apply {
adapter = genreAdapter
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
}
//어린이 공연 리사이클러뷰
rvHomeKidShow.apply {
adapter = kidShowAdapter
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
}
}
}
//3초 후 자동 페이징
private fun nextPage() {
runCatching {
with(binding) {
if (viewPager.currentItem == 9) {
lifecycleScope.launch {
delay(3000)
}
viewPager.currentItem = 0
} else {
viewPager.currentItem++
}
}
}.onFailure {
Toast.makeText(context, "Exception nextPage()", Toast.LENGTH_SHORT).show()
}
}
//페이징 스타트 함수
private fun startPaging() {
isPaging = true
pagingJob = lifecycleScope.launch {
while (true) {
delay(3000)
nextPage()
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
isPaging = false
pagingJob?.cancel()
}
//포스터 클릭 시 티켓
override fun posterClicked(id: String) {
val ticketDialog = TicketDialogFragment()
sharedViewModel.sharedShowId(id) //해당 공연의 id를 MainViewModel로 보내줌
ticketDialog.setStyle(
DialogFragment.STYLE_NORMAL,
R.style.RoundCornerBottomSheetDialogTheme
)
ticketDialog.show(childFragmentManager, ticketDialog.tag)
}
}
offscreenPageLimit = 1
- 미리 로딩할 페이지 수
SliderTransformer - 페이지 변환 효과, 스크롤될 때 각 페이지에 적용되는 시각적 변환
- transformPage 메소드는 페이지의 위치(position)에 따라 페이지의 변환을 결정
- position 값은 현재 중앙에 위치한 페이지를 기준으로 왼쪽에 있는 페이지는 음수(-1, -0.5 등), 오른쪽에 있는 페이지는 양수(0.5, 1 등)으로 표현
- page.translationX는 페이지의 가로 위치를 조정하여, 스크롤 시 페이지가 어떻게 이동할지 정의 (페이지 사이의 간격을 조정하는 데 사용)
- page.scaleY는 페이지의 세로 축 스케일을 조정하여, 페이지가 어느 정도 떨어져 있을 때 크기가 줄어들게 함
- page.alpha는 투명도 설정 (페이지가 중앙에 있을 때(position == 0)) , 최대 100% 불투명도(1.0f) 중앙에서 멀어질 수록 투명 (0.25f 는 최소 투명도)
리소스에서 다음 페이지가 보이는 부분(nextItemVisiblePx)과 현재 페이지의 수평 마진(currentItemHorizontalMarginPx)을 가져오고, 이 두 값을 합산하여 pageTranslationX를 계산 (스크롤 시 페이지의 위치 변환에 사용)
import android.content.Context
import android.view.View
import androidx.viewpager2.widget.ViewPager2
import com.nbc.shownect.R
import kotlin.math.abs
class SliderTransformer(context: Context) :
ViewPager2.PageTransformer {
override fun transformPage(page: View, position: Float) {
page.translationX = -pageTranslationX * position
page.scaleY = 1 - (0.25f * abs(position))
page.alpha = 0.25f + (1 - abs(position))
}
val nextItemVisiblePx =
context.resources.getDimension(R.dimen.viewpager_next_item_visible)
val currentItemHorizontalMarginPx =
context.resources.getDimension(R.dimen.viewpager_current_item_horizontal_margin)
val pageTranslationX = nextItemVisiblePx + currentItemHorizontalMarginPx
}
HorizontalMarginItemDecoration - 리사이클러뷰 항목에 수평 마진을 추가하는 기능 (주로 리사이클러 뷰 내부의 아이템 간격을 조정하는 데 사용)
- 생성자는 Context와 마진 값을 DP 단위로 받아 해당 값을 픽셀 단위로 변환
- context.resources.getDimension() 리소스 ID를 통해 해당 마진 값의 실제 픽셀 크기를 가져옴
- getItemOffsets() 메소드는 리사이클러뷰 내 각 항목에 대한 오프셋(여기서는 마진)을 설정, outRect.left와 outRect.right를 설정하여 각 항목의 왼쪽과 오른쪽에 마진을 적용
import android.content.Context
import android.graphics.Rect
import android.view.View
import androidx.annotation.DimenRes
import androidx.recyclerview.widget.RecyclerView
class HorizontalMarginItemDecoration(context: Context, @DimenRes horizontalMarginInDp: Int) :
RecyclerView.ItemDecoration() {
private val horizontalMarginInPx: Int =
context.resources.getDimension(horizontalMarginInDp).toInt()
override fun getItemOffsets(
outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State
) {
outRect.right = horizontalMarginInPx
outRect.left = horizontalMarginInPx
}
}
res/values/dimens/dimens.xml
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
//추가된 부분
<dimen name="viewpager_next_item_visible">26dp</dimen>
<dimen name="viewpager_current_item_horizontal_margin">42dp</dimen>
</resources>
viewpager_next_item_visible
- ViewPager2를 사용할 때, 슬라이드되고 있는 다음 항목이 얼마나 보여질지를 정의하는 값
- SliderTransformer Class에서 사용
viewpager_current_item_horizontal_margin
- 현재 활성화된 ViewPager2 항목의 수평 마진 크기를 정의
- 현재 보여지는 항목과 양 옆의 항목 사이의 간격을 결정
- HorizontalMarginItemDecoration Class 에서 사용
참고
https://github.com/unaisulhadi/ViewPager2-Carousel