ansh sachdeva
ansh sachdeva

Reputation: 1181

Scrolling a RecyclerView inside another RecyclerView automatically not working correctly

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.

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 handler firing an autoscroll runner 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?

Update: thanks to @ADM , I got this working. I tweaked it as per my requirements, and had to forgo of circular scroll support in the reverse direction, but the given solution was enough to answer my query. thanks!

Upvotes: 0

Views: 570

Answers (1)

SpiritCrusher
SpiritCrusher

Reputation: 21073

Handler is not an issue here its the Runnable. you are using and posting same Runnable each time thats why its getting piled up . You can not remove the previous call because you do not have a Tag or token to this delayed call . take a look at some of Handler's method like sendMessageDelayed these might help .

After giving it some thought i think you can move the Auto scroll part to SnapHelper. Not a full prove solution but i think it will work. You might have to put few checks in SnapHelper . Give it a try and let me know . i haven't tested it.

class AutoPagedSnapHelper(private var autoScrollInterval: Long) : PagerSnapHelper() {
private var recyclerView: RecyclerView? = null
private var currentPage = 0
private var isHold = false

private val autoScrollRunnable = Runnable {
    recyclerView?.let {
        if (recyclerView?.scrollState != RecyclerView.SCROLL_STATE_DRAGGING && !isHold) {
            if (it.adapter != null) {
                val lastPageIndex = (recyclerView?.adapter!!.itemCount - 1)
                var nextIndex: Int
                nextIndex = currentPage + 1
                if (currentPage == lastPageIndex) {
                    nextIndex = 0
                }
                it.post {
                    val linearSmoothScroller = object : LinearSmoothScroller(recyclerView?.context) {
                        override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
                            return MILLISECONDS_PER_INCH / displayMetrics.densityDpi
                        }
                    }
                    linearSmoothScroller.targetPosition = nextIndex
                    (recyclerView?.layoutManager as LinearLayoutManager).startSmoothScroll(linearSmoothScroller)
                }
            }
        } else {
            postNextPage()
        }
    }
}

override fun attachToRecyclerView(recyclerView: RecyclerView?) {
    super.attachToRecyclerView(recyclerView)
    if (this.recyclerView === recyclerView) {
        return
    }
    if (autoScrollInterval != 0L) {
        this.recyclerView = recyclerView
        this.recyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                super.onScrollStateChanged(recyclerView, newState)
                if (newState == RecyclerView.SCROLL_STATE_IDLE || newState == RecyclerView.SCROLL_STATE_SETTLING) {
                    val itemPosition = (recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
                    if (itemPosition != -1) {
                        currentPage = itemPosition
                        postNextPage()
                    }
                }
            }
        })
        postNextPage()
        recyclerView?.addOnItemTouchListener(object : RecyclerView.OnItemTouchListener {
            override fun onInterceptTouchEvent(rv: RecyclerView, event: MotionEvent): Boolean {
                when (event.action) {
                    MotionEvent.ACTION_DOWN -> {
                        isHold = true
                    }
                    MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
                        isHold = false
                    }
                }
                return false
            }

            override fun onTouchEvent(rv: RecyclerView, event: MotionEvent) {}
            override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {}
        })
    }
}

fun postNextPage() {
    recyclerView?.handler?.removeCallbacks(autoScrollRunnable)
    recyclerView?.postDelayed(autoScrollRunnable, autoScrollInterval)
}
companion object {
    private const val MILLISECONDS_PER_INCH = 75f //default is 25f (bigger = slower)
}
}

This should take care of auto change page. You do not have to use scrollListener in Adapter. Give it a try.

Upvotes: 0

Related Questions