RecyclerView中的SnapHelper
RecyclerView相关的文章预计会写六篇,此处是第三篇
- RecyclerView中的position
- RecyclerView中的DiffUtil
- RecyclerView中的SnapHelper
- RecyclerView中的Selection
- RecyclerView中的ConcatAdapter
- RecyclerView中的Glide预加载
SnapHelper是什么
SnapHelper是RecyclerView的辅助类,用来辅助RecyclerView在滚动结束时对齐到某个位置。
SnapHelper
LinearSnapHelper可以让RecyclerView滚动停止时相应的Item停留中间位置,PagerSnapHelper可以使RecyclerView实现像ViewPager一样的效果,一次只能滑一页,而且居中显示。
怎么用
使用比较简单
LinearSnapHelper().attachToRecyclerView(recyclerView)
PagerSnapHelper().attachToRecyclerView(recyclerView)
源码浅析
以LinearSnapHelper为例,简单学习一下原理,为接下来自定义SnapHelper打基础。
/**
* Attaches the {@link SnapHelper} to the provided RecyclerView, by calling
* {@link RecyclerView#setOnFlingListener(RecyclerView.OnFlingListener)}.
* You can call this method with {@code null} to detach it from the current RecyclerView.
*
* @param recyclerView The RecyclerView instance to which you want to add this helper or
* {@code null} if you want to remove SnapHelper from the current
* RecyclerView.
*
* @throws IllegalArgumentException if there is already a {@link RecyclerView.OnFlingListener}
* attached to the provided {@link RecyclerView}.
*
*/
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();
}
}
private void destroyCallbacks() {
mRecyclerView.removeOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(null);
}
private void setupCallbacks() throws IllegalStateException {
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
}
首先看一下attachToRecyclerView,取消snap Helper时可以设置attachToRecyclerView(null),会去掉已添加的scroll listener和fling listener。
添加回调监听时,如果RecyclerView已添加了fling listener,会抛异常。
/**
* Snaps to a target view which currently exists in the attached {@link RecyclerView}. This
* method is used to snap the view when the {@link RecyclerView} is first attached; when
* snapping was triggered by a scroll and when the fling is at its final stages.
*/
void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
接下来看一下snapToTargetExistingView,根据方法名称,我们指定它是用来移动到已存在的目标View,方法逻辑比较简单,其中两个抽象方法findSnapView和calculateDistanceToFinalSnap是我们自定义SnapHelper需要复写的两个抽象方法。
findSnapView - 提供要滚动的目标View
calculateDistanceToFinalSnap - 计算要滚动的距离
// 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();
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
};
接下来看一下给RecyclerView添加的scroll listener,可以看到在滚动结束时会snap到目标的View。
@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);
}
初始设置的fling listener会触发onFling回调,最终触发snapFromFling
/**
* Helper method to facilitate for snapping triggered by a fling.
*
* @param layoutManager The {@link RecyclerView.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 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是一个需要复写的抽象方法,用来提供要滚动的目标adapter 位置。
我们对SnapHelper有了一个大体的了解,接下来看一下LinearSnapHelper是怎么重写这几个抽象方法的。
@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;
}
首先是findSnapView,其中findCenterView,顾名思义就是查找中间的View。
@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;
}
calculateDistanceToFinalSnap的逻辑也很简单。
@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;
}
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;
}
RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
(RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
// deltaJumps sign comes from the velocity which may not match the order of children in
// the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to
// get the direction.
PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
if (vectorForEnd == null) {
// cannot get a vector for the given position.
return RecyclerView.NO_POSITION;
}
int vDeltaJump, hDeltaJump;
if (layoutManager.canScrollHorizontally()) {
hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
getHorizontalHelper(layoutManager), velocityX, 0);
if (vectorForEnd.x < 0) {
hDeltaJump = -hDeltaJump;
}
} else {
hDeltaJump = 0;
}
if (layoutManager.canScrollVertically()) {
vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
getVerticalHelper(layoutManager), 0, velocityY);
if (vectorForEnd.y < 0) {
vDeltaJump = -vDeltaJump;
}
} else {
vDeltaJump = 0;
}
int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
if (deltaJump == 0) {
return RecyclerView.NO_POSITION;
}
int targetPos = currentPosition + deltaJump;
if (targetPos < 0) {
targetPos = 0;
}
if (targetPos >= itemCount) {
targetPos = itemCount - 1;
}
return targetPos;
}
findTargetSnapPosition的逻辑稍显复杂,但感觉也还好。
自定义SnapHelper
接下来按照LinearSnapHelper,咱们自定义一个item停留顶部或左侧的SnapHelper。
override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
if (layoutManager!!.canScrollVertically()) {
return findStartView(layoutManager, getVerticalHelper(layoutManager))
} else if (layoutManager.canScrollHorizontally()) {
return findStartView(layoutManager, getHorizontalHelper(layoutManager))
}
return null
}
private fun findStartView(
layoutManager: RecyclerView.LayoutManager,
helper: OrientationHelper?
): View? {
if (layoutManager !is LinearLayoutManager) {
return null
}
var firstPos = layoutManager.findFirstVisibleItemPosition();
if (firstPos == RecyclerView.NO_POSITION) {
return null
}
if (layoutManager.findLastCompletelyVisibleItemPosition() == layoutManager.itemCount - 1) {
return null
}
var firstView = layoutManager.findViewByPosition(firstPos)
if (helper!!.getDecoratedEnd(firstView) > 0
&& helper.getDecoratedEnd(firstView) >= helper.getDecoratedMeasurement(firstView) / 2
) {
return firstView
}
return layoutManager.findViewByPosition(firstPos + 1)
}
首先是findSnapView,参考了LinearSnapHelper和让你明明白白的使用RecyclerView——SnapHelper详解的写法。
override fun calculateDistanceToFinalSnap(
layoutManager: RecyclerView.LayoutManager,
targetView: View
): IntArray? {
val out = IntArray(2)
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToStart(targetView,
getHorizontalHelper(layoutManager)!!
)
} else {
out[0] = 0
}
if (layoutManager.canScrollVertically()) {
out[1] = distanceToStart(targetView,
getVerticalHelper(layoutManager)!!
)
} else {
out[1] = 0
}
return out
}
private fun distanceToStart(
targetView: View, helper: OrientationHelper
): Int {
return helper.getDecoratedStart(targetView) - helper.startAfterPadding
}
calculateDistanceToFinalSnap稍微简单一点
override fun findTargetSnapPosition(
layoutManager: RecyclerView.LayoutManager?,
velocityX: Int,
velocityY: Int
): Int {
if (layoutManager !is ScrollVectorProvider) {
return RecyclerView.NO_POSITION
}
val itemCount = layoutManager.itemCount
if (itemCount == 0) {
return RecyclerView.NO_POSITION
}
val currentView = findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
val currentPosition = layoutManager.getPosition(currentView)
if (currentPosition == RecyclerView.NO_POSITION) {
return RecyclerView.NO_POSITION
}
// deltaJumps sign comes from the velocity which may not match the order of children in
// the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to
// get the direction.
// deltaJumps sign comes from the velocity which may not match the order of children in
// the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to
// get the direction.
val vectorForEnd = layoutManager.computeScrollVectorForPosition(itemCount - 1)
?: // cannot get a vector for the given position.
return RecyclerView.NO_POSITION
var vDeltaJump: Int
var hDeltaJump: Int
if (layoutManager.canScrollHorizontally()) {
hDeltaJump = estimateNextPositionDiffForFling(
layoutManager,
getHorizontalHelper(layoutManager)!!, velocityX, 0
)
if (vectorForEnd.x < 0) {
hDeltaJump = -hDeltaJump
}
} else {
hDeltaJump = 0
}
if (layoutManager.canScrollVertically()) {
vDeltaJump = estimateNextPositionDiffForFling(
layoutManager,
getVerticalHelper(layoutManager)!!, 0, velocityY
)
if (vectorForEnd.y < 0) {
vDeltaJump = -vDeltaJump
}
} else {
vDeltaJump = 0
}
val deltaJump = if (layoutManager.canScrollVertically()) vDeltaJump else hDeltaJump
if (deltaJump == 0) {
return RecyclerView.NO_POSITION
}
var targetPos = currentPosition + deltaJump
if (targetPos < 0) {
targetPos = 0
}
if (targetPos >= itemCount) {
targetPos = itemCount - 1
}
return targetPos
}
findTargetSnapPosition这部分直接拷贝的LinearSnapHelper的源码。
参考: