姬長信(Redy)

android – 如何捕捉RecyclerView项目,以便每个…


背景

可以使用以下方法将RecyclerView捕捉到其中心:

LinearSnapHelper().attachToRecyclerView(recyclerView)

例:

MainActivity.kt

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val inflater = LayoutInflater.from(this)

        recyclerView.adapter = object : RecyclerView.Adapter() {
            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                val textView = holder.itemView as TextView
                textView.setBackgroundColor(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
                textView.text = position.toString()
            }

            override fun getItemCount(): Int {
                return 100
            }

            override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
                val view = inflater.inflate(android.R.layout.simple_list_item_1, parent, false) as TextView
                val cellSize = recyclerView.width / 3
                view.layoutParams.height = cellSize
                view.layoutParams.width = cellSize
                view.gravity = Gravity.CENTER
                return object : RecyclerView.ViewHolder(view) {}
            }
        }
        LinearSnapHelper().attachToRecyclerView(recyclerView)
    }
}

activity_main.xml中


它也可以将其捕捉到其他方面,就像在某些库中所做的那样,例如here.

还有一些库允许使用可以像ViewPager一样工作的RecyclerView,例如here.

问题

假设我有一个包含许多项目的RecyclerView(在我的情况下为水平),我希望它将每个X项目(X是常量)视为一个单元,并对齐每个单元.

例如,如果我滚动一下,它可以捕捉到0项或X项,但不能捕捉它们之间的某些内容.

在某种程度上,它的行为与普通ViewPager的情况类似,只是每个页面中都有X个项目.

例如,如果我们从上面编写的示例代码继续,假设X == 3,则捕捉将来自此空闲状态:

到这个空闲状态(如果我们滚动得足够多,否则将保持在以前的状态):

更多的投掷或滚动应该像在ViewPager上一样处理,就像我上面提到的库一样.

向下一个捕捉点滚动更多(朝同一方向)将是到达项目“6”,“9”,依此类推……

我尝试了什么

我试图搜索替代库,我也尝试阅读有关此问题的文档,但我没有找到任何可能有用的文档.

也许可以通过使用ViewPager,但我认为这不是最好的方式,因为ViewPager不能很好地回收它的项目,我认为它在如何捕捉方面不如RecyclerView灵活.

问题

>是否可以将RecyclerView设置为捕捉每个X项目,将每个X项目视为单个页面以捕捉到?

当然,这些物品将为整个RecyclerView提供足够的空间,均匀分布.
>假设有可能,当RecyclerView即将捕捉到某个项目(包括拥有此项目)之前,如何捕获它之前我将如何获得回调?我问这个是因为它与我问here的问题有关.

Kotlin解决方案

基于“Cheticamp”答案(here)的工作Kotlin解决方案,无需验证您是否具有RecyclerView大小,并且在示例中选择了网格而不是列表:

MainActivity.kt

class MainActivity : AppCompatActivity() {
    val USE_GRID = false
    //        val USE_GRID = true
    val ITEMS_PER_PAGE = 4
    var selectedItemPos = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val inflater = LayoutInflater.from(this)

        recyclerView.adapter = object : RecyclerView.Adapter() {
            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                val textView = holder.itemView as TextView
                textView.setBackgroundColor(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
                textView.text = if (selectedItemPos == position) "selected: $position" else position.toString()
            }

            override fun getItemCount(): Int {
                return 100
            }

            override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
                val view = inflater.inflate(android.R.layout.simple_list_item_1, parent, false) as TextView
                view.layoutParams.width = if (USE_GRID)
                    recyclerView.width / (ITEMS_PER_PAGE / 2)
                else
                    recyclerView.width / 4
                view.layoutParams.height = recyclerView.height / (ITEMS_PER_PAGE / 2)
                view.gravity = Gravity.CENTER
                return object : RecyclerView.ViewHolder(view) {
                }
            }
        }
        recyclerView.layoutManager = if (USE_GRID)
            GridLayoutManager(this, ITEMS_PER_PAGE / 2, GridLayoutManager.HORIZONTAL, false)
        else
            LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
        val snapToBlock = SnapToBlock(recyclerView, ITEMS_PER_PAGE)
        snapToBlock.attachToRecyclerView(recyclerView)
        snapToBlock.setSnapBlockCallback(object : SnapToBlock.SnapBlockCallback {
            override fun onBlockSnap(snapPosition: Int) {
                if (selectedItemPos == snapPosition)
                    return
                selectedItemPos = snapPosition
                recyclerView.adapter.notifyDataSetChanged()
            }

            override fun onBlockSnapped(snapPosition: Int) {
                if (selectedItemPos == snapPosition)
                    return
                selectedItemPos = snapPosition
                recyclerView.adapter.notifyDataSetChanged()
            }

        })
    }

}

SnapToBlock.kt

/**@param maxFlingBlocks Maxim blocks to move during most vigorous fling*/
class SnapToBlock constructor(private val maxFlingBlocks: Int) : SnapHelper() {
    private var recyclerView: RecyclerView? = null
    // Total number of items in a block of view in the RecyclerView
    private var blocksize: Int = 0
    // Maximum number of positions to move on a fling.
    private var maxPositionsToMove: Int = 0
    // Width of a RecyclerView item if orientation is horizonal; height of the item if vertical
    private var itemDimension: Int = 0
    // Callback interface when blocks are snapped.
    private var snapBlockCallback: SnapBlockCallback? = null
    // When snapping, used to determine direction of snap.
    private var priorFirstPosition = RecyclerView.NO_POSITION
    // Our private scroller
    private var scroller: Scroller? = null
    // Horizontal/vertical layout helper
    private var orientationHelper: OrientationHelper? = null
    // LTR/RTL helper
    private var layoutDirectionHelper: LayoutDirectionHelper? = null

    @Throws(IllegalStateException::class)
    override fun attachToRecyclerView(recyclerView: RecyclerView?) {
        if (recyclerView != null) {
            this.recyclerView = recyclerView
            val layoutManager = recyclerView.layoutManager as LinearLayoutManager
            orientationHelper = when {
                layoutManager.canScrollHorizontally() -> OrientationHelper.createHorizontalHelper(layoutManager)
                layoutManager.canScrollVertically() -> OrientationHelper.createVerticalHelper(layoutManager)
                else -> throw IllegalStateException("RecyclerView must be scrollable")
            }
            scroller = Scroller(this.recyclerView!!.context, sInterpolator)
            initItemDimensionIfNeeded(layoutManager)
        }
        super.attachToRecyclerView(recyclerView)
    }

    // Called when the target view is available and we need to know how much more
    // to scroll to get it lined up with the side of the RecyclerView.
    override fun calculateDistanceToFinalSnap(layoutManager: RecyclerView.LayoutManager, targetView: View): IntArray {
        val out = IntArray(2)
        initLayoutDirectionHelperIfNeeded(layoutManager)
        if (layoutManager.canScrollHorizontally())
            out[0] = layoutDirectionHelper!!.getScrollToAlignView(targetView)
        if (layoutManager.canScrollVertically())
            out[1] = layoutDirectionHelper!!.getScrollToAlignView(targetView)
        if (snapBlockCallback != null)
            if (out[0] == 0 && out[1] == 0)
                snapBlockCallback!!.onBlockSnapped(layoutManager.getPosition(targetView))
            else
                snapBlockCallback!!.onBlockSnap(layoutManager.getPosition(targetView))
        return out
    }

    private fun initLayoutDirectionHelperIfNeeded(layoutManager: RecyclerView.LayoutManager) {
        if (layoutDirectionHelper == null)
            if (layoutManager.canScrollHorizontally())
                layoutDirectionHelper = LayoutDirectionHelper()
            else if (layoutManager.canScrollVertically())
            // RTL doesn't matter for vertical scrolling for this class.
                layoutDirectionHelper = LayoutDirectionHelper(false)
    }

    // We are flinging and need to know where we are heading.
    override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager, velocityX: Int, velocityY: Int): Int {
        initLayoutDirectionHelperIfNeeded(layoutManager)
        val lm = layoutManager as LinearLayoutManager
        initItemDimensionIfNeeded(layoutManager)
        scroller!!.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE)
        return when {
            velocityX != 0 -> layoutDirectionHelper!!.getPositionsToMove(lm, scroller!!.finalX, itemDimension)
            else -> if (velocityY != 0)
                layoutDirectionHelper!!.getPositionsToMove(lm, scroller!!.finalY, itemDimension)
            else RecyclerView.NO_POSITION
        }
    }

    // We have scrolled to the neighborhood where we will snap. Determine the snap position.
    override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
        // Snap to a view that is either 1) toward the bottom of the data and therefore on screen,
        // or, 2) toward the top of the data and may be off-screen.
        val snapPos = calcTargetPosition(layoutManager as LinearLayoutManager)
        val snapView = if (snapPos == RecyclerView.NO_POSITION)
            null
        else
            layoutManager.findViewByPosition(snapPos)
        if (snapView == null)
            Log.d(TAG, "= priorFirstPosition) {
            // Scrolling toward bottom of data
            val firstCompletePosition = layoutManager.findFirstCompletelyVisibleItemPosition()
            snapPos = if (firstCompletePosition != RecyclerView.NO_POSITION && firstCompletePosition % blocksize == 0)
                firstCompletePosition
            else
                roundDownToBlockSize(firstVisiblePos + blocksize)
        } else {
            // Scrolling toward top of data
            snapPos = roundDownToBlockSize(firstVisiblePos)
            // Check to see if target view exists. If it doesn't, force a smooth scroll.
            // SnapHelper only snaps to existing views and will not scroll to a non-existant one.
            // If limiting fling to single block, then the following is not needed since the
            // views are likely to be in the RecyclerView pool.
            if (layoutManager.findViewByPosition(snapPos) == null) {
                val toScroll = layoutDirectionHelper!!.calculateDistanceToScroll(layoutManager, snapPos)
                recyclerView!!.smoothScrollBy(toScroll[0], toScroll[1], sInterpolator)
            }
        }
        priorFirstPosition = firstVisiblePos
        return snapPos
    }

    private fun initItemDimensionIfNeeded(layoutManager: RecyclerView.LayoutManager) {
        if (itemDimension != 0)
            return
        val child = layoutManager.getChildAt(0) ?: return
        if (layoutManager.canScrollHorizontally()) {
            itemDimension = child.width
            blocksize = getSpanCount(layoutManager) * (recyclerView!!.width / itemDimension)
        } else if (layoutManager.canScrollVertically()) {
            itemDimension = child.height
            blocksize = getSpanCount(layoutManager) * (recyclerView!!.height / itemDimension)
        }
        maxPositionsToMove = blocksize * maxFlingBlocks
    }

    private fun getSpanCount(layoutManager: RecyclerView.LayoutManager): Int = (layoutManager as? GridLayoutManager)?.spanCount ?: 1

    private fun roundDownToBlockSize(trialPosition: Int): Int = trialPosition - trialPosition % blocksize

    private fun roundUpToBlockSize(trialPosition: Int): Int = roundDownToBlockSize(trialPosition + blocksize - 1)

    override fun createScroller(layoutManager: RecyclerView.LayoutManager): LinearSmoothScroller? {
        return if (layoutManager !is RecyclerView.SmoothScroller.ScrollVectorProvider)
            null
        else object : LinearSmoothScroller(recyclerView!!.context) {
            override fun onTargetFound(targetView: View, state: RecyclerView.State?, action: RecyclerView.SmoothScroller.Action) {
                val snapDistances = calculateDistanceToFinalSnap(recyclerView!!.layoutManager, targetView)
                val dx = snapDistances[0]
                val dy = snapDistances[1]
                val time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)))
                if (time > 0)
                    action.update(dx, dy, time, sInterpolator)
            }

            override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float = MILLISECONDS_PER_INCH / displayMetrics.densityDpi
        }
    }

    fun setSnapBlockCallback(callback: SnapBlockCallback?) {
        snapBlockCallback = callback
    }

    /*
        Helper class that handles calculations for LTR and RTL layouts.
     */
    private inner class LayoutDirectionHelper {
        // Is the layout an RTL one?
        private val mIsRTL: Boolean

        constructor() {
            mIsRTL = ViewCompat.getLayoutDirection(recyclerView) == ViewCompat.LAYOUT_DIRECTION_RTL
        }

        constructor(isRTL: Boolean) {
            mIsRTL = isRTL
        }

        /*
            Calculate the amount of scroll needed to align the target view with the layout edge.
         */
        fun getScrollToAlignView(targetView: View): Int = if (mIsRTL)
            orientationHelper!!.getDecoratedEnd(targetView) - recyclerView!!.width
        else
            orientationHelper!!.getDecoratedStart(targetView)

        /**
         * Calculate the distance to final snap position when the view corresponding to the snap
         * position is not currently available.
         *
         * @param layoutManager LinearLayoutManager or descendent class
         * @param targetPos     - Adapter position to snap to
         * @return int[2] {x-distance in pixels, y-distance in pixels}
         */
        fun calculateDistanceToScroll(layoutManager: LinearLayoutManager, targetPos: Int): IntArray {
            val out = IntArray(2)
            val firstVisiblePos = layoutManager.findFirstVisibleItemPosition()
            if (layoutManager.canScrollHorizontally()) {
                if (targetPos  maxPositionsToMove)
            // Clamp number of positions to move so we don't get wild flinging.
                positionsToMove = maxPositionsToMove
            if (scroll 
            var t = input
            // _o(t) = t * t * ((tension + 1) * t + tension)
            // o(t) = _o(t - 1) + 1
            t -= 1.0f
            t * t * t + 1.0f
        }

        private val MILLISECONDS_PER_INCH = 100f
        private val TAG = "SnapToBlock"
    }
}

更新

即使我已将an answer标记为已接受,因为它工作正常,我注意到它存在严重问题:

>平滑滚动似乎不能正常工作(不滚动到正确的位置).只滚动该工作是这样的(但具有“拖尾”效果):

(recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(targetPos,0)

>当切换到RTL(从右到左)语言环境,例如希伯来语(“?????”)时,它根本不让我滚动.
>我注意到onCreateViewHolder被大量调用.事实上,每次滚动时都会调用它,即使有时它应该回收ViewHolders.这意味着有过多的视图创建,也可能意味着存在内存泄漏.

我试图自己解决这些问题,但到目前为止失败了.

如果有人知道如何修复它,我将授予额外的新奖励

更新:当我们修复了RTL / LTR时,我在这篇文章中更新了Kotlin解决方案.

更新:关于第3点,这似乎是因为recyclelerView有一个视图池,它会很快被填充.为了解决这个问题,我们可以通过对我们在其中使用的每种视图类型使用recyclelerView.getRecycledViewPool().setMaxRecycledViews(viewType,Integer.MAX_VALUE)来简单地扩大池大小.真的需要这个奇怪的事情.我已经将它发布到谷歌(herehere),但被拒绝,默认情况下该池应该是无限制的.最后,我决定至少要求为所有视图类型提供更方便的功能(here).