一行代碼讓RecyclerView分頁滾動

序言

我曾經(jīng)寫過一個使用RecycleView打造水平分頁GridView贿堰。當時用到的是對數(shù)據(jù)的重排序包颁,但是這樣處理還是有些問題嚷那,比如用戶數(shù)據(jù)更新以后還需要繼續(xù)重排序胞枕,包括對滑動事件的處理也不是很好。當時主要因為時間比較匆忙魏宽,寫的不是很好腐泻,這一次我將采用自定義LayoutManger的方式實現(xiàn)水平分頁的排版决乎,使用一個工具類實現(xiàn)一行代碼就讓RecycleView具有分頁滑動的特性。

效果

1.水平分頁的效果(采用了自定義LayoutManger+滑動工具類實現(xiàn))派桩,關鍵是不需要修改Adapter构诚,可以用來實現(xiàn)表情列表,或者是商品列表铆惑。

這里寫圖片描述

2.垂直方向的分頁顯示范嘱,可以實現(xiàn)讀報的功能,或者其他需要一頁一頁閱讀的功能员魏,采用了LinearLayoutManger+滑動工具類實現(xiàn)丑蛤,比使用LinearLayout布局的優(yōu)勢在于實現(xiàn)了View的復用。

這里寫圖片描述

3.水平分頁撕阎,這是使用LinearLayoutManger+分頁滑動工具類實現(xiàn)的盏阶,這樣LinearLayout就可以橫向的一頁一頁顯示,用這個實現(xiàn)Banner要比ViewPager要簡單很多闻书,性能也會有所提高。因為ViewPager自己并沒有緩存機制脑慧。

這里寫圖片描述

其實還可以實現(xiàn)很多其他的功能魄眉,限于我的想象力有限就先舉這些例子吧。

使用

1.要想數(shù)據(jù)按一頁一頁的排列就使用HorizontalPageLayoutManager闷袒,在構造方法中傳入行數(shù)和列數(shù)就行了

   //構造HorizontalPageLayoutManager,傳入行數(shù)和列數(shù)
   horizontalPageLayoutManager = new HorizontalPageLayoutManager(3,4);
   //這是我自定義的分頁分割線坑律,樣式是每一頁的四周沒有分割線。大家喜歡可以拿去用
   pagingItemDecoration = new PagingItemDecoration(this, horizontalPageLayoutManager);

2.分頁滾動囊骤,上一步的HorizontalPageLayoutManager只負責Item分頁的排列和回收晃择,而要實現(xiàn)分頁滾動需要使用PagingScrollHelper 這個工具類。注意這個工具類很強的挺狰,使用其他的LayoutManger也可以和這個工具類共同使用實現(xiàn)分頁效果敛熬。

  PagingScrollHelper scrollHelper = new PagingScrollHelper();
  scrollHelper.setUpRecycleView(recyclerView);
  //設置頁面滾動監(jiān)聽
  scrollHelper.setOnPageChangeListener(this);

滑動監(jiān)聽類


    public interface onPageChangeListener {
        void onPageChange(int index);
    }

注意

1豹爹。用于使用了RecyclerView的OnFlingListener,所以RecycleView的版本必須要25以上浪蹂。

這里寫圖片描述

2。如果想使用自定義的LayoutManger實現(xiàn)分頁滑動告材,則必須實現(xiàn)LayoutManger的這兩個方法之一坤次,因為工具類是通過這兩個方法判斷應該怎么滾動的。


        /**
         * Query if horizontal scrolling is currently supported. The default implementation
         * returns false.
         *
         * @return True if this LayoutManager can scroll the current contents horizontally
         */
        public boolean canScrollHorizontally() {
            return false;
        }

        /**
         * Query if vertical scrolling is currently supported. The default implementation
         * returns false.
         *
         * @return True if this LayoutManager can scroll the current contents vertically
         */
        public boolean canScrollVertically() {
            return false;
        }

實現(xiàn)

1.分頁布局的實現(xiàn)斥赋。

要實現(xiàn)自定義LayoutManger缰猴,必須對LayoutManger有一個全面的理解,下面的這兩篇博客寫的很好疤剑,謝謝作者的分享滑绒。

打造屬于你的LayoutManager

RecyclerView系列之(2):為RecyclerView添加分隔線

有了基礎以后闷堡,我們知道代碼的關鍵是onLayoutChildren,下面是我的onLayoutChildren;

 @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {

        if (getItemCount() == 0) {
            removeAndRecycleAllViews(recycler);
            return;
        }
        if (state.isPreLayout()) {
            return;
        }
        //獲取每個Item的平均寬高
        itemWidth = getUsableWidth() / columns;
        itemHeight = getUsableHeight() / rows;

        //計算寬高已經(jīng)使用的量蹬挤,主要用于后期測量
        itemWidthUsed = (columns - 1) * itemWidth;
        itemHeightUsed = (rows - 1) * itemHeight;

        //計算總的頁數(shù)
        pageSize = getItemCount() / onePageSize + (getItemCount() % onePageSize == 0 ? 0 : 1);

        //計算可以橫向滾動的最大值
        totalWidth = (pageSize - 1) * getWidth();

        //分離view
        detachAndScrapAttachedViews(recycler);

        int count = getItemCount();
        for (int p = 0; p < pageSize; p++) {
            for (int r = 0; r < rows; r++) {
                for (int c = 0; c < columns; c++) {
                    int index = p * onePageSize + r * columns + c;
                    if (index == count) {
                        //跳出多重循環(huán)
                        c = columns;
                        r = rows;
                        p = pageSize;
                        break;
                    }

                    View view = recycler.getViewForPosition(index);
                    addView(view);
                    //測量item
                    measureChildWithMargins(view, itemWidthUsed, itemHeightUsed);

                    int width = getDecoratedMeasuredWidth(view);
                    int height = getDecoratedMeasuredHeight(view);
                    //記錄顯示范圍
                    Rect rect = allItemFrames.get(index);
                    if (rect == null) {
                        rect = new Rect();
                    }
                    int x = p * getUsableWidth() + c * itemWidth;
                    int y = r * itemHeight;
                    rect.set(x, y, width + x, height + y);
                    allItemFrames.put(index, rect);


                }
            }
            //每一頁循環(huán)以后就回收一頁的View用于下一頁的使用
            removeAndRecycleAllViews(recycler);
        }

        recycleAndFillItems(recycler, state);
    }

需要注意的是對每個Item的測量問題缚窿,大家仔細看Demo中的效果,在一行的Item中焰扳,最右邊的Item是沒有分割線的倦零,而且RecycleView是支持多個分割線的。

這里寫圖片描述

因此測量的時候必須將分割線考慮進來吨悍,實現(xiàn)Item的寬度+分割線的寬度=總的寬度/item的數(shù)量扫茅,
所以得使用這樣的測量方式:

       //計算寬高已經(jīng)使用的量,主要用于后期測量
        itemWidthUsed = (columns - 1) * itemWidth;
        itemHeightUsed = (rows - 1) * itemHeight;
        //測量item
       measureChildWithMargins(view, itemWidthUsed, itemHeightUsed);

而在measureChildWithMargins中只有當子View寬高都是 match_parent的時候才會重新測量子View

   /**
         * Measure a child view using standard measurement policy, taking the padding
         * of the parent RecyclerView, any added item decorations and the child margins
         * into account.
         *
         * <p>If the RecyclerView can be scrolled in either dimension the caller may
         * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
         *
         * @param child Child view to measure
         * @param widthUsed Width in pixels currently consumed by other views, if relevant
         * @param heightUsed Height in pixels currently consumed by other views, if relevant
         */
        public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
            widthUsed += insets.left + insets.right;
            heightUsed += insets.top + insets.bottom;

            final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
                    getPaddingLeft() + getPaddingRight() +
                            lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
                    canScrollHorizontally());
            final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
                    getPaddingTop() + getPaddingBottom() +
                            lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
                    canScrollVertically());
            if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
                child.measure(widthSpec, heightSpec);
            }
        }

因此item的布局文件只這樣的育瓜,最外層的Layout的寬高必須使用match_parent葫隙,這樣才能實現(xiàn),item的寬高適配RecycleView躏仇。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/bg_item">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:padding="20dp"
        android:text="1"
        android:textSize="30sp" />
</RelativeLayout>

2.實現(xiàn)分頁滾動

RecycleView自身并不處理滾動恋脚,因此需要通過特殊手段實現(xiàn)分頁滾動,我在使用RecycleView打造水平分頁GridView 一文中使用的是自定義ScrollListener來實現(xiàn)焰手,但是滑動就處理不了糟描,一滑動就滑了好幾頁,后來研究RecyclerView的源碼發(fā)現(xiàn)了
OnFlingListener (回來才加的书妻,我寫上一篇文章的時候根本就沒有 (≧≦)/)船响。這是它的說明:

    /**
     * This class defines the behavior of fling if the developer wishes to handle it.
     * <p>
     * Subclasses of {@link OnFlingListener} can be used to implement custom fling behavior.
     *
     * @see #setOnFlingListener(OnFlingListener)
     */
    public static abstract class OnFlingListener {

        /**
         * Override this to handle a fling given the velocities in both x and y directions.
         * Note that this method will only be called if the associated {@link LayoutManager}
         * supports scrolling and the fling is not handled by nested scrolls first.
         *
         * @param velocityX the fling velocity on the X axis
         * @param velocityY the fling velocity on the Y axis
         *
         * @return true if the fling washandled, false otherwise.
         */
        public abstract boolean onFling(int velocityX, int velocityY);
    }

當我們放回true的時候系統(tǒng)就不處理滑動了,而是將滑動交給我們自己處理躲履,我的做法就是使用一個ValueAnimator去定時的調(diào)用RecyclerView的ScrollBy方法實現(xiàn)滾動動畫效果见间。下面是我的工具類源碼:

package com.zhuguohui.horizontalpage.view;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

/**
 * 實現(xiàn)RecycleView分頁滾動的工具類
 * Created by zhuguohui on 2016/11/10.
 */

public class PagingScrollHelper {

    RecyclerView mRecyclerView = null;

    private MyOnScrollListener mOnScrollListener = new MyOnScrollListener();

    private MyOnFlingListener mOnFlingListener = new MyOnFlingListener();
    private int offsetY = 0;
    private int offsetX = 0;

    int startY = 0;
    int startX = 0;


    enum ORIENTATION {
        HORIZONTAL, VERTICAL, NULL
    }

    ORIENTATION mOrientation = ORIENTATION.HORIZONTAL;

    public void setUpRecycleView(RecyclerView recycleView) {
        if (recycleView == null) {
            throw new IllegalArgumentException("recycleView must be not null");
        }
        mRecyclerView = recycleView;
        //處理滑動
        recycleView.setOnFlingListener(mOnFlingListener);
        //設置滾動監(jiān)聽,記錄滾動的狀態(tài)工猜,和總的偏移量
        recycleView.setOnScrollListener(mOnScrollListener);
        //記錄滾動開始的位置
        recycleView.setOnTouchListener(mOnTouchListener);
        //獲取滾動的方向
        updateLayoutManger();
    }

    public void updateLayoutManger() {
        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager != null) {
            if (layoutManager.canScrollVertically()) {
                mOrientation = ORIENTATION.VERTICAL;
            } else if (layoutManager.canScrollHorizontally()) {
                mOrientation = ORIENTATION.HORIZONTAL;
            } else {
                mOrientation = ORIENTATION.NULL;
            }
            if (mAnimator != null) {
                mAnimator.cancel();
            }
            startX = 0;
            startY = 0;
            offsetX = 0;
            offsetY = 0;

        }

    }

    ValueAnimator mAnimator = null;

    public class MyOnFlingListener extends RecyclerView.OnFlingListener {

        @Override
        public boolean onFling(int velocityX, int velocityY) {
            if (mOrientation == ORIENTATION.NULL) {
                return false;
            }
            //獲取開始滾動時所在頁面的index
            int p = getStartPageIndex();

            //記錄滾動開始和結束的位置
            int endPoint = 0;
            int startPoint = 0;

            //如果是垂直方向
            if (mOrientation == ORIENTATION.VERTICAL) {
                startPoint = offsetY;

                if (velocityY < 0) {
                    p--;
                } else if (velocityY > 0) {
                    p++;
                }
                //更具不同的速度判斷需要滾動的方向
                //注意米诉,此處有一個技巧,就是當速度為0的時候就滾動會開始的頁面篷帅,即實現(xiàn)頁面復位
                endPoint = p * mRecyclerView.getHeight();

            } else {
                startPoint = offsetX;
                if (velocityX < 0) {
                    p--;
                } else if (velocityX > 0) {
                    p++;
                }
                endPoint = p * mRecyclerView.getWidth();

            }
            if (endPoint < 0) {
                endPoint = 0;
            }

            //使用動畫處理滾動
            if (mAnimator == null) {
                mAnimator = new ValueAnimator().ofInt(startPoint, endPoint);

                mAnimator.setDuration(300);
                mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        int nowPoint = (int) animation.getAnimatedValue();

                        if (mOrientation == ORIENTATION.VERTICAL) {
                            int dy = nowPoint - offsetY;
                            //這里通過RecyclerView的scrollBy方法實現(xiàn)滾動荒辕。
                            mRecyclerView.scrollBy(0, dy);
                        } else {
                            int dx = nowPoint - offsetX;
                            mRecyclerView.scrollBy(dx, 0);
                        }
                    }
                });
                mAnimator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        //回調(diào)監(jiān)聽
                        if (null != mOnPageChangeListener) {
                            mOnPageChangeListener.onPageChange(getPageIndex());
                        }
                    }
                });
            } else {
                mAnimator.cancel();
                mAnimator.setIntValues(startPoint, endPoint);
            }

            mAnimator.start();

            return true;
        }
    }

    public class MyOnScrollListener extends RecyclerView.OnScrollListener {
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            //newState==0表示滾動停止,此時需要處理回滾
            if (newState == 0 && mOrientation != ORIENTATION.NULL) {
                boolean move;
                int vX = 0, vY = 0;
                if (mOrientation == ORIENTATION.VERTICAL) {
                    int absY = Math.abs(offsetY - startY);
                    //如果滑動的距離超過屏幕的一半表示需要滑動到下一頁
                    move = absY > recyclerView.getHeight() / 2;
                    vY = 0;
                    
                    if (move) {
                        vY = offsetY - startY < 0 ? -1000 : 1000;
                    }

                } else {
                    int absX = Math.abs(offsetX - startX);
                    move = absX > recyclerView.getWidth() / 2;
                    if (move) {
                        vX = offsetX - startX < 0 ? -1000 : 1000;
                    }

                }

                mOnFlingListener.onFling(vX, vY);

            }

        }

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            //滾動結束記錄滾動的偏移量
            offsetY += dy;
            offsetX += dx;
        }
    }

    private MyOnTouchListener mOnTouchListener = new MyOnTouchListener();


    public class MyOnTouchListener implements View.OnTouchListener {

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            //手指按下的時候記錄開始滾動的坐標
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                startY = offsetY;
                startX = offsetX;
            }
            return false;
        }

    }

    private int getPageIndex() {
        int p = 0;
        if (mOrientation == ORIENTATION.VERTICAL) {
            p = offsetY / mRecyclerView.getHeight();
        } else {
            p = offsetX / mRecyclerView.getWidth();
        }
        return p;
    }

    private int getStartPageIndex() {
        int p = 0;
        if (mOrientation == ORIENTATION.VERTICAL) {
            p = startY / mRecyclerView.getHeight();
        } else {
            p = startX / mRecyclerView.getWidth();
        }
        return p;
    }

    onPageChangeListener mOnPageChangeListener;

    public void setOnPageChangeListener(onPageChangeListener listener) {
        mOnPageChangeListener = listener;
    }

    public interface onPageChangeListener {
        void onPageChange(int index);
    }

}

下載

這里寫鏈接內(nèi)容

總結

通過這個例子犹褒,我算是吧RecyclerView的源碼看的差不多了抵窒,感覺一切的問題都能從源碼中找到解決方法,所以建議大家多讀源碼叠骑。


2018-02-27 更新

1.解決需要點擊兩次才能刷新的bug(感謝評論區(qū)里面的小伙伴)
2.提供滾動到指定頁面的方法李皇,可以配合數(shù)據(jù)刷新。

       myAdapter.notifyDataSetChanged();
        //滾動到第一頁
        scrollHelper.scrollToPosition(0);

3.提供獲取總頁數(shù)的方法。目前支持的有LinearLayoutManager掉房,StaggeredGridLayoutManager茧跋,HorizontalPageLayoutManager(我自己寫的),如果你想自己的LayoutManger也能獲取到總頁數(shù),請實現(xiàn)相應的方法卓囚。
下面三個是能橫向滾動的LayoutManger,能豎直滾動的有對應的三個方法瘾杭。

  @Override
    public int computeHorizontalScrollRange(RecyclerView.State state) {
       return 0;
    }

    @Override
    public int computeHorizontalScrollOffset(RecyclerView.State state) {
        return 0;
    }

    @Override
    public int computeHorizontalScrollExtent(RecyclerView.State state) {
        return 0;
    }

獲取總頁數(shù)的方法

 //獲取總頁數(shù),采用這種方法才能獲得正確的頁數(shù)。否則會因為RecyclerView.State 緩存問題哪亿,頁數(shù)不正確粥烁。
 //第一次,和每一次更新adapter以后蝇棉。需要使用這樣的方法獲取讨阻。
        recyclerView.post(new Runnable() {
            @Override
            public void run() {
                tv_page_total.setText("共" + scrollHelper.getPageCount() + "頁");
            }
        });

最后的最后,感謝小伙伴的支持篡殷。沒想到這幾百行的小工具這么收歡迎钝吮。大家還有bug,歡迎反饋板辽。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末奇瘦,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子劲弦,更是在濱河造成了極大的恐慌链患,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瓶您,死亡現(xiàn)場離奇詭異,居然都是意外死亡纲仍,警方通過查閱死者的電腦和手機呀袱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來郑叠,“玉大人夜赵,你說我怎么就攤上這事∠绺铮” “怎么了寇僧?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長沸版。 經(jīng)常有香客問我嘁傀,道長,這世上最難降的妖魔是什么视粮? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任细办,我火速辦了婚禮,結果婚禮上蕾殴,老公的妹妹穿的比我還像新娘笑撞。我一直安慰自己岛啸,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布茴肥。 她就那樣靜靜地躺著坚踩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪瓤狐。 梳的紋絲不亂的頭發(fā)上瞬铸,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音芬首,去河邊找鬼赴捞。 笑死,一個胖子當著我的面吹牛郁稍,可吹牛的內(nèi)容都是我干的赦政。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼耀怜,長吁一口氣:“原來是場噩夢啊……” “哼恢着!你這毒婦竟也來了?” 一聲冷哼從身側響起财破,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤掰派,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后左痢,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體靡羡,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年俊性,在試婚紗的時候發(fā)現(xiàn)自己被綠了略步。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡定页,死狀恐怖趟薄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情典徊,我是刑警寧澤杭煎,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站卒落,受9級特大地震影響羡铲,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜儡毕,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一犀勒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦贾费、人聲如沸钦购。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽押桃。三九已至,卻和暖如春导犹,著一層夾襖步出監(jiān)牢的瞬間唱凯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工谎痢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留磕昼,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓节猿,卻偏偏與公主長得像票从,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子滨嘱,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容