Hanbit the Developer

[Kotlin] Carousel RecyclerView With PagerSnapHelper 본문

Mobile/Android

[Kotlin] Carousel RecyclerView With PagerSnapHelper

hanbikan 2022. 1. 1. 14:17

 

 > Layouts

 - Add padding to recycler view so that you can see a part of the adjacent items.

 - In the item, set width to match_parent

Red: An area for Recycler View, Blue: Areas for the items

 

 > Recycler view acting like view pager2

 - Just use PagerSnapHelper

snapHelper = PagerSnapHelper()
snapHelper.attachToRecyclerView(YOUR_RECYCLER_VIEW)

 

 > Add button in last index of recycler view

 - Add 'add_button_item.xml' which contains:

 

 - Divide view holder into two like:

class NormalItemViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
	// views for normal item
}

class AddButtonViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
    // views for add button
}

This is because there are two items(normal_item.xml, add_button_item.xml) so ViewHolder can be divided into two. Then you can handle each views for normal item and add button.

 

- Override getItemViewType(), getItemCount():

override fun getItemViewType(position: Int): Int {
        return if(position == resultList.size) {
            R.layout.add_button
        } else {
            R.layout.normal_item
        }
    }

override fun getItemCount() = recyclerViewItemList.size + 1

Add 1 to the index and the last one item is set to add_button by getItemViewType(). For example, If there is no item, getItemCount returns 1 and the only item will be add_button because (position == resultList.size) is true.

Be careful If you want to check if there is no item, use a condition (position == 1) rather than 0.

 

 - onCreateViewHolder:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    return when(viewType){
        R.layout.normal_item -> {
            val itemView = LayoutInflater.from(parent.context).inflate(R.layout.normal_item, parent, false)
            NormalItemViewHolder(itemView)
        }
        else -> {
            val itemView = LayoutInflater.from(parent.context).inflate(R.layout.add_button, parent, false)
            AddButtonViewHolder(itemView)
        }
    }
}

 

 - onBindViewHolder:

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    when(getItemViewType(position)){
        R.layout.normal_item -> {
            holder as NormalItemListViewHolder
            // For normal_item
        }
        R.layout.add_button ->{
            holder as AddButtonViewHolder
            // For add_button
        }
    }
}

 

 

 > Add dragger for 'drag and drop'

 - Implement drag and drop using ItemTouchHelper.

 

 - Restore scroll after dragging.

ItemTouchHelper doesn't handle scroll like PagerSnapHelper so that the scroll can be stop at any value, not at some specific values which reflect PagerSnapHelper. Therefore If dragging ends, the scroll must be restored.

 

Pass 'restoreScroll: () -> Unit' as a paramater of DragAdapter and the parameter will be:

snapHelper.findSnapView(layoutManager)?.let {
    val position = layoutManager.getPosition(it)
    binding.myPetListRecyclerView.smoothScrollToPosition(position)
}

It finds a current view and get position from the view and finally scroll to the position programatically.

 

And use the restoreScroll when scroll ends by overriding clearView of DragAdapter:

override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
    super.clearView(recyclerView, viewHolder)
    
    restoreScroll.invoke()
}

 

 - Override canDropOver() of DragAdapter to prevent items from dropping over add button.

override fun canDropOver(
    recyclerView: RecyclerView,
    current: RecyclerView.ViewHolder,
    target: RecyclerView.ViewHolder
): Boolean {
    return if(current.itemViewType == R.layout.add_button ||
        target.itemViewType == R.layout.add_button) false
    else super.canDropOver(recyclerView, current, target)
}

 

 - Override interpolateOutOfBoundsScroll to set scroll speed properly.

override fun interpolateOutOfBoundsScroll(
    recyclerView: RecyclerView,
    viewSize: Int,
    viewSizeOutOfBounds: Int,
    totalSize: Int,
    msSinceStartScroll: Long
): Int {
    return if(super.interpolateOutOfBoundsScroll(
                recyclerView,
                viewSize,
                viewSizeOutOfBounds,
                totalSize,
                msSinceStartScroll
            ) > 0) SCROLL_SENSITIVITY
        else -SCROLL_SENSITIVITY
}

I set the returned value to a constant because if not, 'drag and drop' was very uncomfortable.

 

 - Override onSelectedChanged for drag animation additionally.

override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
            if (viewHolder is HistoryListViewHolder) {
            	val anim = ValueAnimator.ofFloat(0.5f, 1f)
                anim.addUpdateListener { valueAnimator ->
                    val value = valueAnimator.animatedValue as Float

                    viewHolder.itemView.scaleX = value
                    viewHolder.itemView.scaleY = value
                }
                anim.duration = 100
                anim.start()
            }
        }
        super.onSelectedChanged(viewHolder, actionState)
    }

 

 

 > References

https://stackoverflow.com/questions/29106484/how-to-add-a-button-at-the-end-of-recyclerview

 

How to add a button at the end of RecyclerView?

I want to show a button at the end of RecyclerView. With ListView there was a method addFooterView(), how to do the same with RecylerView.

stackoverflow.com

 

https://developer.android.com/reference/androidx/recyclerview/widget/PagerSnapHelper

 

PagerSnapHelper  |  Android Developers

 

developer.android.com

 

https://developer.android.com/reference/androidx/recyclerview/widget/ItemTouchHelper.Callback

 

ItemTouchHelper.Callback  |  Android Developers

 

developer.android.com