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这个值,就可以达到自己想要的效果

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容