【问题标题】:Scrolling a RecyclerView inside another RecyclerView automatically not working correctly在另一个 RecyclerView 中滚动 RecyclerView 自动无法正常工作
【发布时间】:2021-03-06 17:16:35
【问题描述】:

所以我有这个 recyclerview,它将包含多种类型的持有者,其中一种可能是边缘到边缘图像的可滚动水平列表,它们会自动滚动并具有当前项目指示器。所以为此我使用了一个viewholder,它本身将包含另一个recyclerview和一个点指示器(它本身是另一个recycler view,所以基本上recyclerview = vh列表,其中一个vh = 2水平recyclerview)。

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

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

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

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

我最里面的水平图像循环视图是这样创建的:


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)
    }
}

这基本上是在愚弄适配器,以为我有一百万张图片,但实际上只有几张图片。这会产生一种循环滚动的印象。

接下来我需要更改第二个 recyclerview 的点指示器。为此,我进入了这个 recyclerview 的父级并附加了一个 onScrollListener 。 onScrollListener 给了我 2 个函数:onScrolled 和 onScrollStateChanged。

  • 使用onScrolled,我确定何时更改下一个点recyclerview 的状态以显示新点。我通过线性布局管理器做到这一点。当它给出findFirstCompletelyVisibleItemPosition 为正数时。
  • 使用onScrollStateChanged(),我运行一种递归,每当我获得状态为 SCROLL_STATE_IDLE 时,我发布一个处理程序以在 2 秒后将回收器视图滚动到下一个项目。 2 秒后,它会自动平滑滚动并再次触发相同的事件,导致处理程序再次触发相同的操作。

所以代码看起来像这样:

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

}

为简洁起见,假设每当 toast 发生时,它都会滚动第二个点指示器 recyclerview。

这一切似乎在原则上有效,但有时处理程序似乎会触发两次或三次,导致用户体验不佳。有时它甚至会发疯,停止显示任何日志或任何内容,只是让 Rails 运行得非常快,就像处理程序每​​毫秒触发一个自动滚动运行器一样。

那么有什么帮助吗?我假设在实现级别出现了问题,例如可以更好地处理触发处理程序事件?

更新: 感谢@ADM,我得到了这个工作。我根据我的要求对其进行了调整,并且不得不放弃对反向循环滚动的支持,但给定的解决方案足以回答我的问题。谢谢!

【问题讨论】:

  • 您一次又一次地发布相同的RunnablebannerChangerRunnable 这可能会造成麻烦,因为bindViewHolder 在正常情况下可以在该时间段内多次调用,而且您正在使用和延迟处理程序这也是为什么。解决方案是因为每个内部 RecyclerView 都拥有一个单独的 ImageAdapter 实例,您应该将处理程序实现移动到 ImageAdapter。您可以传入一个 lambda 以在 run 内部调用。
  • @ADM 但是将处理程序传递给 ImageAdapter 有什么用?目前,它的MainActivity&lt;--RailsRecycler(+RailsAdapter)&lt;--ImageRecycler(+ImageAdapter) railsAdpater 将为 ImageRecycler 创建一个视图,该视图将有权更改图像回收器的位置。这种权力如何转移到内部图像回收器?您能否发布一个答案并提供更多解释,因为我似乎无法想象

标签: android kotlin android-recyclerview


【解决方案1】:

Handler 在这里不是问题,它是Runnable。您每次都在使用和发布相同的Runnable,这就是它被堆积起来的原因。您无法删除之前的呼叫,因为您没有Tag 或此延迟呼叫的令牌。看看Handler's 的一些方法,比如sendMessageDelayed,这些可能会有所帮助。

经过一番思考,我认为您可以将自动滚动部分移至SnapHelper。不是一个完整的证明解决方案,但我认为它会起作用。您可能需要在 SnapHelper 中进行一些检查。试一试,让我知道。我还没有测试过。

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)
}
}

这应该处理自动更改页面。您不必在Adapter 中使用scrollListener。试试看吧。

【讨论】:

  • 嗨,这没有用。我tweaked it a little 根据我的要求,但它既不会手动启动也不会自动启动。 snap 助手工作正常。我通过以下行调用这个val middleOfInfinity = Int.MAX_VALUE / 2 + (Int.MAX_VALUE % itemList.size) ; autoScrollPageSnapper=AutoScrollPageSnapper(bannerChangerDelayMilllis) ; autoScrollPageSnapper?.attachToRecyclerView(this) ; autoScrollPageSnapper?.forceStart(middleOfInfinity)
  • 我猜视图的内部处理程序为空。还关于您之前对 VH 的评论,无论何时我们运行带有可运行对象的 handler.post,它都会调用 bindviewholder,即使 vh 不再可见?所以我想在这种情况下使用帖子总是错误的?我可以使用任何其他基于时间的类/函数吗?
  • 另外,你的意思是删除我的滚动监听器,还是我还需要那个?我的意思是,旧的逻辑是一种递归,但这样我看不到任何递归。
  • 嘿,我刚刚回到我原来的实现并将可运行定义更改为一个函数,即我将 private val bannerChangerRunnable = Runnable {... 更改为 private fun bannerChangerRunnable() = Runnable {... 。我认为这将在我们每次调用它时创建一个新的 runnable 实例。我目前没有看到正在运行的 rails 错误,将进行更多测试。但你认为这能解决问题吗?
  • 嘿@ADM,非常感谢!对于这种方法。我几乎让它工作了,它似乎只是在自动更正之前显示一次正在运行的 rails 错误,就在数据重新加载到适配器中时。所以你知道这里可能是什么错误吗?如果您也覆盖 onScrolled,日志会显示 -ve dx 值
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-09-07
  • 1970-01-01
相关资源
最近更新 更多