Android-->如何将RecyclerView打造成ViewPager的效果


更新于:2017-2-16
以前的实现方式, 虽然面前可以达到效果, 但是着实有点low,
现在提供一种体验相当好的解决方案:SnapHelper

以下是实现代码: 其实就是同时处理OnScrollListener事件和OnFlingListener事件.
比我之前的方法多了一个OnFlingListener事件的监听.

public class ViewPagerSnapHelper extends SnapHelper {

    /**
     * 每一页中, 含有多少个item
     */
    int mPageItemCount = 1;
    /**
     * 当前页面索引
     */
    int mCurrentPageIndex = 0;
    RecyclerView mRecyclerView;
    PageListener mPageListener;

    /**
     * 需要滚动到目标的页面索引
     */
    int mTargetIndex = RecyclerView.NO_POSITION;

    /**
     * fling操作时,需要锁住目标索引位置
     */
    boolean isFling = false;

    int scrollState;

    private RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            scrollState = newState;
            L.w("scroll state : " + newState);

            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                onScrollEnd();
            } else if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
                isFling = false;
            } else if (newState == RecyclerView.SCROLL_STATE_SETTLING) {

            }
        }
    };

    public ViewPagerSnapHelper(int pageItemCount) {
        if (pageItemCount < 1) {
            throw new IllegalStateException("page item count need greater than 1");
        }
        this.mPageItemCount = pageItemCount;
    }

    protected void onScrollEnd() {
        int old = mCurrentPageIndex;
        int index = getPagerIndex(0, 0);
        //L.i("current->" + mCurrentPageIndex + " index->" + index + " target->" + mTargetIndex);

        if (index == mTargetIndex) {
            mCurrentPageIndex = mTargetIndex;
            //滚动结束后, 目标的索引位置和当前的索引位置相同, 表示已经完成了页面切换
            if (old != mCurrentPageIndex) {
                //L.e("page from->" + old + " to->" + mCurrentPageIndex);
            }
            if (mPageListener != null) {
                mPageListener.onPageSelector(mCurrentPageIndex);
            }
        }
    }

    @Override
    public void attachToRecyclerView(@Nullable RecyclerView recyclerView) throws IllegalStateException {
        if (recyclerView == null) {
            throw new NullPointerException("RecyclerView not be null");
        }
        mRecyclerView = recyclerView;
        super.attachToRecyclerView(recyclerView);
        mRecyclerView.addOnScrollListener(mScrollListener);
    }

    @Nullable
    @Override
    public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
                                              @NonNull View targetView) {

        int[] out = new int[2];
        if (layoutManager.canScrollHorizontally()) {
            out[0] = mTargetIndex * mRecyclerView.getMeasuredWidth() - mRecyclerView.computeHorizontalScrollOffset();
        } else {
            out[0] = 0;
        }

        if (layoutManager.canScrollVertically()) {
            out[1] = mTargetIndex * mRecyclerView.getMeasuredHeight() - mRecyclerView.computeVerticalScrollOffset();
        } else {
            out[1] = 0;
        }
        return out;
    }

    @Nullable
    @Override
    public View findSnapView(RecyclerView.LayoutManager layoutManager) {
        int childCount = mRecyclerView.getLayoutManager().getChildCount();
        final int pagerIndex = getPagerIndex(0, 0);
        if (childCount == 0 || isFling) {
            return null;
        }
        mTargetIndex = pagerIndex;
        //随便返回一个补位空的view,就行.不需要通过这个View计算位置.
        return mRecyclerView.getLayoutManager().getChildAt(0);
    }

    @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();

        boolean handle = Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity;
        //L.w("onFling " + handle + " " + isFling);
        if (isFling) {
            return false;
        }

        if (handle) {
            if (mTargetIndex != RecyclerView.NO_POSITION) {
                mCurrentPageIndex = mTargetIndex;
            }

            if (velocityX > 0 || velocityY > 0) {
                mTargetIndex = fixPagerIndex(mCurrentPageIndex + 1);
            } else if (velocityX < 0 || velocityY < 0) {
                mTargetIndex = fixPagerIndex(mCurrentPageIndex - 1);
            } else {
                mTargetIndex = fixPagerIndex(mCurrentPageIndex);
            }

            int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, null);
            if (snapDistance[0] != 0 || snapDistance[1] != 0) {
                isFling = true;
                mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
            } else {
                onScrollEnd();
            }
        }
        return handle;
    }

    /**
     * 只会在onFling的时候调用
     */
    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
                                      int velocityY) {
        final int itemCount = layoutManager.getItemCount();
        if (itemCount == 0) {
            return RecyclerView.NO_POSITION;
        }

        mTargetIndex = fixPagerIndex(getPagerIndex(velocityX, velocityY));
        return mTargetIndex * mPageItemCount;
    }

    /**
     * 获取当前应该显示第几页
     */
    private int getPagerIndex(int velocityX, int velocityY) {
        final int verticalScrollOffset = mRecyclerView.computeVerticalScrollOffset();
        final int horizontalScrollOffset = mRecyclerView.computeHorizontalScrollOffset();

        final int currentVerticalScrollOffset = mCurrentPageIndex * mRecyclerView.getMeasuredHeight();
        final int currentHorizontalScrollOffset = mCurrentPageIndex * mRecyclerView.getMeasuredWidth();

        int index = 0;
        if (mRecyclerView.getLayoutManager().canScrollVertically()) {
            //除掉整页距离之后的距离
            final float offset = verticalScrollOffset * 1.f % mRecyclerView.getMeasuredHeight();
            final float page = verticalScrollOffset * 1.f / mRecyclerView.getMeasuredHeight();//前面还有多少页
            index = (int) Math.floor(page);//前面还有多少页, 取整
            if (offset == 0) {
                return index;
            }

            if (currentVerticalScrollOffset <= verticalScrollOffset) {
                //向上滚动
                if (offset >= mRecyclerView.getMeasuredHeight() / 2) {
                    //超过一半的距离
                    index = mCurrentPageIndex + 1;
                } else {
                    if (velocityY > 0) {
                        index = mCurrentPageIndex + 1;
                    } else {
                        index = mCurrentPageIndex;
                    }
                }

            } else {
                //向下滚动
                if (offset >= mRecyclerView.getMeasuredHeight() / 2) {
                    //超过一半的距离
                    if (velocityY < 0) {
                        index = mCurrentPageIndex - 1;
                    } else {
                        index = mCurrentPageIndex;
                    }
                } else {
                    index = mCurrentPageIndex - 1;
                }
            }

        } else if (mRecyclerView.getLayoutManager().canScrollHorizontally()) {
            final float offset = horizontalScrollOffset * 1.f % mRecyclerView.getMeasuredWidth();
            final float page = horizontalScrollOffset * 1.f / mRecyclerView.getMeasuredWidth();
            index = (int) Math.floor(page);
            if (offset == 0) {
                return index;
            }

            if (currentHorizontalScrollOffset <= horizontalScrollOffset) {
                //向左滚动
                if (offset >= mRecyclerView.getMeasuredWidth() / 2) {
                    //超过一半的距离
                    index = mCurrentPageIndex + 1;
                } else {
                    if (velocityX > 0) {
                        index = mCurrentPageIndex + 1;
                    } else {
                        index = mCurrentPageIndex;
                    }
                }

            } else {
                //向右滚动
                if (offset >= mRecyclerView.getMeasuredWidth() / 2) {
                    //超过一半的距离
                    if (velocityX < 0) {
                        index = mCurrentPageIndex - 1;
                    } else {
                        index = mCurrentPageIndex;
                    }
                } else {
                    index = mCurrentPageIndex - 1;
                }
            }
        }
        return index;
    }

    private int fixPagerIndex(int index) {
        int maxIndex = mRecyclerView.getLayoutManager().getItemCount() / mPageItemCount - 1;
        int minIndex = 0;
        index = Math.max(minIndex, Math.min(index, maxIndex));
        if (index < mCurrentPageIndex) {
            index = mCurrentPageIndex - 1;
        } else if (index > mCurrentPageIndex) {
            index = mCurrentPageIndex + 1;
        }
        return index;
    }

    /**
     * 页面选择回调监听
     */
    public ViewPagerSnapHelper setPageListener(PageListener pageListener) {
        mPageListener = pageListener;
        return this;
    }

    public interface PageListener {
        void onPageSelector(int position);
    }
}

使用方法:

new ViewPagerSnapHelper(getItemCount()).setPageListener(new ViewPagerSnapHelper.PageListener() {
            @Override
            public void onPageSelector(int position) {
                onViewPagerSelect(position);
            }
        }).attachToRecyclerView(recyclerView);

在配合我之前写的RecyclerViewPagerAdapter(必须), 就可以轻松实现效果了.


如题所示,

都支持横向和纵向, 暂不支持StaggeredGridLayoutManager布局管理.

如图:
在LinearLayoutManager中:


这里写图片描述

在GridLayoutManager中:


这里写图片描述

1:当adapter中Item的数量不足时, 需要用假数据填充.
否则最后一页显示不全, 达不到页面的效果.

@Override
public int getItemCount() {
    rawSize = mAllDatas == null ? 0 : mAllDatas.size();
    final int itemCount = mRecyclerViewPager.getItemCount();
    final double ceil = Math.ceil(rawSize * 1f / itemCount);//当给定的item个数不足以填充一屏时, 使用占位item
    return (int) (ceil * itemCount);
}

2:为了达到沾满整屏的效果, 需要动态计算每一个Item的宽高

@Override
protected void onBindView(RBaseViewHolder holder, int position, T bean) {
    holder.itemView.setLayoutParams(new ViewGroup.LayoutParams(mRecyclerViewPager.getItemWidth(),
            mRecyclerViewPager.getItemHeight()));
    if (holder.getItemViewType() == 200) {
        onBindRawView(holder, position, bean);
    }
}

/**
 * 计算每个Item的宽度
 */
public int getItemWidth() {
    final LayoutManager layoutManager = getLayoutManager();
    int itemWidth = 0;
    if (layoutManager instanceof GridLayoutManager) {
        final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
        final int spanCount = gridLayoutManager.getSpanCount();
        if (gridLayoutManager.getOrientation() == LinearLayoutManager.HORIZONTAL) {
            itemWidth = getRawWidth() / (mItemCount / spanCount);
        } else {
            itemWidth = getRawWidth() / spanCount;
        }

    } else if (layoutManager instanceof LinearLayoutManager) {
        final LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
        if (linearLayoutManager.getOrientation() == LinearLayoutManager.HORIZONTAL) {
            itemWidth = getRawWidth() / mItemCount;
        } else {
            itemWidth = getRawWidth();
        }
    }

    return itemWidth;
}

public int getItemHeight() {
    final LayoutManager layoutManager = getLayoutManager();
    int itemHeight = 0;
    if (layoutManager instanceof GridLayoutManager) {
        final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
        final int spanCount = gridLayoutManager.getSpanCount();
        if (gridLayoutManager.getOrientation() == LinearLayoutManager.HORIZONTAL) {
            itemHeight = getRawHeight() / spanCount;
        } else {
            itemHeight = getRawHeight() / (mItemCount / spanCount);
        }
    } else if (layoutManager instanceof LinearLayoutManager) {
        final LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
        if (linearLayoutManager.getOrientation() == LinearLayoutManager.HORIZONTAL) {
            itemHeight = getRawHeight();
        } else {
            itemHeight = getRawHeight() / mItemCount;
        }
    }

    return itemHeight;
}

3:一切准备好了之后,核心的滚动计算要开始了.

private OnScrollListener mOnScrollListener = new OnScrollListener() {
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        if (newState == SCROLL_STATE_DRAGGING) {
            //开始滚动
            mVerticalScrollOffsetStart = recyclerView.computeVerticalScrollOffset();
            mHorizontalScrollOffsetStart = recyclerView.computeHorizontalScrollOffset();
        } else if (newState == SCROLL_STATE_IDLE) {
            //滚动结束之后
            final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset();
            final int horizontalScrollOffset = recyclerView.computeHorizontalScrollOffset();
            final int rawWidth = getRawWidth();
            final int rawHeight = getRawHeight();
            int pagerIndex = mCurrentPager;

            int dx = 0, dy = 0;
            if (verticalScrollOffset == 0 && horizontalScrollOffset != 0) {
                //横向滚动
                final float page = horizontalScrollOffset * 1.f / rawWidth;//当前滚动到了第几页
                final double floor = Math.floor(page);//前一页
                final double ceil = Math.ceil(page);//后一页
                final int offset;
                final int offsetWidth;//滑动之后,  剩余屏幕的宽度

                if (horizontalScrollOffset > mHorizontalScrollOffsetStart) {
                    pagerIndex = (int) floor;

                    //左滑动
                    offset = (int) (horizontalScrollOffset - floor * rawWidth);
                    offsetWidth = rawWidth - offset;
                    if (offset >= rawWidth / 3) {
                        dx = offsetWidth;
                    } else {
                        dx = -offset;
                    }

                } else if (mHorizontalScrollOffsetStart > horizontalScrollOffset) {
                    pagerIndex = (int) ceil;

                    //右滑动
                    offset = (int) (ceil * rawWidth - horizontalScrollOffset);//横向滚动了多少距离
                    offsetWidth = rawWidth - offset;
                    if (offset >= rawWidth / 3) {
                        dx = -offsetWidth;
                    } else {
                        dx = offset;
                    }
                }

            } else if (horizontalScrollOffset == 0 && verticalScrollOffset != 0) {
                //竖向滚动
                final float page = verticalScrollOffset * 1.f / rawHeight;//当前滚动到了第几页
                final double floor = Math.floor(page);//前一页
                final double ceil = Math.ceil(page);//后一页
                final int offset;
                final int offsetHeight;//滑动之后,  剩余屏幕的高度

                if (verticalScrollOffset > mVerticalScrollOffsetStart) {
                    pagerIndex = (int) floor;

                    //上滑动
                    offset = (int) (verticalScrollOffset - floor * rawHeight);
                    offsetHeight = rawHeight - offset;
                    if (offset >= rawHeight / 3) {
                        dy = offsetHeight;
                    } else {
                        dy = -offset;
                    }

                } else if (mVerticalScrollOffsetStart > verticalScrollOffset) {
                    pagerIndex = (int) ceil;

                    //下滑动
                    offset = (int) (ceil * rawHeight - verticalScrollOffset);//横向滚动了多少距离
                    offsetHeight = rawHeight - offset;
                    if (offset >= rawHeight / 3) {
                        dy = -offsetHeight;
                    } else {
                        dy = offset;
                    }
                }
            } else {
                pagerIndex = 0;
            }

            to(dx, dy);

            onViewPagerSelect(pagerIndex);
        }
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    }
};

4:其他需要注意的东西

//重写此方法, 让手指快速滑动的时候, 慢一点...
@Override
public boolean fling(int velocityX, int velocityY) {
    return super.fling((int) (velocityX * 0.3f), (int) (velocityY * 0.3f));
}


开源地址: https://github.com/angcyo/RecyclerViewPager


至此: 文章就结束了,如有疑问: QQ群 Android:274306954 Swift:399799363 欢迎您的加入.

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

推荐阅读更多精彩内容