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方法的整体流程

Android View 绘制流程之三:draw绘制_第1张图片
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内容的绘制;且无论是否硬件加速绘制,都会有相同的处理
Android View 绘制流程之三:draw绘制_第2张图片

三.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的实现

Android View 绘制流程之三:draw绘制_第3张图片Android View 绘制流程之三:draw绘制_第4张图片
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方法进行绘制即可;

这里的位移和剪切很重要,举个例子来说:
Android View 绘制流程之三:draw绘制_第5张图片

  1. 如图一,一个ViewGroup左上角有一个TextView,当ViewGroup调用drawChild时,也就是调用textView.draw时,canvas的原点就是相对于ViewGroup的(0,0),剪切区也就是整个ViewGroup的区域

  2. 当TextView的内容滚动时,比如我们调用textView.scrollTo(0,10),即内容向上滚动了10px,这是mScrollY=10,由代码可知,在textView.draw时,移动canvas原点(0,-10),到了如图二的最左上角的坐标,这样交给textView的onDraw方法的canvas就是从图二左上角为原点的画布,我们一般只管在canvas里从原点绘制内容,不用考虑滚动,那么画出的效果就是如图二所示,这也就是为什么我们不用在onDraw里考虑滚动的原因

  3. 那么如图二所示,上面多出来的部分又不应该显示怎么办呢,就需要剪切canvas到textView的区域,有代码可知,相当于会调用canvas.clipRect(0,mScrollY,getWidth,mScrollY+getHeight),这样剪切出来的区域正好是如图三所示的TextView的区域,只有这部分区域会绘制内容;其实就是在剪切时,将滚动量的部分剪切掉即可

  4. 以上只是考虑滚动量,textView的mLeft和mTop没有考虑(上图其实都为0,不用考虑),考虑的话也是一样的

  5. setWillNotDraw()方法设置view的WILL_NOT_DRAW标志,如上面所说,如果有该标志且背景前景不为null那么就会给view设置SKIP_DRAW标识,绘制时就直接调用其dispatchDraw方法,而不是draw方法;ViewGroup在init方法时会默认设置该方法为true;如果自定义view需要onDraw里绘制内容时就不能设置该方法为true

五.ScrollBar的绘制

Android View 绘制流程之三:draw绘制_第6张图片
由源码可知,滚动条的绘制属于前景绘制,在最后一步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方法:

先来解释几个变量的意义:

  1. range:滚动的范围,可根据实际需求来,比如ListView的range就是整个itemCount*系数

  2. offset:滚动的偏移量(已经滚动的范围),也是根据实际需求来,比如ListView的offset就是已经滚动的item的数量(firstVisiblePosition+1)*系数

  3. extent:当前显示的范围,根据实际需求来,比如ListView的extent就是当前ListView显示的itemCount*系数

  4. xxxTrack:滚动条背景部分drawable,有水平和垂直两个

  5. 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    }}

总的来说就是获取属性,按比例算出绘制范围和大小进行绘制,而这些属性都是可以设置的,下面总结一些自定义滚动条的方法方便开发,虽然并不经常使用。。。
Android View 绘制流程之三:draw绘制_第7张图片
由代码可知,滚动条会考虑内部滚动量mScrollX、mScrollY的值来实现滚动,也就是说我们可以调用view的scrollTo这些方法来实现滚动并更新滚动条,而ListView实际没有使用这俩个属性,而是自己控制的(也就是重载的那几个方法来自定义滚动量);

但是两者都是使用的awakenScrollBars来更新滚动条:改变其状态,发起请求开始重绘;该方法是protected的,是自定义view时内部使用的。

更多相关文章

  1. Android 控件(button)对齐方法实现详解
  2. Android Studio更新升级方法
  3. android 实现 APP 保活且正常升级的方法
  4. android 电池(二):android关机充电流程、充电画面显示
  5. Android保存数据几种常用方法解析
  6. Android OTA升级原理和流程分析(一)--update.zip包的制作
  7. Android 中插件的编写方法
  8. Android shape方法绘制图形的方法和属性解析
  9. Flash Android ANE打包之基本流程

随机推荐

  1. Android定制出厂默认输入法(其他设置类似
  2. Android 下载进度条, 自定义加载进度条,loa
  3. [置顶] Android技巧:ListView去掉默认橙黄
  4. android中清空一个表。类似truncate tabl
  5. 使android应用程序不出现在应用管理中
  6. cocos2d-x 2.x iOS和android多分辨率适配
  7. Android Studio: 模拟器启动失败解决(Emul
  8. android学习笔记----多线程断点续传下载
  9. android 调用 react-native方法
  10. Android 功耗优化(5)---Android O 的Doze模