系列文章:
Android View 绘制流程之一:measure测量
Android View 绘制流程之二:layout布局
Android View 绘制流程之三:draw绘制
Android View 绘制流程之四:绘制流程触发机制
draw()方法是View系统测绘流程的最后一步,就是绘制,当view测量完大小、确定完位置后,就需要在其位置处绘制出其内容等视觉上的东西;View的draw方法有固定的流程,一般ViewGroup需要在dispatchDraw方法中完成对子view的绘制,View需要在onDraw方法里完成对自己内容的绘制。
ViewRootImpl里调用performDraw()方法,该方法里最终会调用根view的draw方法开始整个view的绘制
View的draw(Canvas canvas)方法主要流程已经规定好:
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);//如果view的DIRTY标志是OPAQUE,说明其child的该区域不透明,就不用绘制这块的背景和内容了
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;//标志DRAWN,已经绘制
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {//有透明的需要绘制背景
drawBackground(canvas);
}
// 通常情况是可以省略2、5两步
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, 绘制自身内容
if (!dirtyOpaque) onDraw(canvas);
// Step 4, ViewGroup需要重写该方法实现子view的绘制
dispatchDraw(canvas);
// ...
// Step 6, 绘制一些前景,比如前景图、ScrollBar等
onDrawForeground(canvas);
// we're done...
return;
}
...
//正常的全部流程
// Step 2, 用canvas.saveLayers的方法保存渐变边的图层
...
// Step 3, 4
// Step 5, 绘制图层
...
canvas.restoreToCount(saveCount);//恢复canvas状态
// Step 6
...
}
流程很清晰,大部分view就是先绘制必要的背景,然后调用onDraw绘制自身内容,绘制完后如果是ViewGroup,还会调用dispatchDraw绘制子view,最后绘制前景一些UI即可;对于一些特殊view,只是多加了绘制渐变框的一步(使用保存图层、绘制图层、恢复画布的做法)
下面就来看看这几个流程的具体实现,这里有一点需要注意,整个view调用draw时的参数,是一个Canvas对象,是ViewRootImpl里创建的SurfaceView的画布,对于整个屏幕来说,canvas的起始点就是0,0,但是对于每个View来说,为了使view在使用canvas绘制时的处理方便,每次在调用view的draw方法前,都会用平移的方法将canvas的其实点移动到view的起始点,这样对于view自己来说,canvas总是从"原点"开始的,所以只需照常绘制即可;那么这点是怎么实现的,下面的各个流程会解释到:
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();//设置drawable边界
// 硬件加速绘制
...
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {//如果有滚动偏移量,则要将canvas原点移动,然后交由drawable绘制,还要再将原点移动回来
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
由代码可知,如果有view有偏移量,比如向上滚动了10px,则mScrollY=10(即新的内容原点就是原来的y=10处),那么传递给view的canvas的起始点就是(0,-10),而background是针对于view的,不是view内部内容的,不应该跟随着内容的滚动而滚动,所以canvas需要移动(0,10)到(0,0),再去绘制背景drawable,之后再复原到(0,-10),用于view内容的绘制;且无论是否硬件加速绘制,都会有相同的处理
onDraw(canvas)方法是view绘制自己内容的方法,也是我们自定义view最常用的方法,我们可以使用canvas绘制任意元素,而不需要考虑滚动,因为传递过来的canvas对于view来说就是从(0,0)开始的,下面拿ImageView来举例:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...
if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
mDrawable.draw(canvas);//直接绘制在canvas上即可
} else {
int saveCount = canvas.getSaveCount();//保存画布状态
canvas.save();
...
canvas.translate(mPaddingLeft, mPaddingTop);//平移
if (mDrawMatrix != null) {//矩阵变换
canvas.concat(mDrawMatrix);
}
mDrawable.draw(canvas);//绘制在canvas上
canvas.restoreToCount(saveCount);//恢复画布状态
}
}
有代码可知,onDraw里基本就是使用canvas绘制一些元素即可,其canvas自身也提供了许多绘制元素方法,这里不在赘述。
dispatchDraw(canvas)方法是ViewGroup需要重写,进行子view绘制的方法,上面已经说过view绘制流程,那么可得知该方法是在自身onDraw方法调用后执行,也就是说view先绘制自身内容,再绘制子view而ViewGroup其实已经实现了dispatchDraw方法,而大部分ViewGroup绘制子view的流程都是如此,所以大部分的ViewGroup子类直接调用该方法即可,无需再处理,下面就来看看这个方法:
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
//如果ViewGroup有布局动画
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
final boolean buildCache = !isHardwareAccelerated();
for (int i = 0; i < childrenCount; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
final LayoutParams params = child.getLayoutParams();
attachLayoutAnimationParameters(child, params, i, childrenCount);
bindLayoutAnimation(child);//给每个child绑定该动画
}
}
final LayoutAnimationController controller = mLayoutAnimationController;
if (controller.willOverlap()) {
mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
}
controller.start();//开始动画
mGroupFlags &= ~FLAG_RUN_ANIMATION;
mGroupFlags &= ~FLAG_ANIMATION_DONE;
if (mAnimationListener != null) {//回调布局动画开始监听
mAnimationListener.onAnimationStart(controller.getAnimation());
}
}
//这步比较重要,如果有padding且需要裁剪时,那么需要将canvas裁剪(考虑padding和滚动),裁剪后被裁剪的区域不会绘制内容
//注意,clipRect只是裁剪,并不是将canvas的原点移动了
int clipSaveCount = 0;
final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
if (clipToPadding) {
clipSaveCount = canvas.save();
canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
mScrollX + mRight - mLeft - mPaddingRight,
mScrollY + mBottom - mTop - mPaddingBottom);
}
// We will draw our child's animation, let's reset the flag
mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
boolean more = false;
final long drawingTime = getDrawingTime();
...
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();//是否是自定义绘制顺序
for (int i = 0; i < childrenCount; i++) {
...
int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;//拿到实际绘制顺序的child的index,默认为按顺序绘制,可以通过重写getChildDrawingOrder方法进行自定义
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {//child可见或者有动画时需要绘制
more |= drawChild(canvas, child, drawingTime);//调用View的一个draw方法绘制,该方法下面会细说
}
}
...
// 被移除的view(removeView)会从ViewGroup中移除,不再接受任何响应,但是如果有移除动画的话,会加入到disappearingChildren中执行动画(因为也会显示在界面上),动画执行完后移除即可
if (mDisappearingChildren != null) {
final ArrayList<View> disappearingChildren = mDisappearingChildren;
final int disappearingCount = disappearingChildren.size() - 1;
// Go backwards -- we may delete as animations finish
for (int i = disappearingCount; i >= 0; i--) {
final View child = disappearingChildren.get(i);
more |= drawChild(canvas, child, drawingTime);
}
}
...
if (clipToPadding) {
canvas.restoreToCount(clipSaveCount);
}
// mGroupFlags might have been updated by drawChild()
flags = mGroupFlags;
//如果在绘制子view的过程中又需要重绘(有该标志位),则继续调用invalidate
if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
invalidate(true);
}
//布局动画完成,通知回调
if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&
mLayoutAnimationController.isDone() && !more) {
mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
final Runnable end = new Runnable() {
public void run() {
notifyAnimationListener();
}
};
post(end);
}
}
大致流程就是为子view加入可能有的布局动画,剪切canvas,按指定顺序遍历子view并调用drawChild方法绘制子view,恢复canvas状态,最后看是否需要继续绘制以及布局动画是否完成;可见该方法主要是管理了布局动画的执行以及子view的绘制调用,具体的子view绘制方法就是view的一个draw方法,下面来看看这个方法:
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
...
//获取view的变换矩阵,一种是动画设置的,一种是静态回调方法设置的
Transformation transformToApply = null;
boolean concatMatrix = false;
final boolean scalingRequired = mAttachInfo != null && mAttachInfo.mScalingRequired;
final Animation a = getAnimation();
if (a != null) {//从动画中获取变换矩阵
more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
concatMatrix = a.willChangeTransformationMatrix();
if (concatMatrix) {
mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
}
transformToApply = parent.getChildTransformation();
} else {//从静态回调方法获取矩阵
if ((mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_TRANSFORM) != 0) {
// No longer animating: clear out old animation matrix
mRenderNode.setAnimationMatrix(null);
mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
}
if (!drawingWithRenderNode
&& (parentFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
final Transformation t = parent.getChildTransformation();
final boolean hasTransform = parent.getChildStaticTransformation(this, t);
if (hasTransform) {
final int transformType = t.getTransformationType();
transformToApply = transformType != Transformation.TYPE_IDENTITY ? t : null;
concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
}
}
}
concatMatrix |= !childHasIdentityMatrix;
// Sets the flag as early as possible to allow draw() implementations
// to call invalidate() successfully when doing animations
mPrivateFlags |= PFLAG_DRAWN;
//如果没有变换矩阵且没有动画且在canvas剪切去以外,那么就不不用绘制了
if (!concatMatrix &&
(parentFlags & (ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS |
ViewGroup.FLAG_CLIP_CHILDREN)) == ViewGroup.FLAG_CLIP_CHILDREN &&
canvas.quickReject(mLeft, mTop, mRight, mBottom, Canvas.EdgeType.BW) &&//该方法判断child区域是否在canvas剪切区以外了
(mPrivateFlags & PFLAG_DRAW_ANIMATION) == 0) {
mPrivateFlags2 |= PFLAG2_VIEW_QUICK_REJECTED;
return more;
}
mPrivateFlags2 &= ~PFLAG2_VIEW_QUICK_REJECTED;
...
int sx = 0;
int sy = 0;
if (!drawingWithRenderNode) {
computeScroll();//该方法是view内部的一个可被重写的方法,我们平时使用的Scroller对象,会结合这个方法使用,下面再单独介绍,这里只需要知道是计算并处理滚动量的即可
//获取最新的滚动量
sx = mScrollX;
sy = mScrollY;
}
final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
final boolean offsetForScroll = cache == null && !drawingWithRenderNode;
int restoreTo = -1;
if (!drawingWithRenderNode || transformToApply != null) {
restoreTo = canvas.save();
}
//接下来这步就是最重要的canvas位移原点的重要操作,使得对于view来说canvas的原点始终在view的原点上,直接绘制即可,而不需要考虑view的滚动
if (offsetForScroll) {//当前canvas原点还是处于ViewGroup的原点,尽管之前已经clipRect剪切过,但是并没有移动原点;所以,相对于view的原点就需要将canvas原点平移(mLeft,mTop)即可,并且要考虑到滚动量,则就要再平移(-sx,-sy)
canvas.translate(mLeft - sx, mTop - sy);
} else {
if (!drawingWithRenderNode) {
canvas.translate(mLeft, mTop);
}
if (scalingRequired) {
if (drawingWithRenderNode) {
// TODO: Might not need this if we put everything inside the DL
restoreTo = canvas.save();
}
// mAttachInfo cannot be null, otherwise scalingRequired == false
final float scale = 1.0f / mAttachInfo.mApplicationScale;
canvas.scale(scale, scale);
}
}
//接下来就是应用变换矩阵以及透明度等信息
float alpha = drawingWithRenderNode ? 1 : (getAlpha() * getTransitionAlpha());
if (transformToApply != null
|| alpha < 1
|| !hasIdentityMatrix()
|| (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
if (transformToApply != null || !childHasIdentityMatrix) {
int transX = 0;
int transY = 0;
if (offsetForScroll) {
transX = -sx;
transY = -sy;
}
if (transformToApply != null) {
if (concatMatrix) {
if (drawingWithRenderNode) {
renderNode.setAnimationMatrix(transformToApply.getMatrix());
} else {
// Undo the scroll translation, apply the transformation matrix,
// then redo the scroll translate to get the correct result.
canvas.translate(-transX, -transY);
canvas.concat(transformToApply.getMatrix());//应用变换矩阵
canvas.translate(transX, transY);
}
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}
float transformAlpha = transformToApply.getAlpha();
if (transformAlpha < 1) {
alpha *= transformAlpha;
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}
}
if (!childHasIdentityMatrix && !drawingWithRenderNode) {
canvas.translate(-transX, -transY);
canvas.concat(getMatrix());
canvas.translate(transX, transY);
}
}
// 使用保存图层的方式设置透明度
if (alpha < 1 || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
if (alpha < 1) {
mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_ALPHA;
} else {
mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_ALPHA;
}
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
if (!drawingWithDrawingCache) {
final int multipliedAlpha = (int) (255 * alpha);
if (!onSetAlpha(multipliedAlpha)) {//应用透明度
if (drawingWithRenderNode) {
renderNode.setAlpha(alpha * getAlpha() * getTransitionAlpha());
} else if (layerType == LAYER_TYPE_NONE) {
canvas.saveLayerAlpha(sx, sy, sx + getWidth(), sy + getHeight(),
multipliedAlpha);
}
} else {
// Alpha is handled by the child directly, clobber the layer's alpha
mPrivateFlags |= PFLAG_ALPHA_SET;
}
}
}
} else if ((mPrivateFlags & PFLAG_ALPHA_SET) == PFLAG_ALPHA_SET) {//没有设置透明度则直接设置为255(不透明)
onSetAlpha(255);
mPrivateFlags &= ~PFLAG_ALPHA_SET;
}
//剪切canvas
if (!drawingWithRenderNode) {
// apply clips directly, since RenderNode won't do it for this draw
if ((parentFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0 && cache == null) {
if (offsetForScroll) {
canvas.clipRect(sx, sy, sx + getWidth(), sy + getHeight());//前面以及移动了canvas原点,此处是剪切canvas区域,使得剪切的区域绘制内容
} else {...}
}
if (!drawingWithDrawingCache) {
if (drawingWithRenderNode) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
} else {
//有SKIP_DRAW标志说明该view可跳过绘制,则直接调用dispatchDraw绘制其子view即可(该属性一般view默认没有,一般ViewGroup会通过设置WILL_NOT_DRAW来设置该属性,因为一般ViewGroup不需要绘制自身内容,有背景前景除外)
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {//正常绘制child
draw(canvas);
}
}
} else if (cache != null) {
...
}
if (restoreTo >= 0) {//恢复画布状态
canvas.restoreToCount(restoreTo);
}
if (a != null && !more) {//有动画且无需再绘制,说明该view动画已经绘制完毕
if (!hardwareAcceleratedCanvas && !a.getFillAfter()) {
onSetAlpha(255);
}
parent.finishAnimatingView(this, a);//回调通知动画结束,清空动画,且从disappearing数组里移除该view等操作
}
...
return more;
}
由代码可知,基本流程就是,获取view的变化矩阵和透明度,根据滚动量位移canvas原点,然后应用变化矩阵和透明度,在位置和边框都确定后,剪切canvas,调用child的draw方法进行绘制即可;
如图一,一个ViewGroup左上角有一个TextView,当ViewGroup调用drawChild时,也就是调用textView.draw时,canvas的原点就是相对于ViewGroup的(0,0),剪切区也就是整个ViewGroup的区域
当TextView的内容滚动时,比如我们调用textView.scrollTo(0,10),即内容向上滚动了10px,这是mScrollY=10,由代码可知,在textView.draw时,移动canvas原点(0,-10),到了如图二的最左上角的坐标,这样交给textView的onDraw方法的canvas就是从图二左上角为原点的画布,我们一般只管在canvas里从原点绘制内容,不用考虑滚动,那么画出的效果就是如图二所示,这也就是为什么我们不用在onDraw里考虑滚动的原因
那么如图二所示,上面多出来的部分又不应该显示怎么办呢,就需要剪切canvas到textView的区域,有代码可知,相当于会调用canvas.clipRect(0,mScrollY,getWidth,mScrollY+getHeight),这样剪切出来的区域正好是如图三所示的TextView的区域,只有这部分区域会绘制内容;其实就是在剪切时,将滚动量的部分剪切掉即可
以上只是考虑滚动量,textView的mLeft和mTop没有考虑(上图其实都为0,不用考虑),考虑的话也是一样的
setWillNotDraw()方法设置view的WILL_NOT_DRAW标志,如上面所说,如果有该标志且背景前景不为null那么就会给view设置SKIP_DRAW标识,绘制时就直接调用其dispatchDraw方法,而不是draw方法;ViewGroup在init方法时会默认设置该方法为true;如果自定义view需要onDraw里绘制内容时就不能设置该方法为true
由源码可知,滚动条的绘制属于前景绘制,在最后一步onDrawForegroung里调用onDrawScrollBars执行:
protected final void onDrawScrollBars(Canvas canvas) {
// scrollbars are drawn only when the animation is running
final ScrollabilityCache cache = mScrollCache;//该对象保存这绘制的ScrollBarDrawable
if (cache != null) {
int state = cache.state;//滚动条状态
if (state == ScrollabilityCache.OFF) {//OFF为隐藏态,不再绘制
return;
}
boolean invalidate = false;//是否需要继续刷新
if (state == ScrollabilityCache.FADING) {//FADING状态为正在逐渐消失(透明度)
// We're fading -- get our fade interpolation
if (cache.interpolatorValues == null) {
cache.interpolatorValues = new float[1];
}
float[] values = cache.interpolatorValues;
// Stops the animation if we're done
if (cache.scrollBarInterpolator.timeToValues(values) ==
Interpolator.Result.FREEZE_END) {
cache.state = ScrollabilityCache.OFF;//完成消失过程,状态置为OFF
} else {
cache.scrollBar.mutate().setAlpha(Math.round(values[0]));//设置drawable透明度
}
// 将重新绘制标志位置为true,因为透明度未绘制完毕需要继续绘制
invalidate = true;
} else {//无需渐变,直接将透明度置为255
cache.scrollBar.mutate().setAlpha(255);
}
//拿到是否有水平或垂直的滚动条
final int viewFlags = mViewFlags;
final boolean drawHorizontalScrollBar =
(viewFlags & SCROLLBARS_HORIZONTAL) == SCROLLBARS_HORIZONTAL;
final boolean drawVerticalScrollBar =
(viewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL
&& !isVerticalScrollBarHidden();
//需要绘制滚动条
if (drawVerticalScrollBar || drawHorizontalScrollBar) {
...
//...绘制水平滚动条
//绘制垂直滚动条
if (drawVerticalScrollBar) {
int size = scrollBar.getSize(true);//获取drawable的尺寸
if (size <= 0) {//没有才会取得自定义的size
size = cache.scrollBarSize;
}
//可以通过重写三个方法决定range、offset、extent的值,用处下面会解释
scrollBar.setParameters(computeVerticalScrollRange(),
computeVerticalScrollOffset(),
computeVerticalScrollExtent(), true);
//获取scrollBar的位置
int verticalScrollbarPosition = mVerticalScrollbarPosition;
if (verticalScrollbarPosition == SCROLLBAR_POSITION_DEFAULT) {
verticalScrollbarPosition = isLayoutRtl() ?
SCROLLBAR_POSITION_LEFT : SCROLLBAR_POSITION_RIGHT;
}
//根据位置设置scrollBar的ltrb
switch (verticalScrollbarPosition) {
default:
case SCROLLBAR_POSITION_RIGHT:
left = scrollX + width - size - (mUserPaddingRight & inside);
break;
case SCROLLBAR_POSITION_LEFT:
left = scrollX + (mUserPaddingLeft & inside);
break;
}
top = scrollY + (mPaddingTop & inside);
right = left + size;
bottom = scrollY + height - (mUserPaddingBottom & inside);
//绘制ScrollBarDrawable
onDrawVerticalScrollBar(canvas, scrollBar, left, top, right, bottom);
//如果需要继续绘制则调用invalidate
if (invalidate) {
invalidate(left, top, right, bottom);
}
}
}
}
}
大体流程就是拿到ScrollBar的对象,设置一些属性值,根据其方向计算出绘制范围,然后交由ScrollBarDrawable对象进行绘制即可,下面来看看ScrollBarDrawable的draw方法:
先来解释几个变量的意义:
range:滚动的范围,可根据实际需求来,比如ListView的range就是整个itemCount*系数
offset:滚动的偏移量(已经滚动的范围),也是根据实际需求来,比如ListView的offset就是已经滚动的item的数量(firstVisiblePosition+1)*系数
extent:当前显示的范围,根据实际需求来,比如ListView的extent就是当前ListView显示的itemCount*系数
xxxTrack:滚动条背景部分drawable,有水平和垂直两个
xxxThumb:滚动条部分drawable,有水平和垂直两个
public void draw(Canvas canvas) {
final boolean vertical = mVertical;
final int extent = mExtent;
final int range = mRange;
boolean drawTrack = true;
boolean drawThumb = true;
if (extent <= 0 || range <= extent) {//没有显示范围或者显示了全部的范围则可以不用绘制track和thumb
drawTrack = vertical ? mAlwaysDrawVerticalTrack : mAlwaysDrawHorizontalTrack;//是否还要绘制track
drawThumb = false;//不用绘制thumb
}
final Rect r = getBounds();
if (canvas.quickReject(r.left, r.top, r.right, r.bottom, Canvas.EdgeType.AA)) {//判断一下是否超过了剪切区域
return;
}
if (drawTrack) {
drawTrack(canvas, r, vertical);//绘制track,就是调用setBounds以及draw方法即可
}
if (drawThumb) {//绘制thumb
//根据方向算出滚动条宽高
final int size = vertical ? r.height() : r.width();//View实际尺寸
final int thickness = vertical ? r.width() : r.height();
final int minLength = thickness * 2;//最小的尺寸
int length = Math.round((float) size * extent / range);//按所占比例计算尺寸
if (length < minLength) {//防止过小
length = minLength;
}
int offset = Math.round((float) (size - length) * mOffset / (range - extent));//计算偏移量(当前偏移*(范围剩余尺寸/剩余范围))
if (offset > size - length) {//防止大于剩余尺寸
offset = size - length;
}
drawThumb(canvas, r, offset, length, vertical);//根据偏移量和尺寸绘制Thumb
}
}
总的来说就是获取属性,按比例算出绘制范围和大小进行绘制,而这些属性都是可以设置的,下面总结一些自定义滚动条的方法方便开发,虽然并不经常使用。。。
由代码可知,滚动条会考虑内部滚动量mScrollX、mScrollY的值来实现滚动,也就是说我们可以调用view的scrollTo这些方法来实现滚动并更新滚动条,而ListView实际没有使用这俩个属性,而是自己控制的(也就是重载的那几个方法来自定义滚动量);
但是两者都是使用的awakenScrollBars来更新滚动条:改变其状态,发起请求开始重绘;该方法是protected的,是自定义view时内部使用的。