【问题标题】:How to enable a PagerSnapHelper to snap in both directions in a RecyclerView?如何使 PagerSnapHelper 在 RecyclerView 中双向捕捉?
【发布时间】:2021-04-12 13:55:11
【问题描述】:

我已使用以下代码将 PagerSnapHelper 附加到具有水平 LinearLayoutManager 的 RecyclerView。

SnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(myRecyclerView);

但是,当我执行滚动时,捕捉只发生在一个方向上。即它在向右滑动时会吸附到下一个项目,但不会在向左滑动时吸附到上一个项目。

这是默认行为吗?我可以覆盖它以在左滑动时也能捕捉到上一个项目吗?

这里的任何指针都会有很大帮助。谢谢!

【问题讨论】:

    标签: android android-recyclerview


    【解决方案1】:

    SnapHelper 在某些情况下有时会出现问题,例如项目计数等。

    您可以使用从 LinearSnapHelper 扩展而来的 GravitySnapHelper 类。

      import android.view.Gravity; 
      import android.view.View;  
      import androidx.annotation.NonNull; 
      import androidx.annotation.Nullable; 
      import androidx.recyclerview.widget.LinearLayoutManager; 
      import androidx.recyclerview.widget.LinearSnapHelper; 
      import androidx.recyclerview.widget.OrientationHelper; 
      import androidx.recyclerview.widget.RecyclerView;
    
    
    public class GravitySnapHelper extends LinearSnapHelper {
    
        private OrientationHelper mVerticalHelper;
        private OrientationHelper mHorizontalHelper;
        private int mGravity;
        private boolean mIsRtlHorizontal;
        private boolean mSnapLastItemEnabled;
        SnapListener mSnapListener;
        boolean mSnapping;
        private RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
                    mSnapping = false;
                }
                if (newState == RecyclerView.SCROLL_STATE_IDLE && mSnapping && mSnapListener != null) {
                    int position = getSnappedPosition(recyclerView);
                    if (position != RecyclerView.NO_POSITION) {
                        mSnapListener.onSnap(position);
                    }
                    mSnapping = false;
                }
            }
        };
    
        public GravitySnapHelper(int gravity) {
            this(gravity, false, null);
        }
    
        public GravitySnapHelper(int gravity, boolean enableSnapLastItem) {
            this(gravity, enableSnapLastItem, null);
        }
    
        public GravitySnapHelper(int gravity, boolean enableSnapLastItem, SnapListener snapListener) {
            if (gravity != Gravity.START && gravity != Gravity.END
                && gravity != Gravity.BOTTOM && gravity != Gravity.TOP) {
                throw new IllegalArgumentException("Invalid gravity value. Use START " +
                                                   "| END | BOTTOM | TOP constants");
            }
            mSnapListener = snapListener;
            mGravity = gravity;
            mSnapLastItemEnabled = enableSnapLastItem;
        }
    
        @Override
        public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
                throws IllegalStateException {
            if (recyclerView != null) {
                if (mGravity == Gravity.START || mGravity == Gravity.END) {
                    mIsRtlHorizontal
                            = false;
                }
                if (mSnapListener != null) {
                    recyclerView.addOnScrollListener(mScrollListener);
                }
            }
            super.attachToRecyclerView(recyclerView);
        }
    
        @Override
        public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
                                                  @NonNull View targetView) {
            int[] out = new int[2];
    
            if (layoutManager.canScrollHorizontally()) {
                if (mGravity == Gravity.START) {
                    out[0] = distanceToStart(targetView, getHorizontalHelper(layoutManager), false);
                } else { // END
                    out[0] = distanceToEnd(targetView, getHorizontalHelper(layoutManager), false);
                }
            } else {
                out[0] = 0;
            }
    
            if (layoutManager.canScrollVertically()) {
                if (mGravity == Gravity.TOP) {
                    out[1] = distanceToStart(targetView, getVerticalHelper(layoutManager), false);
                } else { // BOTTOM
                    out[1] = distanceToEnd(targetView, getVerticalHelper(layoutManager), false);
                }
            } else {
                out[1] = 0;
            }
    
            return out;
        }
    
        @Override
        public View findSnapView(RecyclerView.LayoutManager layoutManager) {
            View snapView = null;
            if (layoutManager instanceof LinearLayoutManager) {
                switch (mGravity) {
                    case Gravity.START:
                        snapView = findStartView(layoutManager, getHorizontalHelper(layoutManager));
                        break;
                    case Gravity.END:
                        snapView = findEndView(layoutManager, getHorizontalHelper(layoutManager));
                        break;
                    case Gravity.TOP:
                        snapView = findStartView(layoutManager, getVerticalHelper(layoutManager));
                        break;
                    case Gravity.BOTTOM:
                        snapView = findEndView(layoutManager, getVerticalHelper(layoutManager));
                        break;
                }
            }
    
            mSnapping = snapView != null;
    
            return snapView;
        }
    
        /**
         * Enable snapping of the last item that's snappable.
         * The default value is false, because you can't see the last item completely
         * if this is enabled.
         *
         * @param snap true if you want to enable snapping of the last snappable item
         */
        public void enableLastItemSnap(boolean snap) {
            mSnapLastItemEnabled = snap;
        }
    
        private int distanceToStart(View targetView, OrientationHelper helper, boolean fromEnd) {
            if (mIsRtlHorizontal && !fromEnd) {
                return distanceToEnd(targetView, helper, true);
            }
    
            return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
        }
    
        private int distanceToEnd(View targetView, OrientationHelper helper, boolean fromStart) {
            if (mIsRtlHorizontal && !fromStart) {
                return distanceToStart(targetView, helper, true);
            }
    
            return helper.getDecoratedEnd(targetView) - helper.getEndAfterPadding();
        }
    
        /**
         * Returns the first view that we should snap to.
         *
         * @param layoutManager the recyclerview's layout manager
         * @param helper        orientation helper to calculate view sizes
         * @return the first view in the LayoutManager to snap to
         */
        private View findStartView(RecyclerView.LayoutManager layoutManager,
                                   OrientationHelper helper) {
    
            if (layoutManager instanceof LinearLayoutManager) {
                int firstChild = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
    
                if (firstChild == RecyclerView.NO_POSITION) {
                    return null;
                }
    
                View child = layoutManager.findViewByPosition(firstChild);
    
                float visibleWidth;
    
                // We should return the child if it's visible width
                // is greater than 0.5 of it's total width.
                // In a RTL configuration, we need to check the start point and in LTR the end point
                if (mIsRtlHorizontal) {
                    visibleWidth = (float) (helper.getTotalSpace() - helper.getDecoratedStart(child))
                                   / helper.getDecoratedMeasurement(child);
                } else {
                    visibleWidth = (float) helper.getDecoratedEnd(child)
                                   / helper.getDecoratedMeasurement(child);
                }
    
                // If we're at the end of the list, we shouldn't snap
                // to avoid having the last item not completely visible.
                boolean endOfList = ((LinearLayoutManager) layoutManager)
                                            .findLastCompletelyVisibleItemPosition()
                                    == layoutManager.getItemCount() - 1;
    
                if (visibleWidth > 0.5f && !endOfList) {
                    return child;
                } else if (mSnapLastItemEnabled && endOfList) {
                    return child;
                } else if (endOfList) {
                    return null;
                } else {
                    // If the child wasn't returned, we need to return
                    // the next view close to the start.
                    return layoutManager.findViewByPosition(firstChild + 1);
                }
            }
    
            return null;
        }
    
        private View findEndView(RecyclerView.LayoutManager layoutManager,
                                 OrientationHelper helper) {
    
            if (layoutManager instanceof LinearLayoutManager) {
                int lastChild = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
    
                if (lastChild == RecyclerView.NO_POSITION) {
                    return null;
                }
    
                View child = layoutManager.findViewByPosition(lastChild);
    
                float visibleWidth;
    
                if (mIsRtlHorizontal) {
                    visibleWidth = (float) helper.getDecoratedEnd(child)
                                   / helper.getDecoratedMeasurement(child);
                } else {
                    visibleWidth = (float) (helper.getTotalSpace() - helper.getDecoratedStart(child))
                                   / helper.getDecoratedMeasurement(child);
                }
    
                // If we're at the start of the list, we shouldn't snap
                // to avoid having the first item not completely visible.
                boolean startOfList = ((LinearLayoutManager) layoutManager)
                                              .findFirstCompletelyVisibleItemPosition() == 0;
    
                if (visibleWidth > 0.5f && !startOfList) {
                    return child;
                } else if (mSnapLastItemEnabled && startOfList) {
                    return child;
                } else if (startOfList) {
                    return null;
                } else {
                    // If the child wasn't returned, we need to return the previous view
                    return layoutManager.findViewByPosition(lastChild - 1);
                }
            }
            return null;
        }
    
        int getSnappedPosition(RecyclerView recyclerView) {
            RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
    
            if (layoutManager instanceof LinearLayoutManager) {
                if (mGravity == Gravity.START || mGravity == Gravity.TOP) {
                    return ((LinearLayoutManager) layoutManager).findFirstCompletelyVisibleItemPosition();
                } else if (mGravity == Gravity.END || mGravity == Gravity.BOTTOM) {
                    return ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition();
                }
            }
    
            return RecyclerView.NO_POSITION;
    
        }
    
        private OrientationHelper getVerticalHelper(RecyclerView.LayoutManager layoutManager) {
            if (mVerticalHelper == null) {
                mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
            }
            return mVerticalHelper;
        }
    
        private OrientationHelper getHorizontalHelper(RecyclerView.LayoutManager layoutManager) {
            if (mHorizontalHelper == null) {
                mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
            }
            return mHorizontalHelper;
        }
    
        public interface SnapListener {
            void onSnap(int position);
        }
    
    }
    

    用法很简单:

    SnapHelper snapHelper = new GravitySnapHelper(Gravity.START);
    snapHelper.attachToRecyclerView(recyclerView);
    

    【讨论】:

    • 惊人的答案,只需要更新导入,import android.view.Gravity; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearSnapHelper; import androidx.recyclerview.widget.OrientationHelper; import androidx.recyclerview.widget.RecyclerView;
    猜你喜欢
    • 1970-01-01
    • 2017-09-25
    • 2018-10-21
    • 2018-07-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-30
    相关资源
    最近更新 更多