RecyclerView SnapHelper 无法显示第一个/最后一个项目

Posted

技术标签:

【中文标题】RecyclerView SnapHelper 无法显示第一个/最后一个项目【英文标题】:RecyclerView SnapHelper fails to show first/last items 【发布时间】:2019-03-12 08:09:35 【问题描述】:

我有一个RecyclerView,它附加到LinearSnapHelper 以捕捉到中心项目。当我滚动到第一个或最后一个项目时,这些项目不再完全可见。此问题如下图所示。如何解决?

【问题讨论】:

【参考方案1】:

当第一个/最后一个项目的中心更靠近容器的中心时,会发生此问题。因此,我们应该对捕捉功能进行一些更改以忽略这种情况。由于我们需要LinearSnapHelper类中的一些字段,我们可以复制其源代码并在findCenterView方法上进行如下修改:

MyLinearSnapHelper.kt

/*
 * Copyright (C) 2016 The android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.aminography.view.component

import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.OrientationHelper
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.SnapHelper
import android.view.View

/**
 * Implementation of the [SnapHelper] supporting snapping in either vertical or horizontal
 * orientation.
 *
 *
 * The implementation will snap the center of the target child view to the center of
 * the attached [RecyclerView]. If you intend to change this behavior then override
 * [SnapHelper.calculateDistanceToFinalSnap].
 */
class MyLinearSnapHelper : SnapHelper() 
    // Orientation helpers are lazily created per LayoutManager.
    private var mVerticalHelper: OrientationHelper? = null
    private var mHorizontalHelper: OrientationHelper? = null
    override fun calculateDistanceToFinalSnap(
            layoutManager: RecyclerView.LayoutManager, targetView: View): IntArray? 
        val out = IntArray(2)
        if (layoutManager.canScrollHorizontally()) 
            out[0] = distanceToCenter(layoutManager, targetView,
                    getHorizontalHelper(layoutManager))
         else 
            out[0] = 0
        
        if (layoutManager.canScrollVertically()) 
            out[1] = distanceToCenter(layoutManager, targetView,
                    getVerticalHelper(layoutManager))
         else 
            out[1] = 0
        
        return out
    

    override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager, velocityX: Int,
                                        velocityY: Int): Int 
        if (layoutManager !is RecyclerView.SmoothScroller.ScrollVectorProvider) 
            return RecyclerView.NO_POSITION
        
        val itemCount = layoutManager.itemCount
        if (itemCount == 0) 
            return RecyclerView.NO_POSITION
        
        val currentView = findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
        val currentPosition = layoutManager.getPosition(currentView)
        if (currentPosition == RecyclerView.NO_POSITION) 
            return RecyclerView.NO_POSITION
        
        val vectorProvider = layoutManager as RecyclerView.SmoothScroller.ScrollVectorProvider
        // deltaJumps sign comes from the velocity which may not match the order of children in
        // the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to
        // get the direction.
        val vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1)
                ?: // cannot get a vector for the given position.
                return RecyclerView.NO_POSITION
        var vDeltaJump: Int
        var hDeltaJump: Int
        if (layoutManager.canScrollHorizontally()) 
            hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getHorizontalHelper(layoutManager), velocityX, 0)
            if (vectorForEnd.x < 0) 
                hDeltaJump = -hDeltaJump
            
         else 
            hDeltaJump = 0
        
        if (layoutManager.canScrollVertically()) 
            vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getVerticalHelper(layoutManager), 0, velocityY)
            if (vectorForEnd.y < 0) 
                vDeltaJump = -vDeltaJump
            
         else 
            vDeltaJump = 0
        
        val deltaJump = if (layoutManager.canScrollVertically()) vDeltaJump else hDeltaJump
        if (deltaJump == 0) 
            return RecyclerView.NO_POSITION
        
        var targetPos = currentPosition + deltaJump
        if (targetPos < 0) 
            targetPos = 0
        
        if (targetPos >= itemCount) 
            targetPos = itemCount - 1
        
        return targetPos
    

    override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? 
        if (layoutManager.canScrollVertically()) 
            return findCenterView(layoutManager, getVerticalHelper(layoutManager))
         else if (layoutManager.canScrollHorizontally()) 
            return findCenterView(layoutManager, getHorizontalHelper(layoutManager))
        
        return null
    

    private fun distanceToCenter(layoutManager: RecyclerView.LayoutManager,
                                 targetView: View, helper: OrientationHelper): Int 
        val childCenter = helper.getDecoratedStart(targetView) + helper.getDecoratedMeasurement(targetView) / 2
        val containerCenter: Int = if (layoutManager.clipToPadding) 
            helper.startAfterPadding + helper.totalSpace / 2
         else 
            helper.end / 2
        
        return childCenter - containerCenter
    

    /**
     * Estimates a position to which SnapHelper will try to scroll to in response to a fling.
     *
     * @param layoutManager The [RecyclerView.LayoutManager] associated with the attached
     * [RecyclerView].
     * @param helper        The [OrientationHelper] that is created from the LayoutManager.
     * @param velocityX     The velocity on the x axis.
     * @param velocityY     The velocity on the y axis.
     *
     * @return The diff between the target scroll position and the current position.
     */
    private fun estimateNextPositionDiffForFling(layoutManager: RecyclerView.LayoutManager,
                                                 helper: OrientationHelper, velocityX: Int, velocityY: Int): Int 
        val distances = calculateScrollDistance(velocityX, velocityY)
        val distancePerChild = computeDistancePerChild(layoutManager, helper)
        if (distancePerChild <= 0) 
            return 0
        
        val distance = if (Math.abs(distances[0]) > Math.abs(distances[1])) distances[0] else distances[1]
        return Math.round(distance / distancePerChild)
    

    /**
     * Return the child view that is currently closest to the center of this parent.
     *
     * @param layoutManager The [RecyclerView.LayoutManager] associated with the attached
     * [RecyclerView].
     * @param helper The relevant [OrientationHelper] for the attached [RecyclerView].
     *
     * @return the child view that is currently closest to the center of this parent.
     */
    private fun findCenterView(layoutManager: RecyclerView.LayoutManager,
                               helper: OrientationHelper): View? 
        // ----- Added by aminography
        if (layoutManager is LinearLayoutManager) 
            if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) 
                return layoutManager.getChildAt(0)
             else if (layoutManager.findLastCompletelyVisibleItemPosition() == layoutManager.itemCount - 1) 
                return layoutManager.getChildAt(layoutManager.itemCount - 1)
            
        
        // -----

        val childCount = layoutManager.childCount
        if (childCount == 0) 
            return null
        
        var closestChild: View? = null
        val center: Int = if (layoutManager.clipToPadding) 
            helper.startAfterPadding + helper.totalSpace / 2
         else 
            helper.end / 2
        
        var absClosest = Integer.MAX_VALUE
        for (i in 0 until childCount) 
            val child = layoutManager.getChildAt(i)
            val childCenter = helper.getDecoratedStart(child) + helper.getDecoratedMeasurement(child) / 2
            val absDistance = Math.abs(childCenter - center)
            /** if child center is closer than previous closest, set it as closest   */
            if (absDistance < absClosest) 
                absClosest = absDistance
                closestChild = child
            
        
        return closestChild
    

    /**
     * Computes an average pixel value to pass a single child.
     *
     *
     * Returns a negative value if it cannot be calculated.
     *
     * @param layoutManager The [RecyclerView.LayoutManager] associated with the attached
     * [RecyclerView].
     * @param helper        The relevant [OrientationHelper] for the attached
     * [RecyclerView.LayoutManager].
     *
     * @return A float value that is the average number of pixels needed to scroll by one view in
     * the relevant direction.
     */
    private fun computeDistancePerChild(layoutManager: RecyclerView.LayoutManager,
                                        helper: OrientationHelper): Float 
        var minPosView: View? = null
        var maxPosView: View? = null
        var minPos = Integer.MAX_VALUE
        var maxPos = Integer.MIN_VALUE
        val childCount = layoutManager.childCount
        if (childCount == 0) 
            return INVALID_DISTANCE
        
        for (i in 0 until childCount) 
            val child = layoutManager.getChildAt(i)
            val pos = layoutManager.getPosition(child!!)
            if (pos == RecyclerView.NO_POSITION) 
                continue
            
            if (pos < minPos) 
                minPos = pos
                minPosView = child
            
            if (pos > maxPos) 
                maxPos = pos
                maxPosView = child
            
        
        if (minPosView == null || maxPosView == null) 
            return INVALID_DISTANCE
        
        val start = Math.min(helper.getDecoratedStart(minPosView),
                helper.getDecoratedStart(maxPosView))
        val end = Math.max(helper.getDecoratedEnd(minPosView),
                helper.getDecoratedEnd(maxPosView))
        val distance = end - start
        return if (distance == 0) 
            INVALID_DISTANCE
         else 1f * distance / (maxPos - minPos + 1)
    

    private fun getVerticalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper 
        if (mVerticalHelper == null || mVerticalHelper!!.layoutManager !== layoutManager) 
            mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager)
        
        return mVerticalHelper!!
    

    private fun getHorizontalHelper(
            layoutManager: RecyclerView.LayoutManager): OrientationHelper 
        if (mHorizontalHelper == null || mHorizontalHelper!!.layoutManager !== layoutManager) 
            mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager)
        
        return mHorizontalHelper!!
    

    companion object 
        private const val INVALID_DISTANCE = 1f
    


【讨论】:

【参考方案2】:

我知道我迟到了,但我想建议一个用 Java 代码编写的简单解决方案:

创建CustomSnapHelper 类:

 public class CustomSnapHelper extends LinearSnapHelper 
        @Override
        public View findSnapView(RecyclerView.LayoutManager layoutManager) 
            if(layoutManager instanceof LinearLayoutManager)
                LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
                if(needToDoSnap(linearLayoutManager)==false)
                    return null;
                
            
            return super.findSnapView(layoutManager);
        
        public boolean needToDoSnap(LinearLayoutManager linearLayoutManager)
            return linearLayoutManager.findFirstCompletelyVisibleItemPosition()!=0&&linearLayoutManager.findLastCompletelyVisibleItemPosition()!=linearLayoutManager.getItemCount()-1;
        
    

附加CustomSnapHelper 的对象以供回收站查看:

CustomSnapHelper mSnapHelper = new CustomSnapHelper();
mSnapHelper.attachToRecyclerView(mRecyclerView);

【讨论】:

谢谢!我希望它会帮助某人:)【参考方案3】:

我尝试实施一个简单的解决方案。基本上我检查了第一个/最后一个项目是否完全可见。如果是这样,我们不需要执行快照。请参阅下面的解决方案:

class CarouselSnapHelper : LinearSnapHelper() 

    override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? 
        val linearLayoutManager = layoutManager as? LinearLayoutManager
            ?: return super.findSnapView(layoutManager)

        return linearLayoutManager
            .takeIf  isValidSnap(it) 
            ?.run  super.findSnapView(layoutManager) 
    

    private fun isValidSnap(linearLayoutManager: LinearLayoutManager) =
        linearLayoutManager.findFirstCompletelyVisibleItemPosition() != 0 &&
            linearLayoutManager.findLastCompletelyVisibleItemPosition() != linearLayoutManager.itemCount - 1

【讨论】:

对不起,我现在无法检查这个答案,但我希望这可以帮助其他用户:)【参考方案4】:

我找到了一个侵入性较小的答案:

private class PagerSelectSnapHelper : LinearSnapHelper() 

    override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? 
        // Use existing LinearSnapHelper but override when the itemDecoration calculations are off
        val snapView = super.findSnapView(layoutManager)
        return if (!snapView.isViewInCenterOfParent(layoutManager.width)) 
            val endView = layoutManager.findViewByPosition(layoutManager.itemCount - 1)
            val startView = layoutManager.findViewByPosition(0)

            when 
                endView.isViewInCenterOfParent(layoutManager.width) -> endView
                startView.isViewInCenterOfParent(layoutManager.width) -> startView
                else -> snapView
            
         else 
            snapView
        
    

    private fun View?.isViewInCenterOfParent(parentWidth: Int): Boolean 
        if (this == null || width == 0) 
            return false
        
        val parentCenter = parentWidth / 2
        return left < parentCenter && parentCenter < right
    

【讨论】:

谢谢!我试试看:)

以上是关于RecyclerView SnapHelper 无法显示第一个/最后一个项目的主要内容,如果未能解决你的问题,请参考以下文章

SnapHelper 项目位置

全面解析:SnapHelper用法、原理,含演示效果对比+实现代码

RecyclerView SnapHelper 无法显示第一个/最后一个项目

关于RecyclerView

将RecyclerView项目更改为高于所有其他项目

RecyclerView的好朋友 — SnapHelpter