RecyclerView辅助
RecyclerView 之 SnapHelper
SnapHelper 介绍
RecyclerView 在 24.2.0 版本中新增了 SnapHelper
这个辅助类,用于辅助 RecyclerView 在滚动结束时将 Item 对齐到某个位置。特别是列表横向滑动时,很多时候不会让列表滑到任意位置,而是会有一定的规则限制,这时候就可以通过 SnapHelper 来定义对齐规则了。
SnapHelper 是一个抽象类,官方提供了一个 LinearSnapHelper
的子类,可以让 RecyclerView 滚动停止时中间的 ItemView 停留 RecyclerView 中间位置。25.1.0 版本中官方又提供了一个 PagerSnapHelper
的子类,可以使 RecyclerView 像 ViewPager 一样的效果,一次只能滑一页,而且居中显示。
注意
- 每次只能注册一个 SnapHelper,否则报错
1
java.lang.IllegalStateException: An instance of OnFlingListener already set.
- onScroll 和 onFling 都会滚动,不会冲突吗?
官方提供的 SnapHelper
LinearSnapHelper
默认实现是对齐中间的 childView 到 RecyclerView 的中间;如果需要更改默认行为,复写 SnapHelper#calculateDistanceToFinalSnap()
方法。
PagerSnapHelper
每次滚动一页
SnapHelper 分析
attachToRecyclerView
SnapHelper#attachToRecyclerView
入口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
throws IllegalStateException {
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks(); // 如果该SnapHelper之前已经绑定了一个RecyclerView,解除该RecyclerView历史回调的关系
}
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
setupCallbacks(); // 注册回调
mGravityScroller = new Scroller(mRecyclerView.getContext(),
new DecelerateInterpolator());
snapToTargetExistingView(); // 移动到指定View
}
}
setupCallbacks():
1
2
3
4
5
6
7
private void setupCallbacks() throws IllegalStateException {
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
mRecyclerView.addOnScrollListener(mScrollListener); // 添加OnScrollListener监听
mRecyclerView.setOnFlingListener(this); // 设置OnFlingListener监听
}
destroyCallbacks():
1
2
3
4
private void destroyCallbacks() {
mRecyclerView.removeOnScrollListener(mScrollListener); // 移除OnScrollListener监听
mRecyclerView.setOnFlingListener(null); // 移除OnFlingListener监听
}
SnapHelper 是一个抽象类,实现了 RecyclerView.OnFlingListener 接口,入口方法 attachToRecyclerView 在 SnapHelper 中定义,该方法主要起到清理、绑定回调关系和初始化位置的作用,在 setupCallbacks 中设置了 addOnScrollListener 和 setOnFlingListener 两种回调;
SnapHelper 处理回调流程
滚动状态回调处理 OnScrollListener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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);
// 静止状态且滚动过一段距离,触发snapToTargetExistingView();
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
// 移动到指定的已存在的View
snapToTargetExistingView();
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
};
逻辑处理的入口在 onScrollStateChanged 方法中,当 newState==RecyclerView.SCROLL_STATE_IDLE 且滚动距离不等于 0,触发 snapToTargetExistingView 方法;snapToTargetExistingView():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SnapHelper
// 移动到指定的已存在的View;attachToRecyclerView时会调用,滚动停止时也会调用
void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
View snapView = findSnapView(layoutManager); // 查找需要对齐的SnapView
if (snapView == null) {
return;
}
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView); // 计算SnapView到最终目标的距离
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
snapToTargetExistingView() 方法顾名思义是移动到指定已存在的 View 的位置,findSnapView 是查到目标的 SnapView,calculateDistanceToFinalSnap 是计算 SnapView 到最终位置的距离;由于 findSnapView 和 calculateDistanceToFinalSnap 是抽象方法,所以需要子类的具体实现;
整理一下滚动状态回调下,SnapHelper 的实现流程图如下:
Fling 结果回调处理 OnFlingListener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SnapHelper
@Override
public boolean onFling(int velocityX, int velocityY) {
RecyclerView.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);
}
如果 velocityX 和 velocityY 都大于最小值的 minFlingVelocity(默认值是 50),返回逻辑由 snapFromFling 来处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SnapHelper
private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return false;
}
RecyclerView.SmoothScroller smoothScroller = createScroller(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;
}
子类实现 findTargetSnapPosition
来决定滚动的 position
SnapHelper 重要方法
findSnapView
View findSnapView(RecyclerView.LayoutManager layoutManager)
提供给 scroll/fling 用。在 scroll 处于 idle 时需要 snap 时,回调该方法找一个 snap 的 view。在 fling 时也会调用
calculateDistanceToFinalSnap
int[] calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, View targetView)
提供给 scroll 用。参数 2 targetView 就是 findSnapView 找到的 snap 的 View。计算滚动到 targetView 的坐标
int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY)
提供给 fling 用。要 snap 的在 adapter 的 position。
OrientationHelper
OrientationHelper 其实就是对 RecycleView 中子 View 管理的工具类,并且它只是一个抽象类,类中定义了获取 View 布局信息的相关方法。
默认实现:createHorizontalHelper
(对应水平的 LayoutManager)和 createVerticalHelper
(对应竖直的 LayoutManager)方法
API (需要注意 RTL,很多 API 都是相对于 Left 和 Right 的,要注意转换)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
public static OrientationHelper createHorizontalHelper(
RecyclerView.LayoutManager layoutManager) {
return new OrientationHelper(layoutManager) {
@Override
public int getEndAfterPadding() {
// 返回RecycleView内容右边界位置(RecycleView的宽度,出去右侧内边距)
return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight();
}
@Override
public int getEnd() {
// 返回RecycleView的宽度,不包括padding
return mLayoutManager.getWidth();
}
@Override
public void offsetChildren(int amount) {
//水平横移子View amount距离
mLayoutManager.offsetChildrenHorizontal(amount);
}
@Override
public int getStartAfterPadding() {
// 获取RecycleView左侧内边距(paddingLeft)
return mLayoutManager.getPaddingLeft();
}
@Override
public int getDecoratedMeasurement(View view) {
// 返回view在水平方向上所占位置的大小(包括view的左右外边距)
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
+ params.rightMargin;
}
@Override
public int getDecoratedMeasurementInOther(View view) {
//返回view在竖直方向上所占位置的大小(包括view的上下外边距)
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
+ params.bottomMargin;
}
@Override
public int getDecoratedEnd(View view) {
//返回view右边界点(包含右内边距和右外边距)在父View中的位置(以父View的(0,0)点位坐标系)
//通俗地讲:子View右边界点到父View的(0,0)点的水平间距
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedRight(view) + params.rightMargin;
}
@Override
public int getDecoratedStart(View view) {
//返回view左边界点(包含左内边距和左外边距)在父View中的位置(以父View的(0,0)点位坐标系)
//通俗地讲:子View左边界点到父View的(0,0)点的水平间距
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedLeft(view) - params.leftMargin;
}
@Override
public int getTransformedEndWithDecoration(View view) {
//返回view水平方向的结束位置(包含右侧的装饰,如你自定义了分割线(ItemDecoration),这个是带分割线宽度的结束位置),相对父容器
mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
return mTmpRect.right;
}
@Override
public int getTransformedStartWithDecoration(View view) {
//返回view水平方向的开始位置(包含左侧的装饰,如你自定义了分割线(ItemDecoration),这个是减去分割线宽度的开始位置),相对父容器
mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
return mTmpRect.left;
}
@Override
public int getTotalSpace() {
//返回Recycleview水平内容区空间大小(宽度,除去左右内边距)
return mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft()
- mLayoutManager.getPaddingRight();
}
@Override
public void offsetChild(View view, int offset) {
//相当于layout效果(在layout基础上偏移offset像素)
view.offsetLeftAndRight(offset);
}
@Override
public int getEndPadding() {
//返回Recycleview右侧内边距大小
return mLayoutManager.getPaddingRight();
}
@Override
public int getMode() {
//获取Recycleview宽度测量模式
return mLayoutManager.getWidthMode();
}
@Override
public int getModeInOther() {
//获取Recycleview高度测量模式
return mLayoutManager.getHeightMode();
}
};
}
注:RV 左上角 (0,0) 为坐标原点,下面原点都指该点
getTotalSpace() RV 宽/高减去 padding
1
2
3
4
5
6
7
8
9
10
// 水平
public int getTotalSpace() {
return mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft()
- mLayoutManager.getPaddingRight();
}
// 垂直
public int getTotalSpace() {
return mLayoutManager.getHeight() - mLayoutManager.getPaddingTop()
- mLayoutManager.getPaddingBottom();
}
getEnd() end 边距离原点的距离,包括 padding(同 RV 的宽度)
1
2
3
4
5
6
7
8
// 水平
public int getEnd() {
return mLayoutManager.getWidth();
}
// 垂直
public int getEnd() {
return mLayoutManager.getHeight();
}
getStartAfterPadding() start 边距离原点的距离,加上 paddingStart 距离 (同 RV 的 paddingLeft)
1
2
3
4
5
6
7
8
9
// 水平
public int getStartAfterPadding() {
return mLayoutManager.getPaddingLeft();
}
// 垂直
public int getStartAfterPadding() {
return mLayoutManager.getPaddingTop();
}
getEndAfterPadding() end 边距离原点的距离,加上 paddingEnd 距离 (同 RV 的 width 减去 paddingRight,针对水平方向)
1
2
3
4
5
6
7
8
9
// 水平
public int getEndAfterPadding() {
return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight();
}
// 垂直
public int getEndAfterPadding() {
return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom();
}
getDecoratedMeasurement(View view) 返回 view 在水平方向上所占位置的大小(包括 view 的左右 margin 和 padding)
1
2
3
4
5
6
7
8
9
10
11
// 水平
public int getDecoratedMeasurement(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin + params.rightMargin;
}
// LayoutManager
public int getDecoratedMeasuredWidth(@NonNull View child) {
final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
return child.getMeasuredWidth() + insets.left + insets.right;
}
getDecoratedMeasurementInOther(View view) 返回 view 在竖直方向上所占位置的大小(包括 view 的上下外边距)
1
2
3
4
5
6
7
// 水平
public int getDecoratedMeasurementInOther(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
+ params.bottomMargin;
}
getDecoratedStart(View view) 返回 view 左边界点(包含左内边距和左外边距)在父 View 中的位置(以父 View 的(0,0)点位坐标系)
通俗地讲:子 View 左边界点到父 View 的(0,0)点的水平间距,不管是否 RTL
1
2
3
4
5
6
7
8
9
public int getDecoratedStart(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedLeft(view) - params.leftMargin;
}
// LayoutManager
public int getDecoratedLeft(@NonNull View child) {
return child.getLeft() - getLeftDecorationWidth(child);
}
注意:在 RTL 中,也是距离 RV 原点的距离
getDecoratedEnd(View view) 回 view 右边界点(包含右内边距和右外边距)在父 View 中的位置(以父 View 的(0,0)点位坐标系)
通俗地讲:子 View 右边界点到父 View 的(0,0)点的水平间距,不管是否 RTL
1
2
3
4
5
6
// 水平
public int getDecoratedEnd(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedRight(view) + params.rightMargin;
}
getEndPadding() 返回 Recycleview 右侧内边距大小(paddingRight)
1
2
3
4
// 水平
public int getEndPadding() {
return mLayoutManager.getPaddingRight();
}
getTransformedStartWithDecoration(View view) 返回 view 水平方向的开始位置(包含左侧的装饰,如你自定义了分割线(ItemDecoration),这个是减去分割线宽度的开始位置),相对父容器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 水平
public int getTransformedStartWithDecoration(View view) {
mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
return mTmpRect.left;
}
// LayoutManager
public void getTransformedBoundingBox(@NonNull View child, boolean includeDecorInsets,
@NonNull Rect out) {
if (includeDecorInsets) {
Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
out.set(-insets.left, -insets.top,
child.getWidth() + insets.right, child.getHeight() + insets.bottom);
} else {
out.set(0, 0, child.getWidth(), child.getHeight());
}
if (mRecyclerView != null) {
final Matrix childMatrix = child.getMatrix();
if (childMatrix != null && !childMatrix.isIdentity()) {
final RectF tempRectF = mRecyclerView.mTempRectF;
tempRectF.set(out);
childMatrix.mapRect(tempRectF);
out.set(
(int) Math.floor(tempRectF.left),
(int) Math.floor(tempRectF.top),
(int) Math.ceil(tempRectF.right),
(int) Math.ceil(tempRectF.bottom)
);
}
}
out.offset(child.getLeft(), child.getTop());
}
getTransformedEndWithDecoration(View view) 返回 view 水平方向的结束位置(包含右侧的装饰,如你自定义了分割线(ItemDecoration),这个是带分割线宽度的结束位置),相对父容器
1
2
3
4
5
// 水平
public int getTransformedEndWithDecoration(View view) {
mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
return mTmpRect.right;
}