Autoscroll for a recyclerview inside another recycler view (Rails) goes out of control

So am having this recyclerview which will contain holders of multiple types one of which could be a scrollable horizontal list of edge to edge images, that are being scrolled automatically and have a current item indicator. so for this i used a viewholder which will itself contain another recyclerview and a dots indicator( which itself is another recycler view, so basically recyclerview = a list of vh , where one of the vh = 2 horizontal recyclerview).

title
[A,B,C,D...]
[+ ---]

title
[A,B,C,D...]
[+ --]

title
[A,B,C,D...]
[+ --]

title
[A,B,C,D...]
[+ --]

My innermost recylerview of horizontal images is created something like this:


class ImageAdapter : RecyclerView.Adapter<ImageVH>() {
    var imageResList = mutableListOf<Int>()
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ImageVH(parent, viewType)
    override fun onBindViewHolder(holder: ImageVH, pos: Int)
    = holder.bindData(imageResList[pos % imageResList.size])
    override fun getItemCount() = Int.MAX_VALUE
}

class ImageVH(v: View) : RecyclerView.ViewHolder(v) {
    constructor(parent: ViewGroup, viewtype: Int) : this(
        LayoutInflater.from(parent.context).inflate(R.layout.item_image, parent, false)
    )

    fun bindData(imageRes: Int) {
        Glide.with(itemView.context).load("").error(imageRes).into(itemView.ivImage)
    }
}

it is basically fooling the adapter to think as if i have a million images but will actually have just a few images. this creates an impression of circular scroll.

Next i will need something to change the dots indicator of the second recyclerview. for this i went into the parent of this recyclerview and attached an onScrollListener . The onScrollListener gives me 2 function: onScrolled and onScrollStateChanged.

  • with onScrolled , i determine when to change the next dots recyclerview’s state to show the new dot. i do this via linear layout manager. when it gives findFirstCompletelyVisibleItemPosition as positive number .
  • with onScrollStateChanged(), i run a kind of recursion, where whenever i get the state as SCROLL_STATE_IDLE, I post a handler to scroll the recyclerview to next item after 2 seconds. after 2 seconds, it will automatically smooth scroll and again fire the same event, causing the handler to fire the same action again.

so the code looks something like this:

data class Rails(val title: String, val images: MutableList<Int>,val autoscroll:Boolean =false)

class RailsAdapter : RecyclerView.Adapter<RailVH>() {
    var railsList = mutableListOf<Rails>()
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = RailVH(parent, viewType)
    override fun onBindViewHolder(holder: RailVH, pos: Int) = holder.bindData(railsList[pos])
    override fun getItemCount() = railsList.size
}

class RailVH(v: View) : RecyclerView.ViewHolder(v) {
    constructor(parent: ViewGroup, viewtype: Int) : this(
        LayoutInflater.from(parent.context).inflate(R.layout.item_rails, parent, false)
    )

    private  var autoscrollImages = false
    fun bindData(rails: Rails) {
        autoscrollImages = rails.autoscroll

        with(itemView) {
            tvTitle?.text = rails.title

            rvImagers?.apply {
                adapter = ImageAdapter().also {
                    it.imageResList = rails.images
                    it.notifyDataSetChanged()
                }
                PagerSnapHelper().attachToRecyclerView(this)
                isNestedScrollingEnabled = false
                onFlingListener = null

                addOnScrollListener(onScrollListener)
            }
        }

        if(autoscrollImages){
            bannerChangerHandler.postDelayed(bannerChangerRunnable,bannerChangerDelayMilllis)

        }
    }

    private val onScrollListener = object : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            //super.onScrolled(recyclerView, dx, dy)
            val bannerLLManager = itemView.rvImagers?.layoutManager as? LinearLayoutManager
            bannerLLManager?.let { linearLayoutManager ->
                val bannerCurrentPos = linearLayoutManager.findFirstCompletelyVisibleItemPosition()
                if (bannerCurrentPos >= 0) {
                    val rvDotsDataListSize = 5
                    val positionInRange = bannerCurrentPos % rvDotsDataListSize
                    Toast.makeText(
                        itemView.context,
                        "highlight dot #$positionInRange",
                        Toast.LENGTH_SHORT
                    ).show()
                }
            }
        }
        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            //super.onScrollStateChanged(recyclerView, newState)

            when (newState) {
                RecyclerView.SCROLL_STATE_IDLE -> {
                    if(autoscrollImages){
                        Log.e(">>a>>", "RecyclerView.SCROLL_STATE_IDLE!")
                        bannerChangerHandler.postDelayed(bannerChangerRunnable, bannerChangerDelayMilllis
                        )
                    }

                }
                RecyclerView.SCROLL_STATE_DRAGGING -> {
                    Log.e(">>a>>", "RecyclerView.SCROLL_STATE_DRAGGING!")
                    bannerChangerHandler.removeCallbacks(bannerChangerRunnable)
                }
                else -> {
                }
            }


        }
    }

    private val bannerChangerHandler: Handler = Handler()

    private val bannerChangerRunnable = Runnable {
        itemView.rvImagers?.apply {
            val bannerManager = layoutManager as? LinearLayoutManager
            bannerManager?.let {
                val bannerCurrentPos = it.findFirstCompletelyVisibleItemPosition()
                smoothScrollToPosition(bannerCurrentPos + 1)
            }
        }
    }

    private var bannerChangerDelayMilllis = 2000L

}

for brevity, assume whenever the toast is occuring, its going to scroll the 2nd dots indicator recyclerview .

This all seems to work in principle, but after sometimes the handler seems to fire twice or thrice , causing bad ux. sometimes it even goes berserks and stops showing any logs or anything and just makes the rails run infinetely very fast, like rails changing every millisecond.

So any help with this? i am assuming something is wrong at the implementation level, like firing handler events could be handled better?

@mmurphy can you look into this?

My sincere apologies in the delay in responding! Something appears to have gone haywire, where I am not getting emails from my own discussion board.

That being said, I have not personally tried anything like this, and so I am afraid that I will be of little help.

never mind, i have already written some code with quirky bugs that has gone to production :sweat_smile: . I now just wish to be able to understand the working of android’s view and recycler view in depth and currently looking for simpler explanation for these