前言
最近開發中遇到了一個需求,需要RecyclerView滾動到指定位置後置頂顯示,當時遇到這個問題的時候,心裡第一反應是直接使用RecyclerView的smoothScrollToPosition()方法,實現對應位置的平滑滾動。但是在實際使用中發現並沒有到底自己想要的效果。本想著偷懶直接從網上Copy下,但是發現效果並不是很好。於是就自己去研究原始碼。
該系列文章分為兩篇文章。
- 如果你想解決通過smoothScrollToPosition滾動到頂部,或者滾動加速,請觀看本篇文章,
- 如果你想了解其內部實現,請看RecyclerView.smoothScrollToPosition瞭解一下
注意!!!注意!!!注意!!! 這是使用的LinearLayoutManager且是豎直方向上的,橫向的思路是一樣的,只是修改的方法不一樣,大家一定要注意前提條件。
如何使用smoothScrollToPosition滾動到頂部?
如果你看了我的另一篇文章RecyclerView.smoothScrollToPosition瞭解一下,大家應該會清楚,其實在你設定目標位置後,當找到目標檢視後,最後讓RecyclerView進行滾動的方法是其對應LinearLayoutManager中的LinearSmoothScroller的calculateDtToFit()方法。
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
snapPreference) {
switch (snapPreference) {
case SNAP_TO_START:
return boxStart - viewStart;
case SNAP_TO_END:
return boxEnd - viewEnd;
case SNAP_TO_ANY:
final int dtStart = boxStart - viewStart;
if (dtStart > 0) {
return dtStart;
}
final int dtEnd = boxEnd - viewEnd;
if (dtEnd < 0) {
return dtEnd;
}
break;
default:
throw new IllegalArgumentException("snap preference should be one of the"
+ " constants defined in SmoothScroller, starting with SNAP_");
}
return 0;
}
複製程式碼
也就是說在LinerlayoutManager為豎直的情況下,snapPreference預設為SNAP_ANY,那麼我們就可以得到,下面三種情況。
- 當滾動位置在可見範圍之內時 滾動距離為0,故不會滾動。
- 當滾動位置在可見範圍之前時 內容向上滾動且只能滾動到頂部。
- 當滾動位置在可見範圍距離之外時 內容向下滾動,且只能滾動到底部。
同時snapPreference的值是通過LinearSmoothScroller中的getVerticalSnapPreference()與getHorizontalSnapPreference() 來設定的。
所以為了使滾動位置對應的目標檢視在頂部顯示,那麼我們建立一個新類並繼承LinearLayoutManager。同時建立TopSnappedSmoothScroller繼承LinearSmoothScroller,並重寫它的getVerticalSnapPreference()方法就行了。(如果你是橫向的,請修改getHorizontalSnapPreference方法)
public class LinearLayoutManagerWithScrollTop extends LinearLayoutManager {
public LinearLayoutManagerWithScrollTop(Context context) {
super(context);
}
public LinearLayoutManagerWithScrollTop(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public LinearLayoutManagerWithScrollTop(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
TopSnappedSmoothScroller topSnappedSmoothScroller = new TopSnappedSmoothScroller(recyclerView.getContext());
topSnappedSmoothScroller.setTargetPosition(position);
startSmoothScroll(topSnappedSmoothScroller);
}
class TopSnappedSmoothScroller extends LinearSmoothScroller {
public TopSnappedSmoothScroller(Context context) {
super(context);
}
@Nullable
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return LinearLayoutManagerWithScrollTop.this.computeScrollVectorForPosition(targetPosition);
}
@Override
protected int getVerticalSnapPreference() {
return SNAP_TO_START;//設定滾動位置
}
}
}
複製程式碼
建立該類後,我們接下來就只用給RecyclerView設定對應的新的佈局管理器,並呼叫smoothScrollToPosition()方法就行了。
如何設定smoothScrollToPosition滾動的速度?
其實在RecyclerView中,滾動到指定位置是分為了兩個部分,第一個是沒有找到目標位置對應的檢視之前的速度,一種是找到目標位置對應的檢視之後滾動的速度。
沒有找到目標位置之前
action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO),
(int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO),
(int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
複製程式碼
在開始尋找目標位置時,預設的開始距離是12000(單位:px),且這裡大家注意,我們使用了LinearInterpolator,也就是說在沒有找到目標位置之前,我們的RecyclerView速度是恆定的。
找到目標位置之後
action.update(-dx, -dy, time, mDecelerateInterpolator);
複製程式碼
這裡我們使用了DecelerateInterpolator。也就是說,找到目標位置之後,RecyclerView是速度是慢慢減小。
所以現在就提供了一個思路,我們可以去修改兩個部分的插值器,來改變RecyclerView的滾動速度,當然我這裡並沒有給例項程式碼,因為我發現Google並沒有想讓我們去修改插值器的想法,因為在其LinearSmoothScroller中,他直接把兩個插值器用protected修飾。(所以我覺得這樣改,感覺不優雅)如果有興趣的小夥伴,可以去修改。
那現在怎麼修改速度呢?
既然以修改插值器的方式比較麻煩,那麼我們可以修改滾動時間啊!!!!!!希望大家還記得,我們在呼叫Action的update方法時,我們不僅儲存了RecyclerView需要滾動的距離,我們還儲存了滑動總共需要的時間。
滑動所需要的時間是通過calculateTimeForScrolling()這個方法來進行計算的。
protected int calculateTimeForScrolling(int dx) {
//這裡對時間進行了四捨五入操作。
return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
}
複製程式碼
其中MILLISECONDS_PER_PX 會在LinearSmoothScroller初始化的時候建立。
public LinearSmoothScroller(Context context) {
MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
}
複製程式碼
檢視calculateSpeedPerPixel()方法
private static final float MILLISECONDS_PER_INCH = 25f;// 預設為移動一英寸需要花費25ms
//
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
複製程式碼
也就是說,當前滾動的速度是與螢幕的畫素密度相關, 通過獲取當前手機螢幕每英寸的畫素密度,與每英寸移動所需要花費的時間,用每英寸移動所需要花費的時間除以畫素密度就能計算出移動一個畫素密度需要花費的時間。
那麼現在,就可以通過兩個方法來修改RecyclerView的滾動速度,要麼我們修改calculateSpeedPerPixel方法修改移動一個畫素需要花費的時間。要麼我們修改calculateTimeForScrolling方法。
這裡我採用修改calculateSpeedPerPixel方法來改變速度。這裡我修改移動一英寸需要花費為10ms,那代表著滾動速度加快了。那麼對應的滾動時間就變小了
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return 10f / displayMetrics.densityDpi;
}
複製程式碼
到了這裡我相信大家已經明白了,怎麼去修改速度與滾動位置了。好啦好啦,先睡了太困了。
對了對了,原始碼在這裡。大家如果有興趣,可以去研究一下。
最後
最後,附上我寫的一個基於Kotlin 仿開眼的專案SimpleEyes(ps: 其實在我之前,已經有很多小朋友開始仿這款應用了,但是我覺得要做就做好。所以我的專案和其他的人應該不同,不僅僅是簡單的一個應用。但是,但是。但是。重要的話說三遍。還在開發階段,不要打我),歡迎大家follow和start.