디자인 요구에 따라 뷰페이저에 무한스크롤과 자동스크롤을 구현해야했다.
디테일한 요구사항은 좋아요가 있으면 좋아요 한 이미지가 나오고 없으면 3개의 뷰페이저가 좌우로 흘러가는데 그것들이 다른 방향으로 무한슼크롤 + 자동스크롤이 되어야했다.
처음에 요구사항을 듣고 좌우로 흘러감 + 한개씩 보여지는것 ⇒ 뷰페이저로 해볼까? 라는 생각으로 접근했었다.
이때당시에 무한스크롤과 자동스크롤을 구현하였으나 자동스크롤에 문제가 생겼다.
뷰페이저는 기본적으로 아이템 한개를 화면에 보여주기 위해 만들어 졌다라는것을 생각하지 못했다.
자세한 코드는 포스팅에 적지 않겠지만 실패과정은 다음과같다.
자동스크롤의 좋은 예시는 배민, 요기요의 배너가 하나씩 이동하는것을 상상하면 되겠다.
자동스크롤은 기본적으로 뷰페이저 내부 아이템의 포지션을 +1씩하는것을 기반으로한다.
0 → 1 → 2 → 3 → 4 .....
문제는 이것을 어떻게 실행시켜주느냐였는데
핸들러를 이용하여 실행하였다.
핸들러의 postDelayed를 이용하여 runnable객체를 보낸다.
이때 runnable객체에는 다음 포지션으로 이동하는 메서드가 들어있다.
이렇게 구현한다면 한개씩 떨어지는 화면이 구성된다. ( 자세한 코드가 궁금하면 댓글 부탁드립니다.)
하지만 내가 원하는 그림과는 거리가 멀었다. 끊김없이 물흘러가듯이 << 디자이너님의 말씀.... ㅜㅜㅜ
그래서 뷰페이저를 과감하게 버리고 리사이클러뷰로 다시 시작하기 시작했다.
기본적인 개념은 다음과 같다.
나에게 주어진 아이템이 10개가 있다고 하자. 이 아이템에 무한대로 나와야 하는데 문제점은 1번의 아이템과 11번의 아이템이 동일한 아이템이여야 할것이다.
이를 위해서 두가지 작업을 진행하였다.
먼저 getItemCount에서 MAX_VALUE값을 주어서 아이템의 갯수를 무제한으로 지정한다.(무제한은아니고 엄청 큰수...)
두번째는
아이템의 포지션을 찾아주는 것이다.
아이템의 포지션을 찾을수 있는 bindViewHolder에서 진행하며 기존에 position값을 사용했다면
[layoutPosition % item.size]을 이용한다.
이렇게 된다면 현재 포지션 값을 아이템의 갯수로 나눠서 나머지값을 인덱스로 정한다.
예를들어 아이템의 갯수가 5라고하면
1,6,11,16,21 ... → 나머지 1
2,7,12,17,22 ... → 나머지 2
이와 같은 원리로 진행된다.
class ScrapTestAdapter(val item: List<ItemViewpager>) :
RecyclerView.Adapter<ScrapTestAdapter.PagerViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = PagerViewHolder(parent)
//Item의 갯수를 매우 많이 늘린다.
override fun getItemCount(): Int = Int.MAX_VALUE
@SuppressLint("Range")
override fun onBindViewHolder(holder: PagerViewHolder, position: Int) {
with(holder) {
//glide시 CenterCrop, 원하는 마스크를 씌우기위한 MultiTransforMation
item[layoutPosition % item.size].let { items ->
Glide.with(itemView.context).load(items.videoBackground)
.apply(
RequestOptions.bitmapTransform(
MultiTransformation(
CenterCrop(),
MaskTransformation(items.videoMask)
)
)
)
.into(videoBackground)
//클릭시 인텐트
itemView.setOnClickListener {
Intent(it.context, VideoActivity::class.java).apply {
putExtra("videoId", item[position % 7].videoId)
}.run { it.context.startActivity(this) }
}
//MULTIPLY를 이용한 컬러 필터
videoBackground.setColorFilter(
Color.parseColor(item[position % 7].videoColor),
PorterDuff.Mode.MULTIPLY
)
}
}
inner class PagerViewHolder(parent: ViewGroup) :
RecyclerView.ViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.item_scraphorizontal, parent, false)
) {
val videoBackground: ImageView = itemView.findViewById<ImageView>(R.id.scrapItemBackground)
val glide = Glide.with((itemView).context)
}
}
오토스크롤
두번째는 자동 스크롤이다. 본문의 서론에 설명했듯이 핸들러를 이용한다.
하지만 뷰페이저와 리사이클러뷰가 다른 점은 smoothScrollToPosition을 이용한다는 것이다.
기존의 참고 예제에서는 count를 만들고 100이라는 수 대신 count++을 이용하여 말그대로 무한으로 진행하지만 그렇게 진행해 본 결과 핸들러에 너무 많은 runnable을 담았다.
@AndroidEntryPoint
class ScrapMainFragment : BaseFragment(), View.OnClickListener {
//핸들러를 생성하고 runnable 객체를 생성한다.
private val handler = Handler(Looper.getMainLooper())
private val runnable = object : Runnable {
override fun run() {
binding.scrapViewpagerNoScrap.smoothScrollToPosition(100)
binding.scrapViewpagerNoScrap2.smoothScrollToPosition(100)
binding.scrapViewpagerNoScrap3.smoothScrollToPosition(100)
handler.post(this)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentScrapMainBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
launch {
check = viewModel.getCheck()
initview(check)
addListener()
}
}
private fun initview(check: Boolean) {
if (check) {
initStaggerdView()
} else {
initRecyclerView()
handler.postDelayed(runnable,100)
}
}
private fun getManager(): LinearLayoutManager =
object : LinearLayoutManager(this@ScrapMainFragment.context, HORIZONTAL, false) {
override fun smoothScrollToPosition(
recyclerView: RecyclerView,
state: RecyclerView.State,
position: Int
) {
val smoothScroller: LinearSmoothScroller =
object : LinearSmoothScroller(this@ScrapMainFragment.context) {
private val SPEED = 4000f // Change this value (default=25f)
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
return SPEED
}
}
smoothScroller.targetPosition = position
startSmoothScroll(smoothScroller)
}
}
private fun initStaggerdView() {
binding.scrapMainRecyclerview.visibility = View.VISIBLE
launch {
val scrapMainAdapter = ScrapMainAdapter()
viewModel.searchRepo(requireContext()).collectLatest {
binding.scrapMainRecyclerview.apply {
adapter = scrapMainAdapter
layoutManager = StaggeredGridLayoutManager(2, LinearLayoutManager.VERTICAL)
}
(binding.scrapMainRecyclerview.adapter as ScrapMainAdapter).submitData(it)
}
}
}
private fun initRecyclerView() {
binding.scrapViewpagerNoScrap.visibility = View.VISIBLE
binding.scrapViewpagerNoScrap2.visibility = View.VISIBLE
binding.scrapViewpagerNoScrap3.visibility = View.VISIBLE
binding.textViewNoScrap.visibility = View.VISIBLE
launch {
val responseTest: Response<RecommendedTestResponse> = service.getRecommendedTests()
if (responseTest.isSuccessful) {
val items = ScrapHorizontalFactory().settingItems(responseTest)
val items2 = ScrapHorizontalFactory().settingItems(responseTest)
val items3 = ScrapHorizontalFactory().settingItems(responseTest)
binding.scrapViewpagerNoScrap.apply {
adapter = ScrapHorizontalAdapter(items, this@ScrapMainFragment)
layoutManager = getManager()
}
binding.scrapViewpagerNoScrap2.apply {
adapter = ScrapHorizontalAdapter(items2, this@ScrapMainFragment)
layoutManager = getManager()
}
binding.scrapViewpagerNoScrap3.apply {
adapter = ScrapHorizontalAdapter(items3, this@ScrapMainFragment)
layoutManager = getManager()
}
}
}
}
override fun onResume() {
super.onResume()
launch {
check = viewModel.getCheck()
initview(check)
}
}
override fun onPause() {
super.onPause()
clearView()
handler.removeCallbacks(runnable)
}
private fun clearView() {
binding.scrapViewpagerNoScrap.visibility = View.GONE
binding.scrapViewpagerNoScrap2.visibility = View.GONE
binding.scrapViewpagerNoScrap3.visibility = View.GONE
binding.textViewNoScrap.visibility = View.GONE
binding.scrapMainRecyclerview.visibility = View.GONE
}
private fun moveToVideo(items: List<ItemScrap>, position: Int) {
items[position % items.size].let {
Intent(
requireContext(),
VideoActivity::class.java
).apply {
putExtra(
"videoId",
items[position % items.size].videoId
)
}.run { requireContext().startActivity(this) }
}
}
override fun onClick(v: View?) {
}
private fun addListener() {
launch {
val responseTest: Response<RecommendedTestResponse> = service.getRecommendedTests()
if (responseTest.isSuccessful) {
val items = ScrapHorizontalFactory().settingItems(responseTest)
val items2 = ScrapHorizontalFactory().settingItems(responseTest)
val items3 = ScrapHorizontalFactory().settingItems(responseTest)
binding.scrapViewpagerNoScrap.addOnItemTouchListener(
RecyclerTouchListener(
requireActivity(),
object : RecyclerTouchListener.ClickListener {
@SuppressLint("Recycle")
override fun onClick(view: View?, position: Int) {
moveToVideo(items, position)
}
})
)
binding.scrapViewpagerNoScrap2.addOnItemTouchListener(
RecyclerTouchListener(
requireActivity(),
object : RecyclerTouchListener.ClickListener {
@SuppressLint("Recycle")
override fun onClick(view: View?, position: Int) {
moveToVideo(items2, position)
}
})
)
binding.scrapViewpagerNoScrap3.addOnItemTouchListener(
RecyclerTouchListener(
requireActivity(),
object : RecyclerTouchListener.ClickListener {
@SuppressLint("Recycle")
override fun onClick(view: View?, position: Int) {
moveToVideo(items3, position)
}
})
)
}
}
}
}