RecyclerView 自定义对齐方式,本文讲的是横向滑动,依靠左边对齐。
滑动后,手指松开。不管向左还是向右,滑动一点,不超过itemView宽的一半,就反弹回去。如果超过了一半,加速滑动到整个itemView的宽。
new LeftSnapHelper().attachToRecyclerView(recyclerView)。
import android.graphics.PointF;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.OrientationHelper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SnapHelper;
public class LeftSnapHelperextends SnapHelper {
private static final float INVALID_DISTANCE =1f;
//数值越小,速度越慢
private static final float MILLISECONDS_PER_INCH =40f;
private OrientationHelper mHorizontalHelper;
private RecyclerView mRecyclerView;
@Override
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)throws IllegalStateException {
super.attachToRecyclerView(recyclerView);
//如果SnapHelper之前已经附着到此RecyclerView上,不用进行任何操作
if (mRecyclerView == recyclerView) {
return;
}
//更新RecyclerView对象引用
mRecyclerView = recyclerView;
//如果SnapHelper之前附着的RecyclerView和现在的不一致,清理掉之前RecyclerView的回调
if(mRecyclerView !=null){
//设置当前RecyclerView对象的回调
// setupCallbacks();
mRecyclerView.setOnFlingListener(this);
//调用snapToTargetExistingView()方法以实现对SnapView的对齐滚动处理
snapToTargetExistingView();
}
}
@Nullable
@Override
protected LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) {
if (!(layoutManagerinstanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return null;
}
LinearSmoothScroller smoothScroller =new LinearSmoothScroller(mRecyclerView.getContext()){
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), targetView);
final int dx = snapDistances[0];
final int dy = snapDistances[1];
final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
if (time >0) {
action.update(dx, dy, time, mDecelerateInterpolator);
}
}
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
return smoothScroller;
}
private void snapToTargetExistingView() {
if (mRecyclerView ==null) {
return;
}
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager ==null) {
return;
}
//找出SnapView
View snapView = findSnapView(layoutManager);
if (snapView ==null) {
return;
}
//计算出SnapView需要滚动的距离
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
//如果需要滚动的距离不是为0,就调用smoothScrollBy()使RecyclerView滚动相应的距离
if (snapDistance[0] !=0 || snapDistance[1] !=0) {
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
@Override
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
int[] out =new int[2];
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToStart(targetView, getHorizontalHelper(layoutManager));
}else {
out[0] =0;
}
return out;
}
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
//判断layoutManager是否实现了RecyclerView.SmoothScroller.ScrollVectorProvider这个接口
if (!(layoutManagerinstanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return RecyclerView.NO_POSITION;
}
final int itemCount = layoutManager.getItemCount();
if (itemCount ==0) {
return RecyclerView.NO_POSITION;
}
//找到snapView
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;
// 通过ScrollVectorProvider接口中的computeScrollVectorForPosition()方法
// 来确定layoutManager的布局方向
PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount -1);
if (vectorForEnd ==null) {
return RecyclerView.NO_POSITION;
}
int vDeltaJump=0, hDeltaJump;
//横向
if (layoutManager.canScrollHorizontally()) {
//layoutManager是横向布局,并且内容超出一屏,canScrollHorizontally()才返回true
//估算fling结束时相对于当前snapView位置的横向位置偏移量
hDeltaJump = estimateNextPositionDiffForFling(layoutManager, getHorizontalHelper(layoutManager), velocityX, 0);
//vectorForEnd.x < 0代表layoutManager是反向布局的,就把偏移量取反
if (vectorForEnd.x <0) {
hDeltaJump = -hDeltaJump;
}
}else {
//不能横向滚动,横向位置偏移量当然就为0
hDeltaJump =0;
}
//竖向的原理同上
// if (layoutManager.canScrollVertically()) {
// vDeltaJump = estimateNextPositionDiffForFling(layoutManager, getVerticalHelper(layoutManager), 0, velocityY);
// if (vectorForEnd.y < 0) {
// vDeltaJump = -vDeltaJump;
// }
// } else {
// vDeltaJump = 0;
// }
//根据layoutManager的横竖向布局方式,最终横向位置偏移量和竖向位置偏移量二选一,作为fling的位置偏移量
int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
if (deltaJump ==0) {
return RecyclerView.NO_POSITION;
}
//当前位置加上偏移位置,就得到fling结束时的位置,这个位置就是targetPosition
int targetPos = currentPosition + deltaJump;
if (targetPos <0) {
targetPos =0;
}
if (targetPos >= itemCount) {
targetPos = itemCount -1;
}
return targetPos;
}
private int distanceToStart(View targetView, OrientationHelper helper) {
//找到targetView的坐标
return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
}
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
return findStartView(layoutManager, getHorizontalHelper(layoutManager));
}
private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
int childCount = layoutManager.getChildCount();
if (childCount ==0) {
return null;
}
View closestChild =null;
//找到RecyclerView的left坐标
final int startOrLeft;
if(layoutManager.getClipToPadding()){
startOrLeft = helper.getStartAfterPadding();
}else {
startOrLeft =0;
}
int absClosest = Integer.MAX_VALUE;
//遍历当前layoutManager中所有的ItemView
for (int i =0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
//itemView的坐标
int childLeft = helper.getDecoratedStart(child);
//计算此ItemView与RecyclerView左边坐标的距离
int absDistance = Math.abs(childLeft - startOrLeft);
//对比每个ItemView距离到RecyclerView左边点的距离,找到那个最靠近左边的itemView然后返回
if (absDistance < absClosest) {
absClosest = absDistance;
closestChild = child;
int s = layoutManager.getPosition(child);
Log.e("通知",s+"-------");
}
}
return closestChild;
}
private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager, OrientationHelper helper, int velocityX, int velocityY) {
//计算滚动的总距离,这个距离受到触发fling时的速度的影响
int[] distances = calculateScrollDistance(velocityX, velocityY);
//计算每个ItemView的长度
float distancePerChild = computeDistancePerChild(layoutManager, helper);
if (distancePerChild <=0) {
return 0;
}
//这里其实就是根据是横向布局还是纵向布局,来取对应布局方向上的滚动距离
int distance = Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
//distance的正负值符号表示滚动方向,数值表示滚动距离。横向布局方式,内容从右往左滚动为正;竖向布局方式,内容从下往上滚动为正
// 滚动距离/item的长度=滚动item的个数,这里取计算结果的整数部分
if (distance >0) {
return (int) Math.floor(distance / distancePerChild);
}else {
return (int) Math.ceil(distance / distancePerChild);
}
}
private float computeDistancePerChild(RecyclerView.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 INVALID_DISTANCE;
}
//循环遍历layoutManager的itemView,得到最小position和最大position,以及对应的view
for (int i =0; i < childCount; i++) {
View child = layoutManager.getChildAt(i);
final int pos = layoutManager.getPosition(child);
if (pos == RecyclerView.NO_POSITION) {
continue;
}
if (pos < minPos) {
minPos = pos;
minPosView = child;
}
if (pos > maxPos) {
maxPos = pos;
maxPosView = child;
}
}
if (minPosView ==null || maxPosView ==null) {
return INVALID_DISTANCE;
}
//最小位置和最大位置肯定就是分布在layoutManager的两端,但是无法直接确定哪个在起点哪个在终点(因为有正反向布局)
//所以取两者中起点坐标小的那个作为起点坐标
//终点坐标的取值一样的道理
int start = Math.min(helper.getDecoratedStart(minPosView), helper.getDecoratedStart(maxPosView));
int end = Math.max(helper.getDecoratedEnd(minPosView), helper.getDecoratedEnd(maxPosView));
//终点坐标减去起点坐标得到这些itemview的总长度
int distance = end - start;
if (distance ==0) {
return INVALID_DISTANCE;
}
// 总长度 / itemview个数 = itemview平均长度
return 1f * distance / ((maxPos - minPos) +1);
}
private OrientationHelper getHorizontalHelper(RecyclerView.LayoutManager layoutManager) {
if (mHorizontalHelper ==null) {
mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
}
return mHorizontalHelper;
}
}
以上是整体代码。
需要注意的是onScrollStateChanged() 会执行两次,自己判断下。
关键地方在图1部分
更改startOrLeft这个值,就可以达到自己想要的效果