RecyclerView 自定义对齐方式,横向滑动

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部分

图1

更改startOrLeft这个值,就可以达到自己想要的效果

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容