• SnapHelper解析


    简介

    SnapHelper是RecyclerView的辅助类,可用来控制在滑动结束后,RecyclerView中item的对齐方式。

    SnapHelper是一个抽象类,系统内置了两个默认实现类

    LinearSnapHelper:使当前Item居中显示,常用场景是横向的RecyclerView, 类似ViewPager效果,但是又可以快速滑动(滑动多页)

    PagerSnapHelper:PagerSnapHelper的展示效果和LineSnapHelper是一样的,只是PagerSnapHelper 限制一次只能滑动一页,不能快速滑动。

    滑动基础

    RecyclerView的滚动分为滚动状态和Fling这两类,主要应对的是OnScrollListener和OnFlingListener这两个回调接⼝;

    对应

    1. private void setupCallbacks() throws IllegalStateException {
    2. if (mRecyclerView.getOnFlingListener() != null) {
    3. throw new IllegalStateException("An instance of OnFlingListener already set.");
    4. }
    5. mRecyclerView.addOnScrollListener(mScrollListener);
    6. mRecyclerView.setOnFlingListener(this);
    7. }
    8. private void destroyCallbacks() {
    9. mRecyclerView.removeOnScrollListener(mScrollListener);
    10. mRecyclerView.setOnFlingListener(null);
    11. }

    滚动状态监听

    SCROLL_STATE_IDLE 滚动闲置状态,此时并没有手指滑动或动画执行

    SCROLL_STATE_DRAGGING 滚动拖拽状态,由于用户触摸屏幕产生

    SCROLL_STATE_SETTLING 自动滚动状态,此时手没有触摸,一般是由动画执行滚动到最终位置、包括smoothScrollTo等方法的调用

    Fling操作

    手指在屏幕上滑动RecyclerView然后松手,RecyclerView中的内容会顺着惯性继续往手指滑动的方向继续滚动直到停止,这个过程叫做Fling。Fling操作从手指离开屏幕瞬间被触发,在滚动停止时结束。 

    过程

    调用attachToRecyclerView绑定到RecyclerView时来完成对齐TargetView。

    当Scroll被触发时和Fling操作的末尾阶段时对齐TargetView。

    在attachToRecyclerView和onScrollStateChanged中都调用了snapToTargetExistingView这个方法。

    attachToRecyclerView绑定

    1. public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
    2. throws IllegalStateException {
    3. //如果SnapHelper之前已经附着到此RecyclerView上,不用进行任何操作
    4. if (mRecyclerView == recyclerView) {
    5. return;
    6. }
    7. //如果SnapHelper之前附着的RecyclerView和现在的不一致,清理掉之前RecyclerView的回调
    8. if (mRecyclerView != null) {
    9. destroyCallbacks();
    10. }
    11. //更新RecyclerView对象引用
    12. mRecyclerView = recyclerView;
    13. if (mRecyclerView != null) {
    14. //设置当前RecyclerView对象的回调
    15. setupCallbacks();
    16. //创建一个Scroller对象,用于辅助计算fling的总距离,后面会涉及到
    17. mGravityScroller = new Scroller(mRecyclerView.getContext(),
    18. new DecelerateInterpolator());
    19. //调用snapToTargetExistingView()方法以实现对SnapView的对齐滚动处理
    20. snapToTargetExistingView();
    21. }
    22. }

    snapToTargetExistingView对targetView进行滚动调整

    1. void snapToTargetExistingView() {
    2. if (mRecyclerView == null) {
    3. return;
    4. }
    5. LayoutManager layoutManager = mRecyclerView.getLayoutManager();
    6. if (layoutManager == null) {
    7. return;
    8. }
    9. //找出SnapView
    10. View snapView = findSnapView(layoutManager);
    11. if (snapView == null) {
    12. return;
    13. }
    14. //计算出SnapView需要滚动的距离
    15. int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
    16. //如果需要滚动的距离不是为0,就调用smoothScrollBy()使RecyclerView滚动相应的距离
    17. if (snapDistance[0] != 0 || snapDistance[1] != 0) {
    18. mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
    19. }
    20. }

    Filing 操作 该方法会在RecyclerView开始做fling操作时被调用

    1. @Override
    2. public boolean onFling(int velocityX, int velocityY) {
    3. LayoutManager layoutManager = mRecyclerView.getLayoutManager();
    4. if (layoutManager == null) {
    5. return false;
    6. }
    7. RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
    8. if (adapter == null) {
    9. return false;
    10. }
    11. //获取RecyclerView要进行fling操作需要的最小速率,
    12. //只有超过该速率,ItemView才会有足够的动力在手指离开屏幕时继续滚动下去
    13. int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
    14. //这里会调用snapFromFling()这个方法,就是通过该方法实现平滑滚动并使得在滚动停止时itemView对齐到目的坐标位置
    15. return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
    16. && snapFromFling(layoutManager, velocityX, velocityY);
    17. }
    18. private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
    19. int velocityY) {
    20. //layoutManager必须实现ScrollVectorProvider接口才能继续往下操作
    21. if (!(layoutManager instanceof ScrollVectorProvider)) {
    22. return false;
    23. }
    24. //创建SmoothScroller对象,这个东西是一个平滑滚动器,用于对ItemView进行平滑滚动操作
    25. RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
    26. if (smoothScroller == null) {
    27. return false;
    28. }
    29. //通过findTargetSnapPosition()方法,以layoutManager和速率作为参数,找到targetSnapPosition
    30. int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
    31. if (targetPosition == RecyclerView.NO_POSITION) {
    32. return false;
    33. }
    34. //通过setTargetPosition()方法设置滚动器的滚动目标位置
    35. smoothScroller.setTargetPosition(targetPosition);
    36. //利用layoutManager启动平滑滚动器,开始滚动到目标位置
    37. layoutManager.startSmoothScroll(smoothScroller);
    38. return true;
    39. }

    LinearSnapHelper如何实现居中对齐的

    主要是实现了上面提到的三个抽象方法,findTargetSnapPosition、calculateDistanceToFinalSnap和findSnapView。

    calculateDistanceToFinalSnap

    该方法计算最终对齐要移动的距离,返回一个长度为2的int 数组out,out[0] 为 x 方向移动的距离,out[1] 为 y 方向移动的距离。

    1. @Override
    2. public int[] calculateDistanceToFinalSnap(
    3. @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
    4. int[] out = new int[2];
    5. // 如果是水平方向滚动的,则计算水平方向需要移动的距离,否则水平方向的移动距离为0
    6. if (layoutManager.canScrollHorizontally()) {
    7. out[0] = distanceToCenter(layoutManager, targetView,
    8. getHorizontalHelper(layoutManager));
    9. } else {
    10. out[0] = 0;
    11. }
    12. // 如果是竖直方向滚动的,则计算竖直方向需要移动的距离,否则竖直方向的移动距离为0
    13. if (layoutManager.canScrollVertically()) {
    14. out[1] = distanceToCenter(layoutManager, targetView,
    15. getVerticalHelper(layoutManager));
    16. } else {
    17. out[1] = 0;
    18. }
    19. return out;
    20. }
    21. // 计算水平或者竖直方向需要移动的距离
    22. private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
    23. @NonNull View targetView, OrientationHelper helper) {
    24. final int childCenter = helper.getDecoratedStart(targetView) +
    25. (helper.getDecoratedMeasurement(targetView) / 2);
    26. final int containerCenter;
    27. if (layoutManager.getClipToPadding()) {
    28. containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
    29. } else {
    30. containerCenter = helper.getEnd() / 2;
    31. }
    32. return childCenter - containerCenter;
    33. }

    findSnapView: 找到要对齐的目标View, 最终的逻辑在findCenterView 方法里

    1. // 规则是:循环LayoutManager的所有子元素,计算每个 childView的
    2. //中点距离Parent 的中点,找到距离最近的一个,就是需要居中对齐的目标View
    3. @Override
    4. public View findSnapView(RecyclerView.LayoutManager layoutManager) {
    5. if (layoutManager.canScrollVertically()) {
    6. return findCenterView(layoutManager, getVerticalHelper(layoutManager));
    7. } else if (layoutManager.canScrollHorizontally()) {
    8. return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
    9. }
    10. return null;
    11. }

    findTargetSnapPosition : 在触发fling时找到需要对齐的目标View的的Position 即targetSnapPosition。

    1. @Override
    2. public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
    3. int velocityY) {
    4. //判断layoutManager是否实现了RecyclerView.SmoothScroller.ScrollVectorProvider这个接口
    5. if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
    6. return RecyclerView.NO_POSITION;
    7. }
    8. final int itemCount = layoutManager.getItemCount();
    9. if (itemCount == 0) {
    10. return RecyclerView.NO_POSITION;
    11. }
    12. //找到snapView
    13. final View currentView = findSnapView(layoutManager);
    14. if (currentView == null) {
    15. return RecyclerView.NO_POSITION;
    16. }
    17. final int currentPosition = layoutManager.getPosition(currentView);
    18. if (currentPosition == RecyclerView.NO_POSITION) {
    19. return RecyclerView.NO_POSITION;
    20. }
    21. RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
    22. (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
    23. // 通过ScrollVectorProvider接口中的computeScrollVectorForPosition()方法
    24. // 来确定layoutManager的布局方向
    25. PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
    26. if (vectorForEnd == null) {
    27. return RecyclerView.NO_POSITION;
    28. }
    29. int vDeltaJump, hDeltaJump;
    30. if (layoutManager.canScrollHorizontally()) {
    31. //layoutManager是横向布局,并且内容超出一屏,canScrollHorizontally()才返回true
    32. //估算fling结束时相对于当前snapView位置的横向位置偏移量
    33. hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
    34. getHorizontalHelper(layoutManager), velocityX, 0);
    35. //vectorForEnd.x < 0代表layoutManager是反向布局的,就把偏移量取反
    36. if (vectorForEnd.x < 0) {
    37. hDeltaJump = -hDeltaJump;
    38. }
    39. } else {
    40. //不能横向滚动,横向位置偏移量当然就为0
    41. hDeltaJump = 0;
    42. }
    43. //竖向的原理同上
    44. if (layoutManager.canScrollVertically()) {
    45. vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
    46. getVerticalHelper(layoutManager), 0, velocityY);
    47. if (vectorForEnd.y < 0) {
    48. vDeltaJump = -vDeltaJump;
    49. }
    50. } else {
    51. vDeltaJump = 0;
    52. }
    53. //根据layoutManager的横竖向布局方式,最终横向位置偏移量和竖向位置偏移量二选一,作为fling的位置偏移量
    54. int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
    55. if (deltaJump == 0) {
    56. return RecyclerView.NO_POSITION;
    57. }
    58. //当前位置加上偏移位置,就得到fling结束时的位置,这个位置就是targetPosition
    59. int targetPos = currentPosition + deltaJump;
    60. if (targetPos < 0) {
    61. targetPos = 0;
    62. }
    63. if (targetPos >= itemCount) {
    64. targetPos = itemCount - 1;
    65. }
    66. return targetPos;
    67. }

     

     

     

     

     

  • 相关阅读:
    MAC和PHY的关系
    list部分接口模拟实现(c++)
    大数据课程K15——Spark的TF-IDF计算Term权重
    thinkphp6入门(21)-- 如何删除图片、文件
    Python创建条形图加点重叠
    Java JSON格式简介说明
    【swjtu】算法作业七
    一些服务器常见漏洞的修复方法
    在uniapp中,如何去掉一些不想要的权限,
    Unity中Shader的屏幕坐标
  • 原文地址:https://blog.csdn.net/huahuahua333686/article/details/125545403