概述:
RecyclerView原生的API中,是不支持可以拖动的scrollBar的。它自己带有一个仅供显示的scrollBar,并不能拖动。同时这个原生的scrollBar在item数量比较少的时候,会出现滑块滑不到底的问题。基于上述问题,我重新自己定制了RecyclerView的滑动条效果,一种是可以联动拖动的,一种是仅显示的。
以下是实现的思路和防坑事项:
1、可以联动拖动的scrollBar,其实是一个自定义的竖向的SeekBar,我们且叫它verticalSeekBar。这个竖向的SeekBar在我之前的博客有介绍过。
2、首先监听RecyclerView的滚动事件,根据滚动的进度来设置scrollBar的进度。同时还要监听verticalSeekBar的滑动事件,根据滑动的进度去设置RecyclerView的内容位置。
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
Log.i(TAG,"newState : " + newState);
//这里是为了处理数据很多的时候,列表滚动需要时间,不加控制,会让滑动条也会再次跟着一起跳动
if(newState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL
|| newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE){
scrollFlag = newState;
}
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if(scrollFlag != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL){
return;
}
int range = 0;
int temp = recyclerView.computeVerticalScrollRange();//整体的高度,注意是整体,包括在显示区域之外的 会动态变化
if (temp > range) {
range = temp;
}
int offset = recyclerView.computeVerticalScrollOffset();//已经向下滚动的距离,为0时表示已处于顶部。
int extent = recyclerView.computeVerticalScrollExtent();//RecycleView显示区域的高度。
float proportion = 1 - (float) (offset * 1.0 / (range - extent));//滑动比例
Log.i(TAG,"proportion "+proportion);
verticalSeekBar.setProgress((int) (proportion*100));
}
});
verticalSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
Log.i("VerticalSeekBar","progress:" + i + "; b:" +b);
if(verticalSeekBar.ismIsDragging()){ //防止非手动滑动导致的进度滚动。由于是自定义的SeekBar,这里的布尔值变量始终是false。
int posiotn = (int) (adapter.getItemCount() * (100 -i)/100.0f);
int scrollSize = Math.abs(posiotn - lastpostion);
Log.i(TAG,"scroll size:" + Math.abs(posiotn - lastpostion));
lastpostion = posiotn;
//同样是为了处理滑动项比较多的情况
if(scrollSize > 50){
linearLayoutManager.setTimeElement(500);
}else {
linearLayoutManager.setTimeElement(2000);
}
recyclerView.smoothScrollToPosition(posiotn);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
Log.i("VerticalSeekBar","onStartTrackingTouch");
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
Log.i("VerticalSeekBar","onStopTrackingTouch");
}
});
3、从上面的代码段中,还涉及到一个自定义的LinearLayoutManager,这是由于我们对RecyclerView采用了smoothScrollToPosition的方法,这是一个平滑的过程。但是当数据太大的时候,平滑会比较耗时。所以我们这里需要修改一下滚动的时间。其中最关键的就是LinearLayoutManager中的一个LinearSmoothScroller类,它其中一个方法calculateTimeForScrolling(int dx)就是计算滑动这些dx需要的时间。我们可以修改如下:
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
LinearSmoothScroller linearSmoothScroller =
new LinearSmoothScroller(recyclerView.getContext()) {
@Override
protected int calculateTimeForScrolling(int dx) {
Log.w("smoothScrollToPosition","dx:" + dx);
// 此函数计算滚动dx的距离需要多久,当要滚动的距离很大时,比如说52000,
// 经测试,系统会多次调用此函数,每10000距离调一次,所以总的滚动时间
// 是多次调用此函数返回的时间的和,所以修改每次调用该函数时返回的时间的
// 大小就可以影响滚动需要的总时间
// 这里使用限制dx的值的方式来控制时间
if (dx > timeElement) {
dx = timeElement;
}
return super.calculateTimeForScrolling(dx);
}
};
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}
注意点:
1、 如果想要在代码中去主动设置VerticalSeekBar的Thumb,那么在设置了之后一定要紧接着设置一次ThumbOffset属性。不然就会出现滑块图标在滑动到底部时,被遮挡住一部分显示。
/**
* 设置滑动条的类型
* @param type
*/
public void setScrollThumbType(int type){
Drawable thumb = null;
if(type == THUMB_TYPE_DIAL){
thumb = getResources().getDrawable(R.mipmap.dial_scroll_thumb);
verticalSeekBar.setThumb(thumb);
}else if(type == THUMB_TYPE_CONTACT){
thumb = getResources().getDrawable(R.mipmap.contact_scroll_thumb);
verticalSeekBar.setThumb(thumb);
}
verticalSeekBar.setThumbOffset(0);
}
2、在XML中引用VerticalSeekBar的时候,一定要设置它的背景图,就算是透明的背景,也要去设置一下,不然就会出现非常神奇的事情,在拖动VerticalSeekBar的时候右上脚会出现一个移动的圆点。
gitbub地址:RecyclerView的滑动条效果demo