工具類:判斷RecyclerView是否滑動到頭或底部

https://github.com/CrazyTaro/recycle_header_adapter/blob/master/header/src/main/java/com/taro/headerrecycle/helper/RecycleVIewScrollHelper.java

public class RecycleViewScrollHelper extends RecyclerView.OnScrollListener {
    private RecyclerView mRvScroll = null;
    private OnScrollDirectionChangedListener mScrollDirectionChangedListener = null;
    //滑動位置變動的監(jiān)聽事件
    private OnScrollPositionChangedListener mScrollPositionChangedListener = null;
    //是否同時檢測滑動到頂部及底部
    private boolean mIsCheckTopBottomTogether = false;
    //檢測滑動頂部/底部的優(yōu)先順序,默認先檢測滑動到底部
    private boolean mIsCheckTopFirstBottomAfter = false;
    //檢測底部滑動時是否檢測滿屏狀態(tài)
    private boolean mIsCheckBottomFullRecycle = false;
    //檢測頂部滑動時是否檢測滿屏狀態(tài)
    private boolean mIsCheckTopFullRecycle = false;
    //頂部滿屏檢測時允許的容差值
    private int mTopOffsetFaultTolerance = 0;
    //底部滿屏檢測時允許的容差值
    private int mBottomOffsetFaultTolerance = 0;


    private int mScrollDx = 0;
    private int mScrollDy = 0;

    /**
     * recycleView的滑動監(jiān)聽事件,用于檢測是否滑動到頂部或者滑動到底部.
     *
     * @param listener {@link OnScrollPositionChangedListener}滑動位置變動監(jiān)聽事件
     */
    public RecycleViewScrollHelper(OnScrollPositionChangedListener listener) {
        mScrollPositionChangedListener = listener;
    }

    @Override
    public void onScrollStateChanged(RecyclerView recyclerView,
                                     int newState) {
        if (mScrollPositionChangedListener == null || recyclerView.getAdapter() == null || recyclerView.getChildCount() <= 0) {
            return;
        }
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (layoutManager instanceof LinearLayoutManager) {
            LinearLayoutManager linearManager = (LinearLayoutManager) layoutManager;
            int lastItemPosition = linearManager.findLastVisibleItemPosition();
            int firstItemPosition = linearManager.findFirstVisibleItemPosition();
            RecyclerView.Adapter adapter = recyclerView.getAdapter();
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                //判斷頂部/底部檢測的優(yōu)先順序
                if (!mIsCheckTopFirstBottomAfter) {
                    //先檢測底部
                    if (this.checkIfScrollToBottom(recyclerView, lastItemPosition, adapter.getItemCount())) {
                        //若檢測滑動到底部時,判斷是否需要同時檢測滑動到頂部
                        if (mIsCheckTopBottomTogether) {
                            //檢測是否滑動到頂部
                            this.checkIfScrollToTop(recyclerView, firstItemPosition);
                            //不管是否滑動到頂部,已經(jīng)觸發(fā)了滑動到底部,所以直接返回,否則會調(diào)用滑動到未知位置的
                            return;
                        } else {
                            //若不需要同時檢測,直接返回
                            return;
                        }
                    } else if (this.checkIfScrollToTop(recyclerView, firstItemPosition)) {
                        //當未檢測滑動到底部時,再檢測是否滑動到頂部
                        return;
                    }
                } else {
                    //先檢測是否滑動到頂部
                    if (this.checkIfScrollToTop(recyclerView, firstItemPosition)) {
                        if (mIsCheckTopBottomTogether) {
                            //檢測是否滑動到底部
                            this.checkIfScrollToBottom(recyclerView, lastItemPosition, adapter.getItemCount());
                            return;
                        } else {
                            //若不需要同時檢測,直接返回
                            return;
                        }
                    } else if (this.checkIfScrollToBottom(recyclerView, lastItemPosition, adapter.getItemCount())) {
                        //當未檢測滑動到底部時,再檢測是否滑動到底部
                        return;
                    }
                }
            }
        }
        //其它任何情況
        mScrollPositionChangedListener.onScrollToUnknown(false, false);
    }

    /**
     * 檢測是否滑動到了頂部item并回調(diào)事件
     *
     * @param recyclerView
     * @param firstItemPosition 第一個可見itemView的position
     * @return
     */
    private boolean checkIfScrollToTop(RecyclerView recyclerView, int firstItemPosition) {
        if (firstItemPosition == 0) {
            if (mIsCheckTopFullRecycle) {
                int childCount = recyclerView.getChildCount();
                View firstChild = recyclerView.getChildAt(0);
                View lastChild = recyclerView.getChildAt(childCount - 1);
                int top = firstChild.getTop();
                int bottom = lastChild.getBottom();
                //recycleView顯示itemView的有效區(qū)域的top坐標Y
                int topEdge = recyclerView.getPaddingTop() - mTopOffsetFaultTolerance;
                //recycleView顯示itemView的有效區(qū)域的bottom坐標Y
                int bottomEdge = recyclerView.getHeight() - recyclerView.getPaddingBottom() - mBottomOffsetFaultTolerance;
                //第一個view的頂部大于top邊界值,說明第一個view已經(jīng)完全顯示在頂部
                //同時最后一個view的底部應(yīng)該小于bottom邊界值,說明最后一個view的底部已經(jīng)超出顯示范圍,部分或者完全移出了界面
                if (top >= topEdge && bottom > bottomEdge) {
                    mScrollPositionChangedListener.onScrollToTop();
                    return true;
                } else {
                    mScrollPositionChangedListener.onScrollToUnknown(true, false);
                }
            } else {
                mScrollPositionChangedListener.onScrollToTop();
                return true;
            }
        }
        return false;
    }

    /**
     * 檢測是否滑動到底部item并回調(diào)事件
     *
     * @param recyclerView
     * @param lastItemPosition 最后一個可見itemView的position
     * @param itemCount        adapter的itemCount
     * @return
     */
    private boolean checkIfScrollToBottom(RecyclerView recyclerView, int lastItemPosition, int itemCount) {
        if (lastItemPosition + 1 == itemCount) {
            //是否進行滿屏的判斷處理
            //未滿屏的情況下將永遠不會被回調(diào)滑動到低部或者頂部
            if (mIsCheckBottomFullRecycle) {
                int childCount = recyclerView.getChildCount();
                //獲取最后一個childView
                View lastChildView = recyclerView.getChildAt(childCount - 1);
                //獲取第一個childView
                View firstChildView = recyclerView.getChildAt(0);
                int top = firstChildView.getTop();
                int bottom = lastChildView.getBottom();
                //recycleView顯示itemView的有效區(qū)域的bottom坐標Y
                int bottomEdge = recyclerView.getHeight() - recyclerView.getPaddingBottom() + mBottomOffsetFaultTolerance;
                //recycleView顯示itemView的有效區(qū)域的top坐標Y
                int topEdge = recyclerView.getPaddingTop() + mTopOffsetFaultTolerance;
                //第一個view的頂部小于top邊界值,說明第一個view已經(jīng)部分或者完全移出了界面
                //最后一個view的底部小于bottom邊界值,說明最后一個view已經(jīng)完全顯示在界面
                //若不處理這種情況,可能會存在recycleView高度足夠高時,itemView數(shù)量很少無法填充一屏,但是滑動到最后一項時依然會發(fā)生回調(diào)
                //此時其實并不需要任何刷新操作的
                if (bottom <= bottomEdge && top < topEdge) {
                    mScrollPositionChangedListener.onScrollToBottom();
                    return true;
                } else {
                    mScrollPositionChangedListener.onScrollToUnknown(false, true);
                }
            } else {
                mScrollPositionChangedListener.onScrollToBottom();
                return true;
            }
        }
        return false;
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        if (mScrollDirectionChangedListener != null) {
            if (dx == 0 && dy == 0) {
                mScrollDirectionChangedListener.onScrollDirectionChanged(0, 0);
            } else if (dx == 0) {
                boolean isUp = dy > 0;
                boolean isBeenUp = mScrollDy > 0;
                if (isUp != isBeenUp) {
                    mScrollDx = dx;
                    mScrollDy = dy;
                    mScrollDirectionChangedListener.onScrollDirectionChanged(dx, dy);
                }
            } else if (dy == 0) {
                boolean isLeft = dx > 0;
                boolean isBeenLeft = mScrollDx > 0;
                if (isLeft != isBeenLeft) {
                    mScrollDx = dx;
                    mScrollDy = dy;
                    mScrollDirectionChangedListener.onScrollDirectionChanged(dx, dy);
                }
            }
        }
    }

    //重置數(shù)據(jù)
    private void reset() {
        mScrollDx = 0;
        mScrollDy = 0;
    }

    /**
     * 關(guān)聯(lián)recycleView,當關(guān)聯(lián)新的recycleView時,會自動移除上一個關(guān)聯(lián)recycleView
     *
     * @param recyclerView
     */
    public void attachToRecycleView(RecyclerView recyclerView) {
        if (recyclerView != mRvScroll) {
            unAttachToRecycleView();
            mRvScroll = recyclerView;
            if (recyclerView != null) {
                recyclerView.addOnScrollListener(this);
            }
        }
    }

    /**
     * 移除與recycleView的綁定
     */
    public void unAttachToRecycleView() {
        if (mRvScroll != null) {
            mRvScroll.removeOnScrollListener(this);
        }
        this.reset();
    }

    /**
     * 設(shè)置滑動方向改變時的回調(diào)接口
     *
     * @param listener
     */
    public void setScrollDirectionChangedListener(OnScrollDirectionChangedListener listener) {
        mScrollDirectionChangedListener = listener;
    }

    /**
     * 設(shè)置頂部允許偏移的容差值,此值僅在允許檢測滿屏?xí)r有效,當{@link #setCheckIfItemViewFullRecycleViewForTop(boolean)}設(shè)置為true 或者{@link #setCheckIfItemViewFullRecycleViewForBottom(boolean)}設(shè)置為true 時有效.<br>
     * 在檢測底部滑動時,對頂部的檢測會添加此容差值(更容易判斷當前第一項childView已超出recycleView的顯示范圍),用于協(xié)助判斷是否滑動到底部.
     * 在檢測頂部滑動時,對頂部的檢測會添加此容差值(更容易判斷為滑動到了頂部)
     *
     * @param offset 容差值,此值必須為0或正數(shù)
     */
    public void setTopOffsetFaultTolerance(@IntRange(from = 0) int offset) {
        mTopOffsetFaultTolerance = offset;
    }

    /**
     * 設(shè)置頂部允許偏移的容差值,此值僅在允許檢測滿屏?xí)r有效,當{@link #setCheckIfItemViewFullRecycleViewForTop(boolean)}設(shè)置為true 或者{@link #setCheckIfItemViewFullRecycleViewForBottom(boolean)}設(shè)置為true 時有效.<br>
     * 在檢測底部滑動時,對底部的檢測會添加此容差值(更容易判斷當前最后一項childView已超出recycleView的顯示范圍),用于協(xié)助判斷是否滑動到頂部.
     * 在檢測頂部滑動時,對底部的檢測會添加此容差值(更容易判斷為滑動到了底部)
     *
     * @param offset 容差值,此值必須為0或正數(shù)
     */
    public void setBottomFaultTolerance(@IntRange(from = 0) int offset) {
        mBottomOffsetFaultTolerance = offset;
    }

    /**
     * 設(shè)置是否需要檢測recycleView是否為滿屏的itemView時才回調(diào)事件.<br>
     * <p>
     * 當RecycleView的childView數(shù)量很少時,有可能RecycleView已經(jīng)顯示出所有的itemView,此時不存在向上滑動的可能.<br>
     * 若設(shè)置當前值為true時,只有在RecycleView無法完全顯示所有的itemView時,才會回調(diào)滑動到頂部的事件;否則將不處理;<br>
     * 若設(shè)置為false則反之,不管任何時候只要滑動并頂部item顯示時都會回調(diào)滑動事件
     *
     * @param isNeedToCheck true為當檢測是否滿屏顯示;false不檢測,直接回調(diào)事件
     */
    public void setCheckIfItemViewFullRecycleViewForTop(boolean isNeedToCheck) {
        mIsCheckTopFullRecycle = isNeedToCheck;
    }

    /**
     * 設(shè)置是否需要檢測recycleView是否為滿屏的itemView時才回調(diào)事件.<br>
     * <p>
     * 當RecycleView的childView數(shù)量很少時,有可能RecycleView已經(jīng)顯示o出所有的itemView,此時不存在向下滑動的可能.
     * 若設(shè)置當前值為true時,只有在RecycleView無法完全顯示所有的itemView時,才會回調(diào)滑動到底部的事件;否則將不處理;
     * 若設(shè)置為false則反之,不管任何時候只要滑動到底部都會回調(diào)滑動事件
     *
     * @param isNeedToCheck true為當檢測是否滿屏顯示;false不檢測,直接回調(diào)事件
     */
    public void setCheckIfItemViewFullRecycleViewForBottom(boolean isNeedToCheck) {
        mIsCheckBottomFullRecycle = isNeedToCheck;
    }

    /**
     * 設(shè)置是否先檢測滑動到哪里.默認為false,先檢測滑動到底部
     *
     * @param isTopFirst true為先檢測滑動到頂部再檢測滑動到底部;false為先檢測滑動到底部再滑動到頂部
     */
    public void setCheckScrollToTopFirstBottomAfter(boolean isTopFirst) {
        mIsCheckTopFirstBottomAfter = isTopFirst;
    }

    /**
     * 設(shè)置是否同時檢測滑動到頂部及底部,默認為false,先檢測到任何一個狀態(tài)都會直接返回,不會再繼續(xù)檢測其它狀態(tài)
     *
     * @param isCheckTogether true為兩種狀態(tài)都檢測,即使已經(jīng)檢測到其中某種狀態(tài)了.false為先檢測到任何一種狀態(tài)時將不再檢測另一種狀態(tài)
     */
    public void setCheckScrollToTopBottomTogether(boolean isCheckTogether) {
        mIsCheckTopBottomTogether = isCheckTogether;
    }

    /**
     * 滑動位置改變監(jiān)聽事件,滑動到頂部/底部或者非以上兩個位置時
     */
    public interface OnScrollPositionChangedListener {
        /**
         * 滑動到頂部的回調(diào)事件
         */
        public void onScrollToTop();

        /**
         * 滑動到底部的回調(diào)事件
         */
        public void onScrollToBottom();

        /**
         * 滑動到未知位置的回調(diào)事件
         *
         * @param isTopViewVisible    當前位置頂部第一個itemView是否可見,這里是指adapter中的最后一個itemView
         * @param isBottomViewVisible 當前位置底部最后一個itemView是否可見,這里是指adapter中的最后一個itemView
         */
        public void onScrollToUnknown(boolean isTopViewVisible, boolean isBottomViewVisible);
    }

    /**
     * 滑動方向改變時監(jiān)聽事件
     */
    public interface OnScrollDirectionChangedListener {
        /**
         * 滑動方向改變時監(jiān)聽事件,當兩個參數(shù)值都為0時,數(shù)據(jù)變動重新layout
         *
         * @param scrollVertical   豎直方向的滑動方向,向上&lt;0,向下&gt;0,不動(水平滑動時)=0
         * @param scrollHorizontal 水平方向的滑動方向,向左&lt;0,向右&gt;0,不動(豎直滑動時)=0
         */
        public void onScrollDirectionChanged(int scrollHorizontal, int scrollVertical);
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末强饮,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子糊秆,更是在濱河造成了極大的恐慌,老刑警劉巖爹橱,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锡移,死亡現(xiàn)場離奇詭異,居然都是意外死亡涵紊,警方通過查閱死者的電腦和手機愚铡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門蛉签,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人沥寥,你說我怎么就攤上這事碍舍。” “怎么了邑雅?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵片橡,是天一觀的道長。 經(jīng)常有香客問我淮野,道長捧书,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任录煤,我火速辦了婚禮鳄厌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘妈踊。我一直安慰自己,他們只是感情好泪漂,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布廊营。 她就那樣靜靜地躺著,像睡著了一般萝勤。 火紅的嫁衣襯著肌膚如雪露筒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天敌卓,我揣著相機與錄音慎式,去河邊找鬼。 笑死,一個胖子當著我的面吹牛瘪吏,可吹牛的內(nèi)容都是我干的癣防。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼掌眠,長吁一口氣:“原來是場噩夢啊……” “哼蕾盯!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蓝丙,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤级遭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后渺尘,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挫鸽,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年鸥跟,在試婚紗的時候發(fā)現(xiàn)自己被綠了掠兄。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡锌雀,死狀恐怖蚂夕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情腋逆,我是刑警寧澤婿牍,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站惩歉,受9級特大地震影響等脂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜撑蚌,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一上遥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧争涌,春花似錦粉楚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至饮潦,卻和暖如春燃异,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背继蜡。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工回俐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留逛腿,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓仅颇,卻偏偏與公主長得像单默,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子灵莲,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

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