Android进阶之自定义View原理(三)View的绘制流程

引言

前面我们讲到自定义View的测量和布局原理,并举例说明了这两个知识点的具体应用,本篇我们继续从源码入手看看View的绘制流程,与测量和布局流程,View的绘制过程要简单一些,主要流程如下:
View绘制流程图.png

(一)View的draw流程源码分析:

/**
  * 作用:根据给定的 Canvas 自动渲染 View(包括其所有子 View)。
  * 绘制过程:
  *   1. 绘制view背景
  *   2. 绘制view内容
  *   3. 绘制子View
  *   4. 绘制装饰(渐变框,滑动条等等)
  * 注:
  *    a. 在调用该方法之前必须要完成 measure和layout 过程
  *    b. 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
  *    c. 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法
  */ 
  public void draw(Canvas canvas) {
    ...
    // 步骤1: 绘制本身View背景
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

    // 若有必要,则保存图层(还有一个复原图层)
    // 优化技巧:当不需绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过,因此在绘制时,节省 layer 可以提高绘制效率
    final int viewFlags = mViewFlags;
    if (!verticalEdges && !horizontalEdges) {

    // 步骤2:绘制本身View内容
        if (!dirtyOpaque) 
            onDraw(canvas);
        // View 中:默认为空实现,需复写

    // 步骤3:绘制子View
    // 由于单一View无子View,故View 中:默认为空实现
    // ViewGroup中:系统已经复写好对其子视图进行绘制我们不需要复写
        dispatchDraw(canvas);
    // 步骤4:绘制装饰,如滑动条、前景色等等
        onDrawScrollBars(canvas);
        return;
    }
    ...    
}

1.绘制背景:

private void drawBackground(Canvas canvas) {
        // 拿到背景 drawable
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        // 根据在 layout 过程中获取的 View 的位置参数,来设置背景的边界
        setBackgroundBounds();
        .....
        // 获取 mScrollX 和 mScrollY值,即偏移量
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {//按位或运算相当于与运算
            background.draw(canvas);
        } else {
            // 若 mScrollX 和 mScrollY 有值,则对 canvas 的坐标进行偏移
            canvas.translate(scrollX, scrollY);
            // 调用 Drawable 的 draw 方法绘制背景
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
   }

2.onDraw()方法是空方法,需要在子类中覆写绘制自己的内容。

  1. 由于View没有子View,所以dispatchDraw是空实现.
    4.绘制装饰器,如前置景、滚动条等。
public void onDrawForeground(Canvas canvas) {
        onDrawScrollIndicators(canvas);
        onDrawScrollBars(canvas);

        final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
        if (foreground != null) {
            if (mForegroundInfo.mBoundsChanged) {
                mForegroundInfo.mBoundsChanged = false;
                final Rect selfBounds = mForegroundInfo.mSelfBounds;
                final Rect overlayBounds = mForegroundInfo.mOverlayBounds;

                if (mForegroundInfo.mInsidePadding) {
                    selfBounds.set(0, 0, getWidth(), getHeight());
                } else {
                    selfBounds.set(getPaddingLeft(), getPaddingTop(),
                            getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
                }

                final int ld = getLayoutDirection();
                Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
                        foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
                foreground.setBounds(overlayBounds);
            }

            foreground.draw(canvas);
        }
    }

(二)ViewGroup的draw流程分析。

ViewGroup的绘制流程与View绘制流程基本一致,不同的是,它覆写了dispatchDraw()方法:

protected void dispatchDraw(Canvas canvas) {
        ......
         // 1. 遍历子View
        final int childrenCount = mChildrenCount;
        ......

        for (int i = 0; i < childrenCount; i++) {
                ......
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                  // 2. 绘制子View视图
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                ....
        }
    }

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        // 调用子 View 的 draw()进行子View的绘制,如果是ViewGroup,就继续调用dispatchDraw()方法,这样就实现一颗View树的绘制。
        return child.draw(canvas, this, drawingTime);
    }

(三)细节补充:View的重绘制简单说明。

1.View有两个很重要的方法:invalidate和requestLayout,常用于View重绘和更新;

  1. invalidate()方法会执行onDraw过程,重绘View树,注意它仅仅调用绘制流程,不影响测量和布局;
/**
     *invalidate方法会执行onDraw过程,重绘View树
     * Invalidate the whole view. If the view is visible,
     * {@link #onDraw(android.graphics.Canvas)} will be called at some point in
     * the future.
     * 

* This must be called from a UI thread. To call from a non-UI thread, call * {@link #postInvalidate()}. */ public void invalidate() { invalidate(true); }

3.requestLayout()方法:当View的边界,也可以理解为View的宽高,发生了变化,可以调用requestLayout方法重新对View布局;
4.View执行requestLayout方法,会向上递归到顶级父View中,再执行这个顶级父View的requestLayout,所以其他View的onMeasure,onLayout也可能会被调用。
调用invalidate方法只会执行onDraw方法;调用requestLayout方法只会执行onMeasure方法和onLayout方法,并不会执行onDraw方法。所以当我们进行View更新时,若仅View的显示内容发生改变且新显示内容不影响View的大小、位置,则只需调用invalidate方法;若View宽高、位置发生改变且显示内容不变,只需调用requestLayout方法;若两者均发生改变,则需调用两者,按照View的绘制流程,推荐先调用requestLayout方法再调用invalidate方法。

你可能感兴趣的:(Android进阶之自定义View原理(三)View的绘制流程)