Android View 绘制流程之三:draw绘制
Android View 绘制流程之三:draw绘制
- 一.draw方法的整体流程
- 二.drawBackground实现
- 三.onDraw
- 四.dispatchDraw的实现
- 五.ScrollBar的绘制
系列文章:
Android View 绘制流程之一:measure测量
Android View 绘制流程之二:layout布局
Android View 绘制流程之三:draw绘制
Android View 绘制流程之四:绘制流程触发机制
draw()方法是View系统测绘流程的最后一步,就是绘制,当view测量完大小、确定完位置后,就需要在其位置处绘制出其内容等视觉上的东西;View的draw方法有固定的流程,一般ViewGroup需要在dispatchDraw方法中完成对子view的绘制,View需要在onDraw方法里完成对自己内容的绘制。
一.draw方法的整体流程
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总是从"原点"开始的,所以只需照常绘制即可;那么这点是怎么实现的,下面的各个流程会解释到:
二.drawBackground实现
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
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的实现
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
五.ScrollBar的绘制
由源码可知,滚动条的绘制属于前景绘制,在最后一步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时内部使用的。
更多相关文章
- Android 控件(button)对齐方法实现详解
- Android Studio更新升级方法
- android 实现 APP 保活且正常升级的方法
- android 电池(二):android关机充电流程、充电画面显示
- Android保存数据几种常用方法解析
- Android OTA升级原理和流程分析(一)--update.zip包的制作
- Android 中插件的编写方法
- Android shape方法绘制图形的方法和属性解析
- Flash Android ANE打包之基本流程