Android SnapHelper扒皮分析

你们好,本人是一个萌新android开发,最近对与RecyclerView搭配使用的SnapHelper很是感兴趣,本篇文章是记录了一些我对SnapHelper的研究与体会。android

###SnapHelper简介 SnapHelper是什么,能作什么是我最早感兴趣的东西,从官方文档看来SnapHelper是一个辅助RecyclerView滚动的辅助类,RecyclerView自己是一个滚动容器支持横向竖向多视图布局滚动,SnapHelper则能够辅助RecyclerView滚动结束时对其指定位置,例如ViewPager的效果,以及Google Play的效果。大白话就是在RecyclerView中止滚动时,经过SnapHelper辅助让其继续滚动到指定位置。数组

###开始解析SnapHelper SnapHelper自己是一个抽象类,Google官方给了两个实现类, LinearSnapHelper以及PagerSnapHelper,前者的效果是在RecyclerView滚动中止时对齐中间,其效果相似ViewPager可是一次能够滚动多页,另外一个PagerSnapHelper的话则是一次只能滚动一页。OK,咱们明白了效果就带着疑问来看源码吧!ide

1.怎么样在中止滚动后对齐指定位置 2.LinearSnapHelperPagerSnapHelper的区别 3.怎么自定义一个SnapHelper设置为咱们想要的指定位置工具

咱们先从入口开始布局

public void attachToRecyclerView(@Nullable RecyclerView recyclerView) throws IllegalStateException {
    if (this.mRecyclerView != recyclerView) {
        if (this.mRecyclerView != null) {
            this.destroyCallbacks();
        }

        this.mRecyclerView = recyclerView;
        if (this.mRecyclerView != null) {
            this.setupCallbacks();
            this.mGravityScroller = new Scroller(this.mRecyclerView.getContext(), new DecelerateInterpolator());
            this.snapToTargetExistingView();
        }

    }
}
复制代码

能够看到传入的RecyclerView会先判断是否不等于上一次传入的RecyclerView。若是不相等的话会先调用this.destroyCallbacks();而后从新绑定新传入RecyclerView,依次调用了this

this.setupCallbacks();
  Scroller scroller = new Scroller(this.mRecyclerView.getContext(), new DecelerateInterpolator());
  this.snapToTargetExistingView();
复制代码

####destroyCallbacksspa

this.mRecyclerView.removeOnScrollListener(this.mScrollListener);
    this.mRecyclerView.setOnFlingListener((RecyclerView.OnFlingListener) null);
复制代码

这个方法就是解除了RecyclerView的各类绑定,其中RecyclerView.OnFlingListener看的比较陌生,通过查阅知道这个回调是在Fling事件后回掉,所谓的Fling事件我认为就是手指离开屏幕可是RecyclerView不是会当即中止,而是会根据惯性继续滚动一段距离,直到最后中止,从手指离开到最后中止的这一个完整的过程。3d

###setupCallbackscode

this.mRecyclerView.addOnScrollListener(this.mScrollListener);
        this.mRecyclerView.setOnFlingListener(this);
复制代码

这个方法很简单,绑定了事件cdn

###new Scroller

Scroller scroller = new Scroller(this.mRecyclerView.getContext(), new DecelerateInterpolator());
复制代码

能够看到是初始化了一个Scroller具体做用么,咱们如今还不知道,留着慢慢分析。

###snapToTargetExistingView

void snapToTargetExistingView() {
    if (this.mRecyclerView != null) {
        LayoutManager layoutManager = this.mRecyclerView.getLayoutManager();
        if (layoutManager != null) {
            View snapView = this.findSnapView(layoutManager);
            if (snapView != null) {
                int[] snapDistance = this.calculateDistanceToFinalSnap(layoutManager, snapView);
                if (snapDistance[0] != 0 || snapDistance[1] != 0) {
                    this.mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
                }

            }
        }
    }
}
复制代码

这个方法里面的内容就比较多了,可是我看到了smoothScrollBy说明在最初绑定的时候其实就调用过对齐方法。SnapHelper自己是一个抽象类,里面的抽象方法分别是

@Nullable
public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager var1, @NonNull View var2);

@Nullable
public abstract View findSnapView(LayoutManager var1);

public abstract int findTargetSnapPosition(LayoutManager var1, int var2, int var3);
复制代码

snapToTargetExistingView中调用了findSnapViewcalculateDistanceToFinalSnap咱们来分析子类LinearSnapHelper中的实现方法

####findSnapView

public View findSnapView(LayoutManager layoutManager) {
    if (layoutManager.canScrollVertically()) {
        return this.findCenterView(layoutManager, this.getVerticalHelper(layoutManager));
    } else {
        return layoutManager.canScrollHorizontally() ? this.findCenterView(layoutManager, this.getHorizontalHelper(layoutManager)) : null;
    }
}
复制代码

由于RecyclerView自己支持横向和竖向的滚动,因此有一个判断方法,可是能够看到不论是哪一个方向,最后调用的都为findCenterView方法

####findCenterView

private View findCenterView(LayoutManager layoutManager, android.support.v7.widget.OrientationHelper helper) {
    //当前屏幕上子View的数量
    int childCount = layoutManager.getChildCount();
    if (childCount == 0) {
        return null;
    } else {
        View closestChild = null;
        int center;
        //RecyclerView的clipToPadding是否为true
        if (layoutManager.getClipToPadding()) {
            //RecyclerView的paddingLeft+RecyclerView除去padding的实际宽度 / 2
            center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            //RecyclerView的宽度 / 2
            center = helper.getEnd() / 2;
        }

        int absClosest = Integer.MAX_VALUE;
        for(int i = 0; i < childCount; ++i) {
            View child = layoutManager.getChildAt(i);
            //子view的中心位置
            int childCenter = helper.getDecoratedStart(child) + helper.getDecoratedMeasurement(child) / 2;
            int absDistance = Math.abs(childCenter - center);
            if (absDistance < absClosest) {
                absClosest = absDistance;
                closestChild = child;
            }
        }

        return closestChild;
    }
}
复制代码

大部分代码都加上注释了,OrientationHelper是封装好的一个测量位置的工具类,感兴趣的同窗能够自行看源码由于不涉及逻辑,咱们这里就不分析了,继续看findCenterView方法,先算出了RecyclerView的中心位置,而后一个循环算出最接近中心位置的View并返回,画了个图应改是比较清楚的了。

####calculateDistanceToFinalSnap 分析这个方法前咱们应该还记得在snapToTargetExistingView中是怎么调用方法的吧,

View snapView = this.findSnapView(layoutManager);
            if (snapView != null) {
                int[] snapDistance = this.calculateDistanceToFinalSnap(layoutManager, snapView);
                if (snapDistance[0] != 0 || snapDistance[1] != 0) {
                    this.mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
                }

            }
复制代码

已经知道了findSnapView方法的含义,再来看这个逻辑已经清晰了不少,以LinearSnapHelper为例子,首先找到了离中心最近的View而后调用calculateDistanceToFinalSnap返回了一个长度为2的数组,结合下面的smoothScrollBy咱们就已经能分析出来这个数组包含的确定是一个横向x距离一个竖向的y距离,咱们来看下具体的实现逻辑

public int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager, @NonNull View targetView) {
    int[] out = new int[2];
    if (layoutManager.canScrollHorizontally()) {
        out[0] = this.distanceToCenter(layoutManager, targetView, this.getHorizontalHelper(layoutManager));
    } else {
        out[0] = 0;
    }

    if (layoutManager.canScrollVertically()) {
        out[1] = this.distanceToCenter(layoutManager, targetView, this.getVerticalHelper(layoutManager));
    } else {
        out[1] = 0;
    }

    return out;
}
复制代码

果真是这样的,若是能够横向滚动则计算横向的距离,竖向的也同样,咱们再看看distanceToCenter方法

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

    return childCenter - containerCenter;
}
复制代码

哈哈,出乎意外的简单嘛,咱们用以前算出的中心View的中心距离减去整个RecycleView的中心距离并返回

###阶段总结,回答问题一 至此咱们已经分析了snapToTargetExistingView方法的完整流程,能够小小的总结一下 findSnapView是用来找到须要对齐的item,calculateDistanceToFinalSnap则是用来计算滚动到对齐位置须要的具体偏移量,那么问题一的答案也是很明显了,就是在中止滚动后调用了,snapToTargetExistingView,上代码!

private final OnScrollListener mScrollListener = new OnScrollListener() {
    boolean mScrolled = false;

    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        if (newState == 0 && this.mScrolled) {
            this.mScrolled = false;
            SnapHelper.this.snapToTargetExistingView();
        }

    }

    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        if (dx != 0 || dy != 0) {
            this.mScrolled = true;
        }

    }
};
复制代码

不出所料~

###问题二,LinearSnapHelperPagerSnapHelper的区别

LinearSnapHelperPagerSnapHelper的区别其实就在于前者能够一次滚动多个item,咱们前面也提过Fling事件,因此具体的区别确定是在各自处理Fling的不一样啦~开始撸代码,首先仍是要看下SnapHelper

public boolean onFling(int velocityX, int velocityY) {
    LayoutManager layoutManager = this.mRecyclerView.getLayoutManager();
    if (layoutManager == null) {
        return false;
    } else {
        Adapter adapter = this.mRecyclerView.getAdapter();
        if (adapter == null) {
            return false;
        } else {
        //最小响应Fling的速率
            int minFlingVelocity = this.mRecyclerView.getMinFlingVelocity();
            return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity) && this.snapFromFling(layoutManager, velocityX, velocityY);
        }
    }
}
复制代码

上面的代码很简单,就是判断下是否响应,重点在snapFromFling

###snapFromFling

private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX, int velocityY) {
    if (!(layoutManager instanceof ScrollVectorProvider)) {
        return false;
    } else {
        SmoothScroller smoothScroller = this.createScroller(layoutManager);
        if (smoothScroller == null) {
            return false;
        } else {
            int targetPosition = this.findTargetSnapPosition(layoutManager, velocityX, velocityY);
            if (targetPosition == -1) {
                return false;
            } else {
                smoothScroller.setTargetPosition(targetPosition);
                layoutManager.startSmoothScroll(smoothScroller);
                return true;
            }
        }
    }
}
复制代码

首先判断了layoutManager是否实现了ScrollVectorProvider接口,这个接口只有一个实现方法是用来判断布局方向的,系统提供的layoutManager都是实现了该接口无需咱们操心,后面有一个createScroller咱们看下代码

@Nullable
protected LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
    return !(layoutManager instanceof ScrollVectorProvider) ? null : new LinearSmoothScroller(this.mRecyclerView.getContext()) {
        protected void onTargetFound(View targetView, RecyclerView.State state, RecyclerView.SmoothScroller.Action action) {
            if (SnapHelper.this.mRecyclerView != null) {
                //算出对齐位置的偏移量
                int[] snapDistances = SnapHelper.this.calculateDistanceToFinalSnap(SnapHelper.this.mRecyclerView.getLayoutManager(), targetView);
                int dx = snapDistances[0];
                int dy = snapDistances[1];
                //计算减速滚动的时间
                int time = this.calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
                if (time > 0) {
                    //改变滚动速率
                    action.update(dx, dy, time, this.mDecelerateInterpolator);
                }

            }
        }

        //1dp滚动须要的时间
        protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
            return 100.0F / (float) displayMetrics.densityDpi;
        }
    };
}
复制代码

加上了一些注释,这里就再也不过多分析其原理了,咱们重点放在findTargetSnapPosition看看LinearSnapHelper是怎么实现的

public int findTargetSnapPosition(LayoutManager layoutManager, int velocityX, int velocityY) {
    //判断LayoutManager是否实现ScrollVectorProvider接口
    if (!(layoutManager instanceof ScrollVectorProvider)) {
        return -1;
    } else {
        int itemCount = layoutManager.getItemCount();
        if (itemCount == 0) {
            return -1;
        } else {
            //获取中心的View
            View currentView = this.findSnapView(layoutManager);
            if (currentView == null) {
                return -1;
            } else {
                int currentPosition = layoutManager.getPosition(currentView);
                if (currentPosition == -1) {
                    return -1;
                } else {
                    ScrollVectorProvider vectorProvider = (ScrollVectorProvider) layoutManager;
                    //用来判断布局方向
                    PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
                    if (vectorForEnd == null) {
                        return -1;
                    } else {
                        int hDeltaJump;
                        //若是能够横向滚动
                        if (layoutManager.canScrollHorizontally()) {
                            hDeltaJump = this.estimateNextPositionDiffForFling(layoutManager, this.getHorizontalHelper(layoutManager), velocityX, 0);
                            //若是是方向布局则值取反
                            if (vectorForEnd.x < 0.0F) {
                                hDeltaJump = -hDeltaJump;
                            }
                        } else {
                            hDeltaJump = 0;
                        }

                        int vDeltaJump;
                        //若是能够竖向滚动
                        if (layoutManager.canScrollVertically()) {
                            vDeltaJump = this.estimateNextPositionDiffForFling(layoutManager, this.getVerticalHelper(layoutManager), 0, velocityY);
                            //若是是方向布局则值取反
                            if (vectorForEnd.y < 0.0F) {
                                vDeltaJump = -vDeltaJump;
                            }
                        } else {
                            vDeltaJump = 0;
                        }
                        //fling了多少item
                        int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
                        if (deltaJump == 0) {
                            return -1;
                        } else {
                            //加上最开始算到的中心view的position,获得的就是咱们要滚动到的position
                            int targetPos = currentPosition + deltaJump;
                            if (targetPos < 0) {
                                targetPos = 0;
                            }

                            if (targetPos >= itemCount) {
                                targetPos = itemCount - 1;
                            }

                            return targetPos;
                        }
                    }
                }
            }
        }
    }
}
复制代码

代码有些长,咱们逐步分析,前面都是一些判断与取值已经加上了注释,咱们来看看是怎么算出一次fling事件滚动多少item的,也就是estimateNextPositionDiffForFling方法

private int estimateNextPositionDiffForFling(LayoutManager layoutManager, OrientationHelper helper, int velocityX, int velocityY) {
    int[] distances = this.calculateScrollDistance(velocityX, velocityY);
    float distancePerChild = this.computeDistancePerChild(layoutManager, helper);
    if (distancePerChild <= 0.0F) {
        return 0;
    } else {
        int distance = Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
        return Math.round((float)distance / distancePerChild);
    }
}
复制代码

推了推个人黑框眼镜,亦可赛艇,继续分析calculateScrollDistancecomputeDistancePerChild

public int[] calculateScrollDistance(int velocityX, int velocityY) {
    int[] outDist = new int[2];
    this.mGravityScroller.fling(0, 0, velocityX, velocityY, -2147483648, 2147483647, -2147483648, 2147483647);
    outDist[0] = this.mGravityScroller.getFinalX();
    outDist[1] = this.mGravityScroller.getFinalY();
    return outDist;
}
复制代码

还记得咱们最开始初始化了一个Scroller么,原来是在这里用上了,传入咱们的速率以后调用Scroller.getFinal方法就能获得最终的滚动距离,也就是说calculateScrollDistance方法返回的是滚动总距离,那么computeDistancePerChild

private float computeDistancePerChild(LayoutManager layoutManager, OrientationHelper helper) {
    View minPosView = null;
    View maxPosView = null;
    int minPos = Integer.MAX_VALUE;
    int maxPos = Integer.MIN_VALUE;
    int childCount = layoutManager.getChildCount();
    if (childCount == 0) {
        return 1.0F;
    } else {
        int start;
        int pos;
        for (start = 0; start < childCount; ++start) {
            View child = layoutManager.getChildAt(start);
            pos = layoutManager.getPosition(child);
            if (pos != -1) {
                //筛选到position最小的View
                if (pos < minPos) {
                    minPos = pos;
                    minPosView = child;
                }
                //筛选到position最大的View
                if (pos > maxPos) {
                    maxPos = pos;
                    maxPosView = child;
                }
            }
        }

        if (minPosView != null && maxPosView != null) {
            //比对position最小的View和position最大的View的left
            start = Math.min(helper.getDecoratedStart(minPosView), helper.getDecoratedStart(maxPosView));
            //比对position最小的View和position最大的View的right
            int end = Math.max(helper.getDecoratedEnd(minPosView), helper.getDecoratedEnd(maxPosView));
            //总距离
            pos = end - start;
            if (pos == 0) {
                return 1.0F;
            } else {
                //总距离除总数获得的固然就是平均距离啦~
                return 1.0F * (float) pos / (float) (maxPos - minPos + 1);
            }
        } else {
            return 1.0F;
        }
    }
}
复制代码

这里理解起来仍是比较简单的,这个方法就是返回了平均一个item的平均长度,那么咱们回头看estimateNextPositionDiffForFling也就很是好理解了

int distance = Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
        return Math.round((float)distance / distancePerChild);
复制代码

总距离除平均距离的获得的固然就是平均数量啦。

至此 LinearSnapHelper就分析完毕,相比起来 PagerSnapHelper就很简单啦,这里简单提下,在 PagerSnapHelper中是先获取中心View而后根据滚动方向,中心View的position加一或者减一,若是有这方面的问题的话欢迎私信本人~

###总结一下流程 RecyclerView中止滚动的时候调用snapToTargetExistingView方法,先获取须要对齐的ViewfindSnapView再根据对齐View获取须要滚动的距离calculateDistanceToFinalSnaponFling事件中判断当前的fling是否达到滚动的最小速率,而后调用snapFromFling在其中的findTargetSnapPosition方法得到fling后滚动到的position调用smoothScroller.setTargetPosition(targetPosition)进行滚动。

###问题三,自定义SnapHelper 按照国际惯例,自定义一个上对齐的好啦~

public class TopSnapHelper extends SnapHelper {

private OrientationHelper mVerticalHelper;

@Nullable
@Override
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View view) {
    int[] out = new int[2];
    out[0] = 0;
    if (layoutManager.canScrollVertically()) {
        out[1] = getVerticalHelper(layoutManager).getDecoratedStart(view);
    } else {
        out[1] = 0;
    }
    return out;
}

@Nullable
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
    if (layoutManager.canScrollVertically()) {
        return findTopView(layoutManager, getVerticalHelper(layoutManager));
    }
    return null;
}

@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
    int itemCount = layoutManager.getItemCount();
    if (itemCount == 0) {
        return -1;
    } else {
        View mStartMostChildView = null;
        if (layoutManager.canScrollVertically()) {
            mStartMostChildView = this.findStartView(layoutManager, this.getVerticalHelper(layoutManager));
        }

        if (mStartMostChildView == null) {
            return -1;
        } else {
            int centerPosition = layoutManager.getPosition(mStartMostChildView);
            if (centerPosition == -1) {
                return -1;
            } else {
                boolean forwardDirection;
                if (layoutManager.canScrollHorizontally()) {
                    forwardDirection = velocityX > 0;
                } else {
                    forwardDirection = velocityY > 0;
                }

                return (forwardDirection ? centerPosition + 1 : centerPosition);
            }
        }
    }
}

private View findTopView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
    int childCount = layoutManager.getChildCount();
    if (childCount == 0) {
        return null;
    } else {
        LinearLayoutManager manager = (LinearLayoutManager) layoutManager;
        int firstPosition = manager.findFirstVisibleItemPosition();
        View firstView = manager.findViewByPosition(firstPosition);
        if (firstView == null) return null;
        int lastPosition = manager.findLastCompletelyVisibleItemPosition();
        //滚动到最后不用对齐
        if (lastPosition == manager.getItemCount()) return null;
        int start = Math.abs(helper.getDecoratedStart(firstView));
        if (start >= helper.getDecoratedMeasurement(firstView) / 2) {
            return manager.findViewByPosition(firstPosition + 1);
        }
        return firstView;
    }
}

private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
    int childCount = layoutManager.getChildCount();
    if (childCount == 0) {
        return null;
    } else {
        View closestChild = null;
        int startest = Integer.MAX_VALUE;
        for (int i = 0; i < childCount; ++i) {
            View child = layoutManager.getChildAt(i);
            int childStart = helper.getDecoratedStart(child);
            if (childStart < startest) {
                startest = childStart;
                closestChild = child;
            }
        }

        return closestChild;
    }
}


@NonNull
private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
    if (this.mVerticalHelper == null || this.mVerticalHelper.mLayoutManager != layoutManager) {
        this.mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
    }

    return this.mVerticalHelper;
}}
复制代码

Alt text

###感谢 本文参考了让你明明白白的使用RecyclerView——SnapHelper详解 因为本人是一个新手android开发因此写的东西不太比如较啰嗦,但愿能够对你们的开发起到必定的帮助。

相关文章
相关标签/搜索