zoukankan      html  css  js  c++  java
  • Android SnapHelper

    转载请注明出处:http://blog.csdn.net/crazy1235/article/details/53386286


    SnapHelper 是 Android Support Library reversion 24.2.0 新增加的API。


    SnapHelper 的应用

    SnapHelper 是RecyclerView的一个辅助工具类。

    它实现了RecyclerView.onFlingListener接口。而RecyclerView.onFlingListener 是一个用来响应用户手势滑动的接口。

    SnapHelper是一个抽象类,官方提供了一个LinearSnapHelper子类,可以实现类似ViewPager的滚动效果,滑动结束之后让某个item停留在中间位置。

    这里写图片描述

    效果类似于Google Play主界面中item的滚动效果。


    LinearSnapHelper的使用很简单,只需要调用 attachToRecyclerView(xxx) ,绑定上一个RecyclerView即可。

    上一张自己的效果图:

    LinearSnapHelper 源码分析

    下面来分析一下 LinearSnapHelper

    先从 attachToRecyclerView() 入手。

    public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
                throws IllegalStateException {
            if (mRecyclerView == recyclerView) {
                return; // nothing to do
            }
            if (mRecyclerView != null) {
                destroyCallbacks();
            }
            mRecyclerView = recyclerView;
            if (mRecyclerView != null) {
                setupCallbacks();
                mGravityScroller = new Scroller(mRecyclerView.getContext(),
                        new DecelerateInterpolator());
                snapToTargetExistingView();
            }
        }

    destoryCallback() 作用在于取消之前的RecyclerView的监听接口。

    /**
     * Called when the instance of a {@link RecyclerView} is detached.
     */
        private void destroyCallbacks() {
            mRecyclerView.removeOnScrollListener(mScrollListener);
            mRecyclerView.setOnFlingListener(null);
        }

    setupCallbacks() – 设置监听器

    /**
         * Called when an instance of a {@link RecyclerView} is attached.
         */
        private void setupCallbacks() throws IllegalStateException {
            if (mRecyclerView.getOnFlingListener() != null) {
                throw new IllegalStateException("An instance of OnFlingListener already set.");
            }
            mRecyclerView.addOnScrollListener(mScrollListener);
            mRecyclerView.setOnFlingListener(this);
        }

    此时可以看到,如果当前RecyclerView已经设置了OnFlingListener,会抛出一个 状态异常 

    snapToTargetExistingView()

    /**
     * 找到居中显示的view,计算它的位置,调用smoothScrollBy使其居中
     */
    void snapToTargetExistingView() {
            if (mRecyclerView == null) {
                return;
            }
            LayoutManager layoutManager = mRecyclerView.getLayoutManager();
            if (layoutManager == null) {
                return;
            }
            View snapView = findSnapView(layoutManager);
            if (snapView == null) {
                return;
            }
            // 计算目标View需要移动的距离
            int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
            if (snapDistance[0] != 0 || snapDistance[1] != 0) {
                mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
            }
        }

    该方法中显示调用 findSnapView() 找到目标View(需要居中显示的View),然后调用 calculateDistanceToFinalSnap() 来计算该目标View需要移动的距离。这两个方法均需要LinearSnapHelper重写。

    SnapHelper.Java 中有三个抽象函数需要LinearSnapHelper 重写。

    /**
     * 找到那个“snapView”
     */
    public abstract View findSnapView(LayoutManager layoutManager);
    /**
     * 计算targetView需要移动的距离
     * 该方法返回一个二维数组,分别表示X轴、Y轴方向上需要修正的偏移量
     */
    public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager,
                @NonNull View targetView);
    /**
     * 根据速度找到将要滑到的position
     */
    public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX,
                int velocityY);

    在setupCallbacks() 方法中可以看到对RecyclerView 设置了 OnScrollListener 和 OnFlingListener 两个监听器。

    查看SnapHelper可以发现

    // Handles the snap on scroll case.
        private final RecyclerView.OnScrollListener mScrollListener =
                new RecyclerView.OnScrollListener() {
                    boolean mScrolled = false;
    
                    @Override
                    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                        super.onScrollStateChanged(recyclerView, newState);
                        if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
                            mScrolled = false;
                            snapToTargetExistingView();
                        }
                    }
    
                    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                        if (dx != 0 || dy != 0) {
                            mScrolled = true;
                        }
                    }
                };
    
        @Override
        public boolean onFling(int velocityX, int velocityY) {
            LayoutManager layoutManager = mRecyclerView.getLayoutManager();
            if (layoutManager == null) {
                return false;
            }
            RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
            if (adapter == null) {
                return false;
            }
            int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
            return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
                    && snapFromFling(layoutManager, velocityX, velocityY);
        }

    当滚动结束是,会调用 snapToTargetExistingView() 方法。

    而当手指滑动触发onFling() 函数时,会根据X轴、Y轴方向上的速率加上 snapFromFling() 方法的返回值综合判断。

    看一下 snapFromFling()

    /**
         * Helper method to facilitate for snapping triggered by a fling.
         *
         * @param layoutManager The {@link LayoutManager} associated with the attached
         *                      {@link RecyclerView}.
         * @param velocityX     Fling velocity on the horizontal axis.
         * @param velocityY     Fling velocity on the vertical axis.
         *
         * @return true if it is handled, false otherwise.
         */
        private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
                int velocityY) {
            if (!(layoutManager instanceof ScrollVectorProvider)) {
                return false;
            }
    
            // 创建SmoothScroll对象
            RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
            if (smoothScroller == null) {
                return false;
            }
    
            int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
            if (targetPosition == RecyclerView.NO_POSITION) {
                return false;
            }
    
            smoothScroller.setTargetPosition(targetPosition);
            layoutManager.startSmoothScroll(smoothScroller);
            return true;
        }

    接下来看LinearSnapHelper.java 复写的三个方法

    @Override
        public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
                int velocityY) {
            if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
                return RecyclerView.NO_POSITION;
            }
    
            final int itemCount = layoutManager.getItemCount();
            if (itemCount == 0) {
                return RecyclerView.NO_POSITION;
            }
    
            // 重点在findSnapView()
    
            final View currentView = findSnapView(layoutManager);
            if (currentView == null) {
                return RecyclerView.NO_POSITION;
            }
    
            final int currentPosition = layoutManager.getPosition(currentView);
            if (currentPosition == RecyclerView.NO_POSITION) {
                return RecyclerView.NO_POSITION;
            }
    
            // ...省略若干代码
    
            return targetPos;
        }

    省略的若干代码主要是根据手势滑动的速率计算目标item的位置。具体算法不用多研究。

    可以看到方法内部又调用了 findSnapView() ;

    @Override
        public View findSnapView(RecyclerView.LayoutManager layoutManager) {
            if (layoutManager.canScrollVertically()) {
                return findCenterView(layoutManager, getVerticalHelper(layoutManager));
            } else if (layoutManager.canScrollHorizontally()) {
                return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
            }
            return null;
        }

    这里根据LayoutManager的方向做个判断,进而调用 findCenterView() 方法。

    /**
         * 返回距离父容器中间位置最近的子View
         */
        @Nullable
        private View findCenterView(RecyclerView.LayoutManager layoutManager,
                OrientationHelper helper) {
            int childCount = layoutManager.getChildCount();
            if (childCount == 0) {
                return null;
            }
    
            View closestChild = null;
            final int center; // 中间位值
            if (layoutManager.getClipToPadding()) {
                center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
            } else {
                center = helper.getEnd() / 2;
            }
            int absClosest = Integer.MAX_VALUE;
    
            for (int i = 0; i < childCount; i++) {  // 循环判断子View中间位值距离父容器中间位值的差值
                final View child = layoutManager.getChildAt(i);
                int childCenter = helper.getDecoratedStart(child) +
                        (helper.getDecoratedMeasurement(child) / 2);
                int 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; // 返回距离父容器中间位置最近的子View
        }

    然后来看 calculateDistanceToFinalSnap()

    @Override
        public int[] calculateDistanceToFinalSnap(
                @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
            int[] out = new int[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;
        }

    定义一个二维数组,根据LayoutManager的方向来判断进行赋值。

    private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
                @NonNull View targetView, OrientationHelper helper) {
            final int childCenter = helper.getDecoratedStart(targetView) +
                    (helper.getDecoratedMeasurement(targetView) / 2);
            final int containerCenter;
            if (layoutManager.getClipToPadding()) {
                containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
            } else {
                containerCenter = helper.getEnd() / 2;
            }
            return childCenter - containerCenter;
        }

    该方法的目的即是 计算目标View距离父容器中间位值的差值

    至此,流程已经分析完毕。

    总结如下:

    1. 有速率的滑动,会触发onScrollStateChanged() 和 onFling() 两个方法。

      • onScrollStateChanged() 方法内部调用 findSnapView() 找到对应的View,然后据此View在调用calculateDistanceToFinalSnap() 来计算该目标View需要移动的距离,最后通过RecyclerView.smoothScrollBy() 来移动View。

      • onFling() 方法内部调用 snapFromFling(), 然后在此方法内部首先创建了一个SmoothScroller 对象。接着调用 findTargetSnapPosition() 找到目标View的position,然后对smoothScroller设置该position,最后通过LayoutManager.startSmoothScroll() 开始移动View。

    2. 没有速率的滚动只会触发 onScrollStateChanged() 函数。

    扩展

    LinearSnapHelper 类的目的是将某个View停留在正中间,我们也可以通过这种方式来实现每次滑动结束之后将某个View停留在最左边或者最右边。

    其实通过上面的分析,就会发现最主要的就是 calculateDistanceToFinalSnap 和 findSnapView 这两个函数。

    在寻找目标View的时候,不像findCenterView那么简单。 
    以为需要考虑到最后item的边界情况。判断的不好就会出现,无论怎么滑动都会出现最后一个item无法完整显示的bug。

    且看我的代码:

    /**
         * 注意判断最后一个item时,应通过判断距离右侧的位置
         *
         * @param layoutManager
         * @param helper
         * @return
         */
        private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
            if (!(layoutManager instanceof LinearLayoutManager)) { // only for LinearLayoutManager
                return null;
            }
            int childCount = layoutManager.getChildCount();
            if (childCount == 0) {
                return null;
            }
    
            View closestChild = null;
            final int start = helper.getStartAfterPadding();
    
            int absClosest = Integer.MAX_VALUE;
            for (int i = 0; i < childCount; i++) {
                final View child = layoutManager.getChildAt(i);
                int childStart = helper.getDecoratedStart(child);
                int absDistance = Math.abs(childStart - start);
    
                if (absDistance < absClosest) {
                    absClosest = absDistance;
                    closestChild = child;
                }
            }
    
            // 边界情况判断
            View firstVisibleChild = layoutManager.getChildAt(0);
    
            if (firstVisibleChild != closestChild) {
                return closestChild;
            }
    
            int firstChildStart = helper.getDecoratedStart(firstVisibleChild);
    
            int lastChildPos = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
            View lastChild = layoutManager.getChildAt(childCount - 1);
            int lastChildCenter = helper.getDecoratedStart(lastChild) + (helper.getDecoratedMeasurement(lastChild) / 2);
            boolean isEndItem = lastChildPos == layoutManager.getItemCount() - 1;
            if (isEndItem && firstChildStart < 0 && lastChildCenter < helper.getEnd()) {
                return lastChild;
            }
    
            return closestChild;
        }

    对于“反向的”同样要考虑边界情况。

    private View findEndView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
            if (!(layoutManager instanceof LinearLayoutManager)) { // only for LinearLayoutManager
                return null;
            }
            int childCount = layoutManager.getChildCount();
            if (childCount == 0) {
                return null;
            }
    
            if (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition() == 0) {
                return null;
            }
    
            View closestChild = null;
            final int end = helper.getEndAfterPadding();
    
            int absClosest = Integer.MAX_VALUE;
            for (int i = 0; i < childCount; i++) {
                final View child = layoutManager.getChildAt(i);
                int childStart = helper.getDecoratedEnd(child);
                int absDistance = Math.abs(childStart - end);
    
                if (absDistance < absClosest) {
                    absClosest = absDistance;
                    closestChild = child;
                }
            }
    
            // 边界情况判断
            View lastVisibleChild = layoutManager.getChildAt(childCount - 1);
    
            if (lastVisibleChild != closestChild) {
                return closestChild;
            }
    
            if (layoutManager.getPosition(closestChild) == ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition()) {
                return closestChild;
            }
    
            View firstChild = layoutManager.getChildAt(0);
            int firstChildStart = helper.getDecoratedStart(firstChild);
    
            int firstChildPos = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
            boolean isFirstItem = firstChildPos == 0;
    
    
            int firstChildCenter = helper.getDecoratedStart(firstChild) + (helper.getDecoratedMeasurement(firstChild) / 2);
            if (isFirstItem && firstChildStart < 0 && firstChildCenter > helper.getStartAfterPadding()) {
                return firstChild;
            }
    
            return closestChild;
        }

    果图如下:

    这里写图片描述

    这里写图片描述


    完整代码,请移步:JackSnapHelper.java

  • 相关阅读:
    原生js可爱糖果数字时间特效
    jQuery绑定事件的四种方式
    jQuery选择器总结
    正则表达式
    this对象
    网页瀑布流效果实现的几种方式
    关于DOM
    SparkSQL读写外部数据源--数据分区
    SparkSQL读写外部数据源-通过jdbc读写mysql数据库
    SparkSQL读写外部数据源-基本操作load和save
  • 原文地址:https://www.cnblogs.com/zhujiabin/p/6117482.html
Copyright © 2011-2022 走看看