美文网首页各种viewAndroid开发日记
Android SnapHelper扒皮分析

Android SnapHelper扒皮分析

作者: 苏武难飞 | 来源:发表于2018-08-11 23:42 被阅读79次

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

SnapHelper简介

SnapHelper是什么,能做什么是我最先感兴趣的东西,从官方文档看来SnapHelper是一个辅助RecyclerView滚动的辅助类,RecyclerView本身是一个滚动容器支持横向竖向多视图布局滚动,SnapHelper则可以辅助RecyclerView滚动结束时对其指定位置,例如ViewPager的效果,以及Google Play的效果。大白话就是在RecyclerView停止滚动时,通过SnapHelper辅助让其继续滚动到指定位置。


IMG_0403.jpg

开始解析SnapHelper

SnapHelper本身是一个抽象类,Google官方给了两个实现类, LinearSnapHelper以及PagerSnapHelper,前者的效果是在RecyclerView滚动停止时对齐中间,其效果类似ViewPager但是一次可以滚动多页,另一个PagerSnapHelper的话则是一次只能滚动一页。OK,我们明白了效果就带着疑问来看源码吧!

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.setupCallbacks();
  Scroller scroller = new Scroller(this.mRecyclerView.getContext(), new DecelerateInterpolator());
  this.snapToTargetExistingView();

destroyCallbacks

   this.mRecyclerView.removeOnScrollListener(this.mScrollListener);
    this.mRecyclerView.setOnFlingListener((RecyclerView.OnFlingListener) null);

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

setupCallbacks

  this.mRecyclerView.addOnScrollListener(this.mScrollListener);
        this.mRecyclerView.setOnFlingListener(this);

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

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并返回,画了个图应改是比较清楚的了。

未命名文件.png

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的中心距离并返回


图2

阶段总结,回答问题一

至此我们已经分析了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);

总距离除平均距离的得到的当然就是平均数量啦。


图3

至此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;
}}
图4

感谢

本文参考了让你明明白白的使用RecyclerView——SnapHelper详解
由于本人是一个新手android开发所以写的东西不太好比较啰嗦,希望可以对大家的开发起到一定的帮助。

相关文章

网友评论

    本文标题:Android SnapHelper扒皮分析

    本文链接:https://www.haomeiwen.com/subject/zyujbftx.html