tommybuonomo / dotsindicator

Three material Dots Indicators for view pagers in Android !
Apache License 2.0
3.44k stars 353 forks source link

Support Recyclerview with PagerSnapHelper #124

Closed mboudraa closed 2 years ago

mboudraa commented 3 years ago

VIewpaging can also be achieved very easily with a recyclerview and a PagerSnapHelper. Using a recyclerview allows for more customization as viewpager2 enforce the children to be the width and height of the viewpager

mboudraa commented 3 years ago

I managed to write an extension but refreshDots being protected and DotsIndicator not being open I can't actually add this extension in my code without modifying the library

private fun DotsIndicator.attachToRecyclerView(recyclerView: RecyclerView, snapHelper: PagerSnapHelper) {
    if (recyclerView.adapter == null) {
        throw IllegalStateException("You have to set an adapter to the recyccler view before initializing the dots indicator !")
    }

    if (recyclerView.layoutManager == null) {
        throw IllegalStateException("You have to set a layout manager to the recyccler view before initializing the dots indicator !")
    }

    val adapter = recyclerView.adapter!!
    val layoutManager = recyclerView.layoutManager!!

    recyclerView.adapter!!.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
        override fun onChanged() {
            super.onChanged()
            refreshDots()
        }
    })

    pager = object : BaseDotsIndicator.Pager {
        var onScrollListener: RecyclerView.OnScrollListener? = null

        override val isNotEmpty: Boolean
            get() = adapter.itemCount > 0
        override val currentItem: Int
            get() = layoutManager.getPosition(snapHelper.findSnapView(layoutManager)!!)
        override val isEmpty: Boolean
            get() = adapter.itemCount == 0
        override val count: Int
            get() = adapter.itemCount

        override fun setCurrentItem(item: Int, smoothScroll: Boolean) {
            if (smoothScroll) {
                recyclerView.smoothScrollToPosition(item)
            } else {
                recyclerView.scrollToPosition(item)
            }
        }

        override fun removeOnPageChangeListener() {
            onScrollListener?.let(recyclerView::removeOnScrollListener)
        }

        override fun addOnPageChangeListener(
            onPageChangeListenerHelper: OnPageChangeListenerHelper
        ) {
            onScrollListener = object : RecyclerView.OnScrollListener() {
                override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                    val snapPosition = layoutManager.getPosition(snapHelper.findSnapView(layoutManager)!!)
                    onPageChangeListenerHelper.onPageScrolled(snapPosition, dx.toFloat())
                }
            }
            recyclerView.addOnScrollListener(onScrollListener!!)
        }
    }

    refreshDots()
}
tommybuonomo commented 2 years ago

Hello @mboudraa I started to implement your changes on the develop branch (look RecyclerAttacher class), but there is a lot of bugs using recycler with snap helper I pause this for the moment, feel free to open a pull request on the project to make it work

Zhuinden commented 1 year ago
            onScrollListener = object : RecyclerView.OnScrollListener() {
                override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                    val snapPosition = layoutManager.getPosition(snapHelper.findSnapView(layoutManager)!!)
                    onPageChangeListenerHelper.onPageScrolled(snapPosition, dx.toFloat())
                }
            }

this would only work if the items are full-page, they were completely broken for me

I broke a whole lot of things so this is not PR-able, but in a horizontal recycler view, I did this to make it work:

            override fun addOnPageChangeListener(
                onPageChangeListenerHelper: OnPageChangeListenerHelper
            ) {
                onScrollListener = object : RecyclerView.OnScrollListener() {
                    private var currentDx = initialDx
                    private var currentPosition: Int? = null

                    override fun onScrolled(attachable: RecyclerView, dx: Int, dy: Int) {
                        this.currentDx += dx

                        val view = snapHelper.findSnapView(attachable.layoutManager)
                        if (view != null) {
                            val width = view.width

                            if (currentPosition == null && initialDx > width) {
                                setCurrentItem(max(this.currentDx / width, count), false) // reset
                            }

                            val newPosition = this.currentDx / width
                            this.currentPosition = newPosition

                            onPageChangeListenerHelper.onPageScrolled(
                                newPosition,
                                try {
                                    (this.currentDx % width).toFloat() / width
                                } catch (e: ArithmeticException) {
                                    0f
                                }
                            )
                        }
                    }
                }
                attachable.addOnScrollListener(onScrollListener!!)
            }
        }
    }
kevintorch commented 3 months ago

i also tried. this code working fine for me(see the recyclerAttacher in repo and tried to make it work). but animation not working for worm indicator, as for this to work base class has to change which is internal in the library.

fun BaseDotsIndicator.attachToRecyclerView(recyclerView: RecyclerView, snapHelper: SnapHelper) {
    RecyclerAttacher(snapHelper).setup(this, recyclerView)
}

class RecyclerAttacher(private val snapHelper: SnapHelper) {
    fun setup(dotsIndicator: BaseDotsIndicator, recyclerView: RecyclerView) {

        val adapter = recyclerView.adapter ?: throw IllegalStateException(
                "Please set an adapter to the recyclerview before initializing the dots indicator"
        )

        adapter.registerAdapterDataObserver(OnChangedAdapterDataObserver(onChange = {
            dotsIndicator.post { dotsIndicator.refreshDots() }
        }))
        val pager = RecyclerViewDotsPager(recyclerView, snapHelper)
        dotsIndicator.pager = pager
        dotsIndicator.refreshDots()
    }
}

class OnChangedAdapterDataObserver(private val onChange: () -> Unit) :
    RecyclerView.AdapterDataObserver() {
    override fun onChanged() {
        super.onChanged()
        onChange()
    }

    override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
        super.onItemRangeChanged(positionStart, itemCount)
        onChange()
    }

    override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
        super.onItemRangeChanged(positionStart, itemCount, payload)
        onChange()
    }

    override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
        super.onItemRangeInserted(positionStart, itemCount)
        onChange()
    }

    override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
        super.onItemRangeRemoved(positionStart, itemCount)
        onChange()
    }

    override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
        super.onItemRangeMoved(fromPosition, toPosition, itemCount)
        onChange()
    }
}

class RecyclerViewDotsPager(val recyclerView: RecyclerView, val snapHelper: SnapHelper) :
    BaseDotsIndicator.Pager {
    private var mScrollListener: RecyclerView.OnScrollListener? = null

    override val count: Int
        get() = recyclerView.adapter?.itemCount ?: 0
    override val currentItem: Int
        get() = snapHelper.findSnapView(recyclerView.layoutManager)?.let {
            recyclerView.layoutManager?.getPosition(it)
        } ?: 0
    override val isEmpty: Boolean get() = count == 0
    override val isNotEmpty: Boolean get() = !isEmpty

    override fun addOnPageChangeListener(onPageChangeListenerHelper: OnPageChangeListenerHelper) {
        mScrollListener = object : RecyclerView.OnScrollListener() {
            var mScrolled: Boolean = false

            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                super.onScrollStateChanged(recyclerView, newState)
                if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
                    mScrolled = false
                    snapHelper.findSnapView(recyclerView.layoutManager)
                        ?.let { recyclerView.layoutManager?.getPosition(it) }
                        ?.let { snapPosition ->
                            onPageChangeListenerHelper.onPageScrolled(snapPosition, 0F)
                        }
                }
            }

            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                if (dx != 0 || dy != 0) {
                    mScrolled = true
                }
            }
        }
        recyclerView.addOnScrollListener(mScrollListener!!)
    }

    override fun removeOnPageChangeListener() {
        mScrollListener?.let(recyclerView::removeOnScrollListener)
    }

    override fun setCurrentItem(item: Int, smoothScroll: Boolean) {
        if (smoothScroll) {
            recyclerView.smoothScrollToPosition(item)
        } else {
            recyclerView.scrollToPosition(item)
        }
    }
}