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;
? ? //數(shù)值越小,速度越慢
? ? 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之前已經(jīng)附著到此RecyclerView上呛牲,不用進行任何操作
? ? ? ? if (mRecyclerView == recyclerView) {
return;
? ? ? ? }
//更新RecyclerView對象引用
? ? ? ? mRecyclerView = recyclerView;
? ? ? ? //如果SnapHelper之前附著的RecyclerView和現(xiàn)在的不一致刮萌,清理掉之前RecyclerView的回調(diào)
? ? ? ? if(mRecyclerView !=null){
//設(shè)置當(dāng)前RecyclerView對象的回調(diào)
//? ? ? ? ? ? setupCallbacks();
? ? ? ? ? ? mRecyclerView.setOnFlingListener(this);
? ? ? ? ? ? //調(diào)用snapToTargetExistingView()方法以實現(xiàn)對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,就調(diào)用smoothScrollBy()使RecyclerView滾動相應(yīng)的距離
? ? ? ? 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) {
//判斷l(xiāng)ayoutManager是否實現(xiàn)了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是橫向布局娘扩,并且內(nèi)容超出一屏着茸,canScrollHorizontally()才返回true
? ? ? ? ? ? //估算fling結(jié)束時相對于當(dāng)前snapView位置的橫向位置偏移量
? ? ? ? ? ? hDeltaJump = estimateNextPositionDiffForFling(layoutManager, getHorizontalHelper(layoutManager), velocityX, 0);
? ? ? ? ? ? //vectorForEnd.x < 0代表layoutManager是反向布局的,就把偏移量取反
? ? ? ? ? ? if (vectorForEnd.x <0) {
hDeltaJump = -hDeltaJump;
? ? ? ? ? ? }
}else {
//不能橫向滾動琐旁,橫向位置偏移量當(dāng)然就為0
? ? ? ? ? ? hDeltaJump =0;
? ? ? ? }
//豎向的原理同上
//? ? ? ? if (layoutManager.canScrollVertically()) {
//? ? ? ? ? ? vDeltaJump = estimateNextPositionDiffForFling(layoutManager, getVerticalHelper(layoutManager), 0, velocityY);
//? ? ? ? ? ? if (vectorForEnd.y < 0) {
//? ? ? ? ? ? ? ? vDeltaJump = -vDeltaJump;
//? ? ? ? ? ? }
//? ? ? ? } else {
//? ? ? ? ? ? vDeltaJump = 0;
//? ? ? ? }
? ? ? ? //根據(jù)layoutManager的橫豎向布局方式涮阔,最終橫向位置偏移量和豎向位置偏移量二選一,作為fling的位置偏移量
? ? ? ? int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
? ? ? ? if (deltaJump ==0) {
return RecyclerView.NO_POSITION;
? ? ? ? }
//當(dāng)前位置加上偏移位置灰殴,就得到fling結(jié)束時的位置敬特,這個位置就是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的坐標(biāo)
? ? ? ? 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坐標(biāo)
? ? ? ? final int startOrLeft;
? ? ? ? if(layoutManager.getClipToPadding()){
startOrLeft = helper.getStartAfterPadding();
? ? ? ? }else {
startOrLeft =0;
? ? ? ? }
int absClosest = Integer.MAX_VALUE;
? ? ? ? //遍歷當(dāng)前l(fā)ayoutManager中所有的ItemView
? ? ? ? for (int i =0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
? ? ? ? ? ? //itemView的坐標(biāo)
? ? ? ? ? ? int childLeft = helper.getDecoratedStart(child);
? ? ? ? ? ? //計算此ItemView與RecyclerView左邊坐標(biāo)的距離
? ? ? ? ? ? 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) {
//計算滾動的總距離牺陶,這個距離受到觸發(fā)fling時的速度的影響
? ? ? ? int[] distances = calculateScrollDistance(velocityX, velocityY);
? ? ? ? //計算每個ItemView的長度
? ? ? ? float distancePerChild = computeDistancePerChild(layoutManager, helper);
? ? ? ? if (distancePerChild <=0) {
return 0;
? ? ? ? }
//這里其實就是根據(jù)是橫向布局還是縱向布局伟阔,來取對應(yīng)布局方向上的滾動距離
? ? ? ? int distance = Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
? ? ? ? //distance的正負(fù)值符號表示滾動方向,數(shù)值表示滾動距離掰伸。橫向布局方式皱炉,內(nèi)容從右往左滾動為正;豎向布局方式狮鸭,內(nèi)容從下往上滾動為正
? ? ? ? // 滾動距離/item的長度=滾動item的個數(shù)合搅,這里取計算結(jié)果的整數(shù)部分
? ? ? ? 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;
? ? ? ? }
//循環(huán)遍歷layoutManager的itemView,得到最小position和最大position怕篷,以及對應(yīng)的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的兩端历筝,但是無法直接確定哪個在起點哪個在終點(因為有正反向布局)
? ? ? ? //所以取兩者中起點坐標(biāo)小的那個作為起點坐標(biāo)
? ? ? ? //終點坐標(biāo)的取值一樣的道理
? ? ? ? int start = Math.min(helper.getDecoratedStart(minPosView), helper.getDecoratedStart(maxPosView));
? ? ? ? int end = Math.max(helper.getDecoratedEnd(minPosView), helper.getDecoratedEnd(maxPosView));
? ? ? ? //終點坐標(biāo)減去起點坐標(biāo)得到這些itemview的總長度
? ? ? ? int distance = end - start;
? ? ? ? if (distance ==0) {
return INVALID_DISTANCE;
? ? ? ? }
// 總長度 / itemview個數(shù) = itemview平均長度
? ? ? ? return 1f * distance / ((maxPos - minPos) +1);
? ? }
private OrientationHelper getHorizontalHelper(RecyclerView.LayoutManager layoutManager) {
if (mHorizontalHelper ==null) {
mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
? ? ? ? }
return mHorizontalHelper;
? ? }
}
? 以上是整體代碼。
需要注意的是onScrollStateChanged() 會執(zhí)行兩次廊谓,自己判斷下梳猪。
關(guān)鍵地方在圖1部分
更改startOrLeft這個值,就可以達到自己想要的效果