Scrollbar样式

2,679 阅读4分钟

最近有个需求其滚动条样式类似淘宝频道栏的滚动条,需要修改滚动条的样式,因此对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),即实际偏移值,最终效果如下图,滚动条滑块限制在实际背景范围内滑动。