Android中RecyclerView實現(xiàn)分頁滾動的方法詳解
一、需求分析
最近公司項目要實現(xiàn)一個需求要滿足以下功能:
1)顯示一個 list 列表, item 數(shù)量不固定。
2)實現(xiàn)翻頁功能,一次翻一頁。
3)實現(xiàn)翻至某一頁功能。
下面介紹通過 RecyclerView 實現(xiàn)該需求的實現(xiàn)過程(效果圖如下)。
二、功能實現(xiàn)
2.1 OnTouchListener 記錄當(dāng)前開始滑動位置
要實現(xiàn)翻頁滑動首先我們要確定是向前翻頁還是向后翻頁,這里通過記錄開始翻頁前當(dāng)前的位置和滑動后的位置比較即可得知,下面選擇手指觸摸按下時滑動的位置為當(dāng)前開始滑動位置:
//當(dāng)前滑動距離 private int offsetY = 0; private int offsetX = 0; //按下屏幕點 private int startY = 0; private int startX = 0; @Override public boolean onTouch(View v, MotionEvent event) { //手指按下的時候記錄開始滾動的坐標(biāo) if (event.getAction() == MotionEvent.ACTION_DOWN) { //手指按下的開始坐標(biāo) startY = offsetY; startX = offsetX; } return false; } }
好了,當(dāng)我們確定了滑動方向,下面要考慮的就是如何實現(xiàn)滑動?
2.2 scrollTo(int x, int y) 和 scrollBy(int x, int y) 實現(xiàn)滑動
滑動我們最容易想到的方法就是 scrollTo(int x, int y)
和 scrollBy(int x, int y)
這兩個方法, scrollTo(int x, int y)
是將當(dāng)前 View 的內(nèi)容滑動至某一位置, scrollBy(int x, int y)
是將當(dāng)前 View 內(nèi)容相對于當(dāng)前位置滑動一定的距離,其實 scrollBy(int x, int y)
內(nèi)部是調(diào)用了 scrollTo(int x, int y)
方法實現(xiàn)的 一開始想用 scrollTo(int x, int y)
去實現(xiàn),但是簡單看了下源碼發(fā)現(xiàn), RecyclerView 不支持這個方法:
@Override public void scrollTo(int x, int y) { Log.w(TAG, "RecyclerView does not support scrolling to an absolute position. " + "Use scrollToPosition instead"); }
所以這里我們就選擇使用 scrollBy(int x, int y)
去實現(xiàn)滑動。
2.3 OnFlingListener 和 OnScrollListener 調(diào)用滑動時機
上面我們決定使用 scrollBy(int x, int y)
去實現(xiàn)滑動,那么現(xiàn)在我們就要確定 scrollBy(int x, int y)
這個方法的調(diào)用時機,我們知道當(dāng)我們滑動 RecyclerView 的時候一般分為兩種情況,一種是手指在屏幕上面緩慢滑動(Scroll),另一種是飛速滑動(onFling),經(jīng)過一番查閱,發(fā)現(xiàn) RecyclerView 中有這兩種狀態(tài)的監(jiān)聽,那么我們一起看一下這兩種狀態(tài)的方法定義,先看 onFling(int velocityX, int velocityY)
:
Note: 由于使用了 RecyclerView 的 OnFlingListener,所以 RecycleView 的版本必須要 recyclerview-v7:25.0.0 以上。
/** * 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); }
方法的注釋寫的也很清楚,當(dāng)這個方法被調(diào)用并且返回 true 的時候系統(tǒng)就不處理滑動了,而是將滑動交給我們自己處理。所以我們可以監(jiān)聽這個方法,當(dāng)我們執(zhí)行快速滑動的時候在這個方法里面計算要滑動的距離并執(zhí)行 scrollBy(int x, int y)
實現(xiàn)滑動,然后直接返回 true,表示滑動我們自己處理了,不需要系統(tǒng)處理。
處理代碼如下:
//當(dāng)前滑動距離 private int offsetY = 0; private int offsetX = 0; //按下屏幕點 private int startY = 0; private int startX = 0; //最后一個可見 view 位置 private int lastItemPosition = -1; //第一個可見view的位置 private int firstItemPosition = -2; //總 itemView 數(shù)量 private int totalNum; @Override public boolean onFling(int velocityX, int velocityY) { if (mOrientation == ORIENTATION.NULL) { return false; } //獲取開始滾動時所在頁面的index int page = getStartPageIndex(); //記錄滾動開始和結(jié)束的位置 int endPoint = 0; int startPoint = 0; //如果是垂直方向 if (mOrientation == ORIENTATION.VERTICAL) { //開始滾動位置,當(dāng)前開始執(zhí)行 scrollBy 位置 startPoint = offsetY; if (velocityY < 0) { page--; } else if (velocityY > 0) { page++; } else if (pageNum != -1) { if (lastItemPosition + 1 == totalNum) { mRecyclerView.scrollToPosition(0); } page = pageNum - 1; } //更具不同的速度判斷需要滾動的方向 //一次滾動一個 mRecyclerView 高度 endPoint = page * mRecyclerView.getHeight(); } else { startPoint = offsetX; if (velocityX < 0) { page--; } else if (velocityX > 0) { page++; } else if (pageNum != -1) { if (lastItemPosition + 1 == totalNum) { mRecyclerView.scrollToPosition(0); } page = pageNum - 1; } endPoint = page * mRecyclerView.getWidth(); } //使用動畫處理滾動 if (mAnimator == null) { mAnimator = 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; if (dy == 0) return; //這里通過RecyclerView的scrollBy方法實現(xiàn)滾動。 mRecyclerView.scrollBy(0, dy); } else { int dx = nowPoint - offsetX; mRecyclerView.scrollBy(dx, 0); } } }); mAnimator.addListener(new AnimatorListenerAdapter() { //動畫結(jié)束 @Override public void onAnimationEnd(Animator animation) { //回調(diào)監(jiān)聽 if (null != mOnPageChangeListener) { mOnPageChangeListener.onPageChange(getPageIndex()); } //滾動完成,進(jìn)行判斷是否滾到頭了或者滾到尾部了 RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager(); //判斷是當(dāng)前l(fā)ayoutManager是否為LinearLayoutManager // 只有LinearLayoutManager才有查找第一個和最后一個可見view位置的方法 if (layoutManager instanceof LinearLayoutManager) { LinearLayoutManager linearManager = (LinearLayoutManager) layoutManager; //獲取最后一個可見view的位置 lastItemPosition = linearManager.findLastVisibleItemPosition(); //獲取第一個可見view的位置 firstItemPosition = linearManager.findFirstVisibleItemPosition(); } totalNum = mRecyclerView.getAdapter().getItemCount(); if (totalNum == lastItemPosition + 1) { updateLayoutManger(); } if (firstItemPosition == 0) { updateLayoutManger(); } } }); } else { mAnimator.cancel(); mAnimator.setIntValues(startPoint, endPoint); } mAnimator.start(); return true; } }
再看 OnScrollListener 滾動監(jiān)聽方法:
public abstract static class OnScrollListener { /** * Callback method to be invoked when RecyclerView's scroll state changes. * * @param recyclerView The RecyclerView whose scroll state has changed. * @param newState The updated scroll state. One of {@link #SCROLL_STATE_IDLE}, * {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}. */ public void onScrollStateChanged(RecyclerView recyclerView, int newState){} /** * Callback method to be invoked when the RecyclerView has been scrolled. This will be * called after the scroll has completed. * <p> * This callback will also be called if visible item range changes after a layout * calculation. In that case, dx and dy will be 0. * 滾動完成調(diào)用 * @param recyclerView The RecyclerView which scrolled. * @param dx The amount of horizontal scroll. * @param dy The amount of vertical scroll. */ public void onScrolled(RecyclerView recyclerView, int dx, int dy){} }
這個監(jiān)聽類主要有兩個方法一個是 onScrollStateChanged(RecyclerView recyclerView, int newState)
滾動狀態(tài)發(fā)生變化調(diào)用, onScrolled(RecyclerView recyclerView, int dx, int dy) RecyclerView
發(fā)生滾動和滾動完成調(diào)用。有了這兩個監(jiān)聽,當(dāng)我們進(jìn)行緩慢滑動我們就可以在 onScrollStateChanged(RecyclerView recyclerView, int newState)
中監(jiān)聽滑動如果結(jié)束并且超過一定距離去執(zhí)行翻頁,而通過 onScrolled(RecyclerView recyclerView, int dx, int dy)
方法可以記錄當(dāng)前的滑動距離。
處理方法如下:
@Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { //如果滑動停止 if (newState == RecyclerView.SCROLL_STATE_IDLE && 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; } } //調(diào)用滑動 mOnFlingListener.onFling(vX, vY); } } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { //滾動結(jié)束記錄滾動的偏移量 //記錄當(dāng)前滾動到的位置 offsetY += dy; offsetX += dx; } }
到這里我們要實現(xiàn)滑動的方法和時機基本就搞定了,剩下的就是滑動位置計算和滑動效果實現(xiàn),滑動位置計算就是一次滑動一整頁,這個沒什么可說的,所以簡單說下實現(xiàn)彈性滑動效果。
2.4 ValueAnimator 實現(xiàn)彈性滑動效果
我們知道如果我們直接調(diào)用 scrollBy(int x, int y)
這個方法去滑動,那么是沒有緩慢滑動的效果,看著有點愣,所以這里我們通過 ValueAnimator 這個類來實現(xiàn)緩慢滑動的效果,這個就很簡單了,直接貼代碼:
if (mAnimator == null) { mAnimator = 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; if (dy == 0) return; //這里通過RecyclerView的scrollBy方法實現(xiàn)滾動。 mRecyclerView.scrollBy(0, dy); } else { int dx = nowPoint - offsetX; mRecyclerView.scrollBy(dx, 0); } } });
2.5 翻頁至某一頁
這里翻頁至某一頁的實現(xiàn)有了上面的基礎(chǔ)就很好實現(xiàn)了,就是直接調(diào)用 我們已經(jīng)實現(xiàn)好了的 onFling(int velocityX, int velocityY)
方法,然后把頁數(shù)傳遞過去計算一下就可以了 :
public void setPageNum(int page) { this.pageNum = page; mOnFlingListener.onFling(0, 0); }
到這里前面說的功能已經(jīng)全部實現(xiàn)完畢.
具體代碼細(xì)節(jié)請見:
github 地址: pagerecyclerview
本地下載:點擊這里
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對各位Android開發(fā)者們能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
Android開發(fā)實現(xiàn)按鈕點擊切換背景并修改文字顏色的方法
這篇文章主要介紹了Android開發(fā)實現(xiàn)按鈕點擊切換背景并修改文字顏色的方法,涉及Android界面布局與相關(guān)屬性設(shè)置技巧,需要的朋友可以參考下2018-01-01Android自定義豎排TextView實現(xiàn)實例
這篇文章主要介紹了Android自定義豎排TextView實現(xiàn)實例的相關(guān)資料,需要的朋友可以參考下2017-05-05Android中activity跳轉(zhuǎn)按鈕事件的四種寫法
這篇文章主要介紹了Android中activity跳轉(zhuǎn)按鈕事件的四種寫法,下文中包括四個activity的內(nèi)容詳解,非常不錯具有參考借鑒價值,需要的朋友可以參考下2016-10-10Android使用AndroidUtilCode實現(xiàn)多語言
這篇文章主要為大家介紹了Android使用AndroidUtilCode實現(xiàn)多語言示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01Activity/Fragment結(jié)束時處理異步回調(diào)的解決方案
這篇文章主要介紹了關(guān)于在Activity/Fragment結(jié)束時處理異步回調(diào)的解決方案,文中介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考價值,需要的朋友們下面來一起看看吧。2017-03-03