728x90

뷰페이저 무한 스크롤

 

기존의 자동 페이징 기능만 있던 뷰 페이저를 계속 스크롤할 수 있게 변경했다. 

구글링을 해서 찾아본 결과 지원하는 메서드는 따로 없는 것 같고, 눈 속임을 하는 듯하다. 

 

설명 예시1
설명 예시2

 

 

무한 스크롤 검색 예제

검색으로 자료를 찾아 봤을 때 예제들이 거의 이런 방식이다. 

override fun getItemCount(): Int = Int.MAX_VALUE

Int가 표현할 수 있는 Max인 억 자리 숫자를 return 해주고, position을 list의 크기로 나눈 나머지 값을 사용하면 계속 스크롤되는 것처럼 보이는 편법을 사용하는 듯하다. 

 

코드적용 

 

UpcomingShowAdapter 

더보기
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.nbc.curtaincall.databinding.ItemUpcomingShowBinding
import com.nbc.curtaincall.fetch.model.DbResponse


class UpcomingShowAdapter(private val listener: PosterClickListener? = null) :
    ListAdapter<DbResponse, UpcomingShowAdapter.UpcomingShowViewHolder>(object :
        DiffUtil.ItemCallback<DbResponse>() {
        override fun areItemsTheSame(oldItem: DbResponse, newItem: DbResponse): Boolean {
            return oldItem.mt20id == newItem.mt20id
        }

        override fun areContentsTheSame(oldItem: DbResponse, newItem: DbResponse): Boolean {
            return oldItem == newItem
        }
    }) {
    inner class UpcomingShowViewHolder(private val binding: ItemUpcomingShowBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(item: DbResponse) {
            with(binding) {
                Glide.with(itemView).load(item.poster).into(ivHomeUpcomingShowPoster)
                tvPerformanceName.text = item.prfnm
                tvPeriod.text = "${item.prfpdfrom} ~ ${item.prfpdto}"
                tvFacilityName.text = item.fcltynm
                ivHomeUpcomingShowPoster.setOnClickListener {
                    item.mt20id?.let { id -> listener?.posterClicked(id) }
                }
            }
        }
    }
    //Int.MAX_VALUE 가 아닌 받아오는 list의 사이즈 의 제곱을 리턴 해줌
    override fun getItemCount(): Int = currentList.size * currentList.size
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UpcomingShowViewHolder {
        val inflater =
            parent.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        val binding = ItemUpcomingShowBinding.inflate(inflater, parent, false)
        return UpcomingShowViewHolder(binding)
    }

    override fun onBindViewHolder(holder: UpcomingShowViewHolder, position: Int) {
        holder.bind(currentList[position % currentList.size])
    }
}

 

HomeFragment

더보기
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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.nbc.curtaincall.R
import com.nbc.curtaincall.databinding.FragmentHomeBinding
import com.nbc.curtaincall.fetch.network.retrofit.RetrofitClient.fetch
import com.nbc.curtaincall.fetch.repository.impl.FetchRepositoryImpl
import com.nbc.curtaincall.presentation.home.HorizontalMarginItemDecoration
import com.nbc.curtaincall.ui.home.adapter.GenreAdapter
import com.nbc.curtaincall.ui.home.adapter.KidShowAdapter
import com.nbc.curtaincall.ui.home.adapter.PosterClickListener
import com.nbc.curtaincall.ui.home.adapter.TopRankAdapter
import com.nbc.curtaincall.ui.home.adapter.UpcomingShowAdapter
import com.nbc.curtaincall.ui.main.MainViewModel
import com.nbc.curtaincall.ui.main.MainViewModelFactory
import com.nbc.curtaincall.ui.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)
            //인디케이터를 표시하기 위한 코드 EX) (1/10)
            binding.tvPageIndicator.text =
                "${(position % upComingShowAdapter.currentList.size) + 1} / ${upComingShowAdapter.currentList.size}"
        }
    }

    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)

            //PageChangeCallback
            registerOnPageChangeCallback(onPageChangeCallback)

        }
        //장르 연극 초기화
        viewModel.fetchGenre(0)
    }

    //옵저브 세팅
    private fun setUpObserve() {
        with(viewModel) {
            showList.observe(viewLifecycleOwner) {
                upComingShowAdapter.submitList(it)
                //포지션을 중간위치에 맞추기 위한 코드 / 추가된 리스트의 크기가 짝수/홀수 일때 처리 (인디케이터) 
                binding.viewPager.currentItem =
                    if (upComingShowAdapter.itemCount % 2 == 0) (upComingShowAdapter.itemCount / 2)
                    else (upComingShowAdapter.itemCount / 2) - (upComingShowAdapter.currentList.size / 2)
                //페이징 초기화
                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 == upComingShowAdapter.itemCount - 1) {
                    lifecycleScope.launch {
                        delay(3000)
                    }
                    viewPager.currentItem =
                        (upComingShowAdapter.itemCount / 2) - (upComingShowAdapter.currentList.size / 2)
                } else {
                    viewPager.currentItem++
                }
            }
        }
    }

    //페이징 스타트 함수
    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)
    }
}

 

메모리 누수가 생겨서 

 

 

참고 

https://medium.com/@ashishpandey.professional/android-infinite-scrolling-viewpager2-195e3786ec3f

 

Android Infinite scrolling ViewPager2

Achieve the smooth bi-directional infinite scrolling ViewPager 2

medium.com

 

+ Recent posts