Android View的绘制流程

Android View的绘制流程

源码版本为 Android 10(Api 29),不同Android版本可能有一些差别

View 的绘制从哪里开始

《Activity常见问题》的 Activity 在 onResume 之后才显示的原因是什么? 部分中我们知道了View是在 onResume() 回调之后才显示出来的,显示过程主要是通过 WindowManagerImpl#addView() -> WindowManagerGlobal#addView() -> ViewRootImpl#setView() 这个过程,我们再次看一下 ViewRootImpl#setView() 的代码(核心代码):

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
     synchronized (this) {
         if (mView == null) {
             mView = view;
             // 调用 requestLayout() 方法,进行布局(包括measue、layout、draw)
             requestLayout();
             
             mOrigWindowType = mWindowAttributes.type;
             mAttachInfo.mRecomputeGlobalAttributes = true;
             collectViewAttributes();
             // 通过调用 Session 的 addToDisplay() 方法
             res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                     getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                     mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                     mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                     mTempInsets);
             setFrame(mTmpFrame);
         }
     }
 }

有这样一行 requestLayout() ,表示请求布局,我们界面的绘制也是从这一行代码开始的,接下来,我们就来看一下跟踪一下这段代码(ViewRootImpl#requestLayout()):

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

调用 ViewRootImpl#scheduleTraversals() 方法:

@UnsupportedAppUsage
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

scheduleTraversals() 方法中通过 mChoreographer.postCallback() 方法发送一个要执行的实现了 RunnableTraversalRunnable 的对象 mTraversalRunnable

  1. mChoreographer.postCallback() 方法内部就是通过Handler机制

  2. TraversalRunnableViewRootImpl 的内部类

     final class TraversalRunnable implements Runnable {
         @Override
         public void run() {
             doTraversal();
         }
     }
    

调用 ViewRootImpl#doTraversal() 方法

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

调用 ViewRootImpl#performTraversals() 方法,该方法中关于绘制的代码

private void performTraversals(){
    ……
    // 方法测量组件的大小
    performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
    ……
    // 方法用于子组件的定位(放在窗口的什么地方)
    performLayout(lp,desiredWindowWidth,desiredWindowHeight);
    ……
    // 绘制组件内容
    performDraw();
    ……
}

而在performMeasure()performLayout()performDraw()方法的调用过程可以用下面的图来表示:

Android View绘制流程.png

从图中可以看出系统的View类已经写好了measure()layout()draw()方法:

  1. 在系统View类中measure()方法用了final修饰,不能被重写(我觉得这应该是google不想让开发者更改measure()方法里面的逻辑而设计的,但是开发者有时又有需求需要自己测量,所以提供了onMeasure()方法可以重写);
    代码
  2. 在系统View类中的layout()方法在调用onLayout()方法前调用了setFrame()方法,这个方法作用是判断View的位置是否发生改变,如果没有发生改变,就不调用onLayout()方法,主要是为了提高性能,在View类中onLayout()方法是空实现,这是因为view没有子类,而当在自定义的控件如果是直接继承ViewGroup时就必须重写onLayout()方法;
    代码
  3. 在系统View类中的draw()方法,开发者一般不会重写,因为当我们如果重写draw()时,就需要按照系统定义好的步骤一步一步的画,否则会显示不出来,相对来说比较麻烦。而如果我们实现onDraw()方法,我们只要关注我们画的内容即可(画出来的内容就是显示到界面的内容);
    代码
  4. 当开发者在自定义控件时一般只需重写onMeasure()onLayout()onDraw()方法就可以了。

测量 -- measure

源码追踪

ViewRootImpl中的performMeasure()方法:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

调用了mViewmeasure()方法,mView就是 DecorView,是通过 ViewRootImpl#setView() 传入进来的,也就是调用了FrameLayout#measure()方法,FrameLayout继承ViewGroupViewGroup没有重写也不能重写measure()方法,所以最终调用的是View类中的measure()方法:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    if (cacheIndex < 0 || sIgnoreMeasureCache) {
        // measure ourselves, this should set the measured dimension flag back
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
    ...
}

在View类中的measure()中系统帮我们做了很多的处理并且不想让开发者重写measure的逻辑,所以使用了final修饰符进行修饰,并且调用了onMeasure()方法,所以在Activity中View树的测量过程中,最终是从FrameLayout#onMeasure()方法开始的,FrameLayoutonMeasure()方法如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    ......
    // 判断孩子控件的Visibility属性,如果为gone时,就跳过,因为gone属性不占用空间
    if (count > 1) {// 判断是否有孩子控件
        for (int i = 0; i < count; i++) {
            // 通过LayoutParams参数获取孩子控件的margin、padding值
            ...
            // 调用孩子控件的measure()方法测量自身大小
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

FrameLayoutonMeasure()方法中,先是获取了孩子控件的个数,然后获取每一个孩子控件并判断visibility属性;最终调用孩子View#measure()方法测量自身大小。

当开发者有需要重新测量控件时,只需要重写onMeasure()方法即可,系统在View的measure()方法中会回调onMeasure()方法,使测量值生效。

下面是继承自View时重写的onMeasure()方法,我就只是简单的将宽和高都设置成500:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    setMeasuredDimension(500,500);
}

下面是继承ViewGroup时重写的onMeasure()方法,将所有孩子控件的宽和高的和计算出来作为父控件的宽和高,最终调用setMeasuredDimension()方法设置值:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMeasure = 0,heightMeasure = 0;
    // 直接调用系统方法测量每一个孩子控件的宽和高
    measureChildren(widthMeasureSpec,heightMeasureSpec);
    /**
     * 系统在调用measureChildren(widthMeasureSpec,heightMeasureSpec)的过程中,
     * 如果孩子控件依然是ViewGroup类型的,那么又会调用measureChildren()方法,否则会调用
     * child.measure(childWidthMeasureSpec, childHeightMeasureSpec)方法测量每一个孩子控件
     * 的宽和高,直到所有的孩子控件都测量完成。
     * 这就可以说明measure的过程可以看成是一个递归的过程。
     */
 
    // 获取孩子控件的个数
    int childCount = getChildCount();
    // 循环测量每一个孩子控件的宽和高,得到的和就作为控件的宽和高
    for (int i = 0; i < childCount; i++) {
        View view = getChildAt(i);
        // 获取每一个孩子的宽和高
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
        // 把每一个孩子控件的宽和高加上
        widthMeasure += width;
        heightMeasure += height;
    }
 
    // 调用setMeasuredDimension()方法保存宽和高,表示measure的结束
    setMeasuredDimension(widthMeasure,heightMeasure);
}

另外在measure的过程中还可能会用到MeasureSpec类(View的内部类):

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
 
    // 父控件不没有对子施加任何约束,子可以是任意大小(也就是未指定)
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
 
    // 父控件决定子的确切大小,表示width和height属性设置成match_parent或具体值
    public static final int EXACTLY     = 1 << MODE_SHIFT;
 
    // 子最大可以达到的指定大小,当设置为wrap_content时,模式为AT_MOST
    public static final int AT_MOST     = 2 << MODE_SHIFT;
    
    /*
     * 通过模式和大小创建一个测量规范
     * @param size the size of the measure specification
     * @param mode the mode of the measure specification
     * @return the measure specification based on size and mode
     */
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
 
    public static int makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }
 
    /*
     * 获取模式
     */
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
 
    /*
     * 获取大小
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
 
    ...
}

MeasureSpecs使用了二进制去减少对象的分配,用最高的两位数来表示模式(Mode),剩下的30位表示大小(size)。

Mode有三种:UNSPECIFIED(未指定,没有约束,可以任意大小)、EXACTLY(精确,表示match_parent或者具体的大小值)、AT_MOST(最大值,表示wrap_content)

measure总结:

  1. View的measure()方法被final修饰,子类不可以重写,但可以通过重写onMeasure()方法来测量大小,当然也可以不重写onMeasure()方法使用系统默认测量大小;

  2. 如果想要让自己设置的值生效,就必须调用setMeasuredDimension()方法设置宽和高;

  3. 如果在ActivityonCreate()方法或onResume()方法里面直接调用getWidth()/getHeight()getMeasureWidth()/getMeasureHeight()获取控件的大小得到的结果很可能是0,因为在onCreate()onResume()的时候系统还没有调用measure()方法(getMeasureWidth()getMeasureHeight()的赋值在View的setMeasuredDimension()方法中,所以在调用完View的setMeasuredDimension()方法之后getMeasuredWidth()getMeasuredHeight()就已经有值了。而getWidth()getHeight()要在onLayout()方法完成之后才会被赋值),如果一定要在onCreate()方法或onResume()方法里面获取控件的大小,可以通过以下方法得到:

     view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
         @Override
         public void onGlobalLayout() {
             int width = view.getWidth();
             int height = view.getHeight();
             view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
         }
     });
    
  4. 通过setMeasuredDimension()方法设置的值并不一定就是控件的最终大小,组件真正的大小最终是由setFrame()方法决定的,该方法一般情况下会参考measure出来的尺寸值;

  5. 子视图View的大小是由父容器View和子视图View布局共同决定的;

  6. 如果控件是ViewGroup的子类,那就必须测量每一个孩子控件的大小,可以调用系统的measureChildren()方法测量,也可以自己测量;

  7. Android系统对控件的测量过程可以看做是一个递归的过程。

摆放 -- layout

源码追踪

ViewRootImpl中的performLayout()方法:

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
    ...
    final View host = mView;
    ...
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ...
    for (int i = 0; i < numValidRequests; ++i) {
        final View view = validLayoutRequesters.get(i);
        Log.w("View", "requestLayout() improperly called by " + view +
                " during layout: running second layout pass");
        view.requestLayout();
    }
}

代码中的host是View树中的根视图(DecroView),也就是最外层容器,容器的位置安排在左上角(0,0),其大小默认会填满 mContentParent容器。该方法作用是确定孩子控件的位置,所以该方法只针对ViewGroup容器类,最终调用了Viewlayout()方法确定每一个孩子控件的位置:

public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
 
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
 
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
 
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
 
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }
 
    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

layout()中确定位置之前会判断是否需要重新测量控件的大小,如果需要,就会调用onMeasure()方法重新测量控件,接下来执行 setOpticalFrame()setFrame()方法确定自身的位置与大小,这一步并不会绘制出来,只是将控件位子和大小值保存起来;接着调用onLayout()方法,在View中onLayout()方法是空实现。onLayout()方法的作用是当当前控件是容器控件时,那就必须重写onLayout()方法确定每一个孩子控件的位置,而当孩子控件还是ViewGroup的子类时,继续调用onLayout()方法,直到所有的孩子控件都有了确定的位置和大小,这个过程和measure一样,也可以看做是一个递归的过程。下面是FrameLayout#onLayout()方法源码:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
 
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    final int count = getChildCount();
    ...// 遍历所有的孩子控件
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        ... // 判断控件的visible属性书否为gone,如果为gone就不占用位置
        ... // 计算childLeft、childTop、childRight、childBottom,确定位置
        child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

layout总结:

  1. View的布局逻辑是由父View,也就是ViewGroup容器布局来实现的。因此,我们如果自定义View一般都无需重写onLayout()方法,但是如果自定义一个ViewGroup容器的话,就必须实现onLayout()方法,因为该方法在ViewGroup类中是抽象的,ViewGroup的所有子类必须实现onLayout()方法(如果我们定义的容器控件是继承FrameLayout或其他已经继承了ViewGroup类的容器控件时,如果没有必要可以不用实现onLayout()方法,因为FrameLayout类中已经实现了);
  2. 如果view控件使用了gone属性时,在onLayout()方法遍历中就会跳过当前的View,因为gone属性表示不占用位置;
  3. 当layout()方法执行完成之后,调用getHeight()/getWidth()方法就能够得到控件的宽和高的值了;
  4. View的layout过程和measure过程类似,都可以看做是一个递归的过程;
  5. 在Activity中,layout的过程是从DecorView控件开始的。

绘制 -- draw

源码追踪

ViewRootImpl中的performDraw()方法:

private void performDraw() {  
    ...  
    final boolean fullRedrawNeeded = mFullRedrawNeeded;  
    mFullRedrawNeeded = false;  
    mIsDrawing = true;  
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");  
    try {  
        draw(fullRedrawNeeded);  
    } finally {  
        mIsDrawing = false;  
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);  
    }  
  
    ...  
}  
  
private void draw(boolean fullRedrawNeeded) {  
    ...  
    if (!drawSoftware(surface, attachInfo, yoff, scalingRequired, dirty)) {  
        return;  
    }  
    ...  
}  
  
/** 
 * @return true if drawing was succesfull, false if an error occurred 
 */  
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int yoff,  
        boolean scalingRequired, Rect dirty) {  
    Canvas canvas;  
    ...  
    try {  
        ...  
        int left = dirty.left;  
        int top = dirty.top;  
        int right = dirty.right;  
        int bottom = dirty.bottom;  
        canvas = mSurface.lockCanvas(dirty);  
        if (!canvas.isOpaque() || yoff != 0) {  
            canvas.drawColor(0, PorterDuff.Mode.CLEAR);  
        }  
        mView.draw(canvas);  
        ...  
    } finally {  
        surface.unlockCanvasAndPost(canvas);  
    }  
    ...  
    return true;  
}

canvas对象是从surface中获取到的,surface是中提供了一套双缓存机制,这样就提高了绘图的效率.通过代码可以看到最后调用了mViewdraw()方法,mViewecorView,也就是FrameLayout,在FrameLayoutViewGroup中都是没有重写draw()方法的,所以最终调用的是View中的draw()方法:

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background   绘制视图View的背景
     *      2. If necessary, save the canvas' layers to prepare for fading 保存画布canvas的边框参数
     *      3. Draw view's content 绘制视图View的内容(调用了onDraw()方法)
     *      4. Draw children 绘制当前视图View的子视图(调用dispatchDraw()方法)
     *      5. If necessary, draw the fading edges and restore layers 绘制边框的渐变效果并重置画布
     *      6. Draw decorations (scrollbars for instance) 绘制前景、滚动条等修饰
     */
 
    // Step 1, draw the background, if needed 绘制视图View的背景
    int saveCount;
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }
    // skip step 2 & 5 if possible (common case)
    ...
 
    // Step 2, save the canvas' layers 保存画布canvas的边框参数
    saveCount = canvas.getSaveCount();
 
    // Step 3, draw the content 绘制视图View的内容(调用了onDraw()方法)
    if (!dirtyOpaque) onDraw(canvas);
 
    // Step 4, draw the children 绘制当前视图View的子视图(调用dispatchDraw()方法)
    dispatchDraw(canvas);
 
    // Step 5, draw the fade effect and restore layers 绘制边框的渐变效果并重置画布
    ...
    canvas.restoreToCount(saveCount);
 
    // Step 6, draw decorations (foreground, scrollbars) 绘制前景、滚动条等修饰
    onDrawForeground(canvas);
}

由以上代码可以看得到,系统在View类中的draw()方法已经将背景、边框、修饰等都绘制出来了,而且在第三步和第四步的时候调用了onDraw()方法和dispatchDraw()方法,这样开发者在自定义控件的时候就只需要重写onDraw()方法或者dispatchDraw()方法,也就是只需要关注最终显示的内容就可以了,而不需要去绘制其他的修饰。
View里面的onDraw()方法和dispatchDraw()方法都是空实现,也就是留给开发者去实现里面的具体逻辑。同时,在开发者实现onDraw()方法和dispatchDraw()方法时也可以不用去绘制其他的修饰了。需要说明一点,View中的draw()方法并不是和measure()方法一样被final修饰,draw()方法没有被final修饰,所以是可以重写的,但是当我们重写draw()方法时,必须和系统中Viewdraw()方法一样,一步一步的实现,否则就不能将控件绘制出来,所以在自定义控件的时候,一般都是重写onDraw()dispatchDraw()方法。

如果自定义控件是继承至View时,就重写onDraw()方法,在onDraw()方法中绘制的结果就是最终显示的结果。

以下代码就是在界面上以坐标(150,150)绘制了一个半径为35的红色实心圆:

public class CircleView extends View {
    Paint paint;
    public CircleView(Context context) {
        this(context, null);
    }
 
    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.FILL);
    }
 
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawCircle(150,150,35,paint);
    }
}
View 绘制流程_circle.png

如果自定义控件是继承至ViewGroup时,就重写dispatchDraw()方法,这里直接查看系统FrameLayoutdispatchDraw()方法:

@Override
protected void dispatchDraw(Canvas canvas) {
    final int childrenCount = mChildrenCount;
    ...
    for (int i = 0; i < childrenCount; i++) {
        while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
            final View transientChild = mTransientViews.get(transientIndex);
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
        }
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
   ...
}

在代码里面调用了drawChild(canvas, child, drawingTime)方法用来绘制孩子:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

而在drawChild()方法中就是调用了viewdraw(Canvas canvas, ViewGroup parent, long drawingTime)方法(三个参数的方法)来绘制自己,如果孩子控件还是ViewGroup的子类,又会重新调用drawChild()方法递归处理,直到所有的孩子控件绘制完成,也就表示控件绘制完成了。其实这也和measure、layout的过程一样,可以看做是一个递归的过程。

看一下view中三个参数的draw(Canvas canvas, ViewGroup parent, long drawingTime)方法:

/**
 * This method is called by ViewGroup.drawChild() to have each child view draw itself.
 * 这个方法是在ViewGroup的drawChild()方法中调用,用来绘制每一个孩子控件自身。
 * This draw() method is an implementation detail and is not intended to be overridden or
 * to be called from anywhere else other than ViewGroup.drawChild().
 * 这个方法除了在ViewGroup的drawChild()方法中被调用外,不应该在其它任何地方去复写或调用该方法,它属于ViewGroup。
 * 而这个方法最终也会调用View的draw(canvas)一个参数的方法来进行绘制。
 */
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    ...
    if (!hasDisplayList) {
        // 调用computeScroll()方法,这个方法是用来与Scroller类结合实现实现动画效果的
        computeScroll();
        sx = mScrollX;
        sy = mScrollY;
    }
    ...
     if (!hasDisplayList) {
        // Fast path for layouts with no backgrounds
        if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            // 继续调用dispatchDraw()方法递归处理
            dispatchDraw(canvas);
        } else {
            // 调用View的draw(canvas)一个参数的方法
            draw(canvas);
        }
    } else {
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        ((HardwareCanvas) canvas).drawRenderNode(renderNode, null, flags);
    }
    ...
    return more;
}

了解更多关于Scroller类的相关内容,可以查看《 Android中的Scroller类》这篇博客。

draw总结:

  1. View绘制的画布canvas是从surface对象中获得,而最终也是绘制到surface中去。surface提供了一个双缓存机制,可以提高绘制的效率;
  2. 系统在View类中的draw()方法已经将背景、边框、修饰等都绘制出来了,如果在自定义View时直接继承View时是重写draw()方法,就必须和系统中Viewdraw()方法一样,一步一步的实现,否则就不能将控件展示出来;
  3. 因为自定义控件一般重写onDraw()方法,所以每一个控件都会绘制滚动条和其他的修饰;
  4. 自定义View如果直接继承制View时,需要重写onDraw()方法,在onDraw()中绘制的内容就是最终展示到界面的内容,自定义View如果是直接继承ViewGroup,那就重写dispatchDraw()方法,绘制ViewGroup的孩子控件;
  5. Android绘制的过程和measure、layout一样,可以看做是一个递归的过程。

总体来说,View的绘制从开始到结束要经历几个过程:

测量大小,回调 onMeasure()方法

组件定位,回调 onLayout()方法

组件绘制,回调 onDraw()方法

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容