
前言
最近開發(fā)中遇到了一個(gè)需求,需要RecyclerView滾動(dòng)到指定位置后置頂顯示,當(dāng)時(shí)遇到這個(gè)問題的時(shí)候,心里第一反應(yīng)是直接使用RecyclerView的smoothScrollToPosition()方法,實(shí)現(xiàn)對(duì)應(yīng)位置的平滑滾動(dòng)。但是在實(shí)際使用中發(fā)現(xiàn)并沒有到底自己想要的效果。本想著偷懶直接從網(wǎng)上Copy下,但是發(fā)現(xiàn)效果并不是很好。于是就自己去研究源碼。
該系列文章分為兩篇文章。
- 如果你想解決通過smoothScrollToPosition滾動(dòng)到頂部,或者滾動(dòng)加速,請(qǐng)觀看本篇文章,
- 如果你想了解其內(nèi)部實(shí)現(xiàn),請(qǐng)看RecyclerView.smoothScrollToPosition了解一下
注意?。?!注意?。?!注意!?。?br> 這是使用的LinearLayoutManager且是豎直方向上的,橫向的思路是一樣的,只是修改的方法不一樣,大家一定要注意前提條件。
如何使用smoothScrollToPosition滾動(dòng)到頂部?
如果你看了我的另一篇文章RecyclerView.smoothScrollToPosition了解一下,大家應(yīng)該會(huì)清楚,其實(shí)在你設(shè)定目標(biāo)位置后,當(dāng)找到目標(biāo)視圖后,最后讓RecyclerView進(jìn)行滾動(dòng)的方法是其對(duì)應(yīng)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默認(rèn)為SNAP_ANY,那么我們就可以得到,下面三種情況。
- 當(dāng)滾動(dòng)位置在可見范圍之內(nèi)時(shí)
滾動(dòng)距離為0,故不會(huì)滾動(dòng)。 - 當(dāng)滾動(dòng)位置在可見范圍之前時(shí)
內(nèi)容向上滾動(dòng)且只能滾動(dòng)到頂部。 - 當(dāng)滾動(dòng)位置在可見范圍距離之外時(shí)
內(nèi)容向下滾動(dòng),且只能滾動(dòng)到底部。
同時(shí)snapPreference的值是通過LinearSmoothScroller中的getVerticalSnapPreference()與getHorizontalSnapPreference() 來設(shè)定的。
所以為了使?jié)L動(dòng)位置對(duì)應(yīng)的目標(biāo)視圖在頂部顯示,那么我們創(chuàng)建一個(gè)新類并繼承LinearLayoutManager。同時(shí)創(chuàng)建TopSnappedSmoothScroller繼承LinearSmoothScroller,并重寫它的getVerticalSnapPreference()方法就行了。(如果你是橫向的,請(qǐng)修改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;//設(shè)置滾動(dòng)位置
}
}
}
創(chuàng)建該類后,我們接下來就只用給RecyclerView設(shè)置對(duì)應(yīng)的新的布局管理器,并調(diào)用smoothScrollToPosition()方法就行了。
如何設(shè)置smoothScrollToPosition滾動(dòng)的速度?
其實(shí)在RecyclerView中,滾動(dòng)到指定位置是分為了兩個(gè)部分,第一個(gè)是沒有找到目標(biāo)位置對(duì)應(yīng)的視圖之前的速度,一種是找到目標(biāo)位置對(duì)應(yīng)的視圖之后滾動(dòng)的速度。
沒有找到目標(biāo)位置之前
action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO),
(int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO),
(int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
在開始尋找目標(biāo)位置時(shí),默認(rèn)的開始距離是12000(單位:px),且這里大家注意,我們使用了LinearInterpolator,也就是說在沒有找到目標(biāo)位置之前,我們的RecyclerView速度是恒定的。
找到目標(biāo)位置之后
action.update(-dx, -dy, time, mDecelerateInterpolator);
這里我們使用了DecelerateInterpolator。也就是說,找到目標(biāo)位置之后,RecyclerView是速度是慢慢減小。
所以現(xiàn)在就提供了一個(gè)思路,我們可以去修改兩個(gè)部分的插值器,來改變RecyclerView的滾動(dòng)速度,當(dāng)然我這里并沒有給實(shí)例代碼,因?yàn)槲野l(fā)現(xiàn)Google并沒有想讓我們?nèi)バ薷牟逯灯鞯南敕?,因?yàn)樵谄銵inearSmoothScroller中,他直接把兩個(gè)插值器用protected修飾。(所以我覺得這樣改,感覺不優(yōu)雅)如果有興趣的小伙伴,可以去修改。
那現(xiàn)在怎么修改速度呢?
既然以修改插值器的方式比較麻煩,那么我們可以修改滾動(dòng)時(shí)間啊!!!!!!希望大家還記得,我們?cè)谡{(diào)用Action的update方法時(shí),我們不僅保存了RecyclerView需要滾動(dòng)的距離,我們還保存了滑動(dòng)總共需要的時(shí)間。
滑動(dòng)所需要的時(shí)間是通過calculateTimeForScrolling()這個(gè)方法來進(jìn)行計(jì)算的。
protected int calculateTimeForScrolling(int dx) {
//這里對(duì)時(shí)間進(jìn)行了四舍五入操作。
return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
}
其中MILLISECONDS_PER_PX 會(huì)在LinearSmoothScroller初始化的時(shí)候創(chuàng)建。
public LinearSmoothScroller(Context context) {
MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
}
查看calculateSpeedPerPixel()方法
private static final float MILLISECONDS_PER_INCH = 25f;// 默認(rèn)為移動(dòng)一英寸需要花費(fèi)25ms
//
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
也就是說,當(dāng)前滾動(dòng)的速度是與屏幕的像素密度相關(guān), 通過獲取當(dāng)前手機(jī)屏幕每英寸的像素密度,與每英寸移動(dòng)所需要花費(fèi)的時(shí)間,用每英寸移動(dòng)所需要花費(fèi)的時(shí)間除以像素密度就能計(jì)算出移動(dòng)一個(gè)像素密度需要花費(fèi)的時(shí)間。
那么現(xiàn)在,就可以通過兩個(gè)方法來修改RecyclerView的滾動(dòng)速度,要么我們修改calculateSpeedPerPixel方法修改移動(dòng)一個(gè)像素需要花費(fèi)的時(shí)間。要么我們修改calculateTimeForScrolling方法。
這里我采用修改calculateSpeedPerPixel方法來改變速度。這里我修改移動(dòng)一英寸需要花費(fèi)為10ms,那代表著滾動(dòng)速度加快了。那么對(duì)應(yīng)的滾動(dòng)時(shí)間就變小了
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return 10f / displayMetrics.densityDpi;
}
到了這里我相信大家已經(jīng)明白了,怎么去修改速度與滾動(dòng)位置了。好啦好啦,先睡了太困了。
對(duì)了對(duì)了,源碼在這里。大家如果有興趣,可以去研究一下。
最后
最后,附上我寫的一個(gè)基于Kotlin 仿開眼的項(xiàng)目SimpleEyes(ps: 其實(shí)在我之前,已經(jīng)有很多小朋友開始仿這款應(yīng)用了,但是我覺得要做就做好。所以我的項(xiàng)目和其他的人應(yīng)該不同,不僅僅是簡單的一個(gè)應(yīng)用。但是,但是。但是。重要的話說三遍。還在開發(fā)階段,不要打我),歡迎大家follow和start.