最近有个需求其滚动条样式类似淘宝频道栏的滚动条,需要修改滚动条的样式,因此对View的scrollbar做了总结。
scrollbar的属性如下:
属性 | 描述 |
---|---|
android:scrollbars | 是否显示滚动条。none:不显示,horizontal:水平方向,vertical:垂直方向 |
android:scrollbarStyle | 滚动条风格和样式。insideOverlay:默认值,表示在padding区域内并且覆盖在在内容上面。insideInset:表示在padding区域内并且插入在view后面。 outsideOverlay:表示在padding区域外并且覆盖在view上;outsideInset:表示在padding区域外并且插入在view后面 。 |
android:scrollbarSize | 设置滚动条的宽度。 |
android:scrollbarThumbHorizontal[Vertical] | 设置水平/垂直方向的滚动条(滑块)样式,@drawable或@color。 |
android:scrollbarTrackHorizontal[Vertical] | 设置水平/垂直方向的滚动条背景样式,@drawable或@color。 |
android:fadeScrollbars | 不滚动时是否隐藏滚动条,默认true |
因为要实现滚动条样式如下图所示,
由于需求是通过RecyclerView实现,因此在RecyclerView中通过设置scrollbarThumbHorizontal和scrollbarTrackHorizontal两项属性实现自定义滚动条样式。
android:scrollbarThumbHorizontal="@drawable/scrollbar_thumb"
android:scrollbarTrackHorizontal="@drawable/scrollbar_track"
滚动条样式scrollbar_thumb.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="20dp"
android:left="170dp">
<shape>
<solid android:color="#FF97AF" />
<corners android:radius="1.5dp" />
</shape>
</item>
</layer-list>
滚动条背景样式scrollbar_track.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="60dp"
android:left="170dp">
<shape>
<solid android:color="#FFEDF2" />
<corners android:radius="1.5dp" />
</shape>
</item>
</layer-list>
通过with和left限制滚动条和背景的大小和位置 效果如下图所示:
效果看起来很理想,然而一滑动发现滚动条会超出背景范围,如下图所示:
很显然滚动条的实际滚动范围并没有改变,接下来需要设置滚动条的实际滚动范围。 通过查看View中代码,方法onDrawScrollBars实现scrollbar的绘制,其中关键代码为:
final ScrollBarDrawable scrollBar = cache.scrollBar;
if (drawHorizontalScrollBar) {
scrollBar.setParameters(computeHorizontalScrollRange(),
computeHorizontalScrollOffset(),
computeHorizontalScrollExtent(), false);
final Rect bounds = cache.mScrollBarBounds;
getHorizontalScrollBarBounds(bounds, null);
onDrawHorizontalScrollBar(canvas, scrollBar, bounds.left, bounds.top,
bounds.right, bounds.bottom);
if (invalidate) {
invalidate(bounds);
}
}
其中getHorizontalScrollBarBounds用于计算bounds的left和right值,关键代码如下:
final int inside = (mViewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0;
final boolean drawVerticalScrollBar = isVerticalScrollBarEnabled() && !isVerticalScrollBarHidden();
final int size = getHorizontalScrollbarHeight();
final int verticalScrollBarGap = drawVerticalScrollBar ? getVerticalScrollbarWidth() : 0;
final int width = mRight - mLeft;
final int height = mBottom - mTop;
bounds.top = mScrollY + height - size - (mUserPaddingBottom & inside);
bounds.left = mScrollX + (mPaddingLeft & inside);
bounds.right = mScrollX + width - (mUserPaddingRight & inside) - verticalScrollBarGap;
bounds.bottom = bounds.top + size;
可见bounds的left和right值是根据View的实际宽度计算的。 之后再通过onDrawHorizontalScrollBar,将bounds值传到scrollBar
protected void onDrawHorizontalScrollBar(Canvas canvas, Drawable scrollBar,
int l, int t, int r, int b) {
scrollBar.setBounds(l, t, r, b);
scrollBar.draw(canvas);
}
可见最终计算srcollbar是在ScrollBarDrawable中实现,ScrollBarDrawable中draw方法实现滚动条和背景的绘制,关键代码如下
final Rect r = getBounds();
if (drawTrack) {
drawTrack(canvas, r, vertical);
}
if (drawThumb) {
final int scrollBarLength = vertical ? r.height() : r.width();
final int thickness = vertical ? r.width() : r.height();
final int thumbLength =
ScrollBarUtils.getThumbLength(scrollBarLength, thickness, extent, range);
final int thumbOffset =
ScrollBarUtils.getThumbOffset(scrollBarLength, thumbLength, extent, range,
mOffset);
drawThumb(canvas, r, thumbOffset, thumbLength, vertical);
}
其中thumbLength和thumbOffset用于控制滚动条滑块的长度和移动位置 ScrollBarUtils中具体代码如下
public static int getThumbLength(int size, int thickness, int extent, int range) {
// Avoid the tiny thumb.
final int minLength = thickness * 2;
int length = Math.round((float) size * extent / range);
if (length < minLength) {
length = minLength;
}
return length;
}
public static int getThumbOffset(int size, int thumbLength, int extent, int range, int offset) {
// Avoid the too-big thumb.
int thumbOffset = Math.round((float) (size - thumbLength) * offset / (range - extent));
if (thumbOffset > size - thumbLength) {
thumbOffset = size - thumbLength;
}
return thumbOffset;
}
其中length是通过size,extent,range三项计算出来,thumbOffset是通过size,thumbLength,offset,range,extent几项计算出来,因此滚动条滑块的滑动范围关键在于这两个方法;通过ScrollBarDrawable中draw方法可知size为bounds的with,因此只要range为View长度,extent为想要设置的滑块宽度,offset为想要的自定义滑块的偏移值,就可以实现改变滚动条滑块滑动范围。而range,offset,extent三项是通过ScrollBarDrawable的setParameters方法设置,即computeHorizontalScrollRange(),computeHorizontalScrollOffset(),computeHorizontalScrollExtent(),因此只要通过这三个compute值就可以进行设置。 而通过查看RecyclerView相关代码
/**
* <p>Compute the horizontal range that the horizontal scrollbar represents.</p>
*
* <p>The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeHorizontalScrollExtent()} and {@link #computeHorizontalScrollOffset()}.</p>
*
* <p>Default implementation returns 0.</p>
*
* <p>If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State)} in your
* LayoutManager.</p>
*
* @return The total horizontal range represented by the vertical scrollbar
* @see RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State)
*/
@Override
public int computeHorizontalScrollRange() {
if (mLayout == null) {
return 0;
}
return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollRange(mState) : 0;
}
可知computeHorizontalScrollRange(),computeHorizontalScrollOffset(),computeHorizontalScrollExtent()三项的值通过LayoutManager计算,通过查看LinearLayoutManager中这三个方法代码逻辑可知,这三个方法是计算所有item的长度,移动范围;由此可知滚动条的移动计算是根据RecyclerView中所有Item项的移动比例计算的,因此只要自定义LinearLayoutManager,并做如下修改即可:
public class CustomLinearLayoutManager extends LinearLayoutManager {
private int viewW;// recyclerView 宽
private int trackW;// 滚动条背景宽
private int thumbW;// 滚动条滑块宽
public ScrollbarLinearLayoutManager(Context context) {
super(context);
}
public ScrollbarLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public ScrollbarLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public void setParams(int viewW, int trackW, int thumbW) {
this.viewW = viewW;
this.trackW = trackW;
this.thumbW = thumbW;
}
@Override
public int computeHorizontalScrollOffset(RecyclerView.State state) {
return isCustomScrollbar() ? (int) ((float) super.computeHorizontalScrollOffset(state) / (super
.computeHorizontalScrollRange(state) - super.computeHorizontalScrollExtent(state)) * (trackW - thumbW)) :
super.computeHorizontalScrollOffset(state);
}
@Override
public int computeHorizontalScrollExtent(RecyclerView.State state) {
return isCustomScrollbar() ? thumbW : super.computeHorizontalScrollExtent(state);
}
@Override
public int computeHorizontalScrollRange(RecyclerView.State state) {
return isCustomScrollbar() ? viewW : super.computeHorizontalScrollRange(state);
}
private boolean isCustomScrollbar() {
return viewW > 0 && trackW > 0 && thumbW > 0 && viewW > trackW && trackW > thumbW;
}
}
即computeHorizontalScrollRange结果为RecyclerView的宽,computeHorizontalScrollExtent为实际滑块的宽,computeHorizontalScrollOffset为super.Offset/(super.Range-super.Extent) 再 乘以 (trackW - thumbW),即实际偏移值,最终效果如下图,滚动条滑块限制在实际背景范围内滑动。