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

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时内部使用的。

你可能感兴趣的:(原理解析,android相关)