View的绘制流程

  在自定义View的时候,大家都知道一般需要重写三个方法:onMeasure()(确定大小和模式)、onLayout()(确定摆放位置)、onDraw()(画,显示)。但是大部分只知道需要重写,但是不知道为什么是这三个,而不是别的,而且一般只用到第一和第三个就能满足大部分需求。刚刚开始的时候我也是只知道重写,能实现需求就好了,但是过段时间之后,会发现自己还停留在用的层面上,直到去年有一次面试的时候被面试官问道:为什么要重写这三个方法?那三种模式是如何被确定的?View和ViewGround各自的实现有什么区别和为什么?等等。因为简历上写着熟悉自定义View,好吧,应该写着会用而不是熟悉,如果这样能有多的面试机会的话。。。从那以后我决定要做有自我意识的程序员而不是面向谷歌程序员
  熟悉View的绘制流程,可以写出提高性能的UI和布局,这会大大地改善APP的质量。相信很多人在Google如何提高app的性能的时候会看到大部分都介绍要减少布局嵌套、要防止过度渲染绘制,要控制好background的设置等等。虽然告诉解决办法,或者说是折中处理方法,但是却不知道为什么?有成语说得对:知彼知己,百战不殆。无论是做什么,在行动前都必须要了解自己所面对的事物,才能把事情完成得更好,编程也是一样,一天到晚都在那里改,改来改去,效果不甚好,更浪费时间,我们得找准原因,逐一击破才是。
  本片内容需要结合着上一边文章来讲,所以需要先了解一下Activity的启动流程和setContentView()的流程分析最佳,还没看过的童鞋可以先看看:基于9.0的setContentView源码分析基于Android 9.0的Activity启动流程源码分析

View的绘制入口

  之前分析过了setContentView()的源码,知道了怎么把我们设置的布局给添加进去,但是还不知道怎样把布局渲染绘制出来。接下来我们开始分析这个过程
类:PhoneWindow

    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();   //注释1
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);   //注释2
        }
        mContentParent.requestApplyInsets();     //注释3
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

  这个是之前分析setContentView()的源码的时候的一段源码,我们已经分析过来,这里就不便详细叙说,大概讲一下就好了。在注释1创建了DecorView,还创建了mContentParent给添加到DecorView上,注释2就把我们的布局给添加到mContentParent中,就这样把整个布局给添加进去了,接下来看一下注释3:
类 View

 public void requestApplyInsets() {
        requestFitSystemWindows();
    }

    public void requestFitSystemWindows() {
        if (mParent != null) {
            mParent.requestFitSystemWindows();    //注释1
        }
    }

当执行注释1的时候,mParent是ViewParent类型的变量:

  protected ViewParent mParent;

赋值:

   void assignParent(ViewParent parent) {
        if (mParent == null) {
            mParent = parent;
        } else if (parent == null) {
            mParent = null;
        } else {
            throw new RuntimeException("view " + this + " being added, but"
                    + " it already has a parent");
        }
    }

  ViewParent 是一个抽象类,我们需要找它的实现类ViewRootImpl,so,搜索一下ViewRootImpl的requestFitSystemWindows();方法:
类 ViewRootImpl

    public void requestFitSystemWindows() {
        checkThread();   //检查当前线程是否为之前之前调用过的线程,也就是UI线程
        mApplyInsetsRequested = true;
        scheduleTraversals();   
    }


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


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


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

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

            performTraversals();

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

  依次执行下来,直到 performTraversals(),一般网上的文章都是直接从 performTraversals()开始分析,但是很多人不知道是怎么找到这个入口的,就直接搜索,现在我直接把如何进入到 performTraversals()过程直接贴出来,应该清楚了吧。废话不说了,继续点击查看 performTraversals()的执行代码:
类 ViewRootImpl

private void performTraversals() {

//注释1
 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

//注释2
 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

//注释3
  performLayout(lp, mWidth, mHeight);

  //注释4
   performDraw();
}

  看到注释1、2、3、4有木有很是激动啊?终于找到了performMeasure()、performLayout()、performLayout()这三个方法,这三个方法会依次执行onMeasure()、onLayout()、onDraw()方法这三个方法。
  建议对照着View的绘制流程图来看,这样看起来逻辑比较清晰:


View的绘制流程.png

先看getRootMeasureSpec()方法,这里根据根部局设置的宽高属性来决定了根部局的模式和大小
类 ViewRootImpl

//决定了根部局的模式和大小
 private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

        //确定模式和大小
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }


  经过上面所贴的代码之后,确定了根布局的模式和大小,接下来就需要确定内容(控件)的模式和大小了

一、View的onMeasure

类 ViewRootImpl

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

  直接点开看 mView.measure():
类 View

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            //注释1
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }

        省略......

        onMeasure(widthMeasureSpec, heightMeasureSpec);

        省略......

    }

  在注释1的时候判断自身的模式和大小:


 static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            int size = getSize(measureSpec);
            if (mode == UNSPECIFIED) {    //如果是UNSPECIFIED模式
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(size, UNSPECIFIED);
            }
            size += delta;
            if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
            }
            return makeMeasureSpec(size, mode);   //EXACTLY 和 AT_MOST模式
        }

   public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

  可以看出在执行getMode()方法获取模式的时候是根据传进来的measureSpec的&运算得到的:

     /**
         * Extracts the mode from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the mode from
         * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
         *         {@link android.view.View.MeasureSpec#AT_MOST} or
         *         {@link android.view.View.MeasureSpec#EXACTLY}
         */
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

  回头看measureSpec到底是怎么传进来的,我们可以退到前一步的measure()方法来看,发现是我们直接传进来的:

   public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

       省略......

       widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
       heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);

       省略......

}

  经过执行 MeasureSpec.adjust()方法,把measure()的两个参数分别传进来计算的,而widthMeasureSpec和vheightMeasureSpec则是在ViewRootImpl的performMeasure()传过来的,而performMeasure()则是通过getRootMeasureSpec()方法,结合根部局(父布局)来运算得到,这些在前面也讲到了这些。
  由此可以得出一个结论,自身的模式和大小不仅仅受自身设置的宽高属性有关,还和父布局设置的属性有关,所显示的范围还受父布局的影响。
  接下来到我们的重头戏了:onMeasure()
类 View


  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

  当需要重新设置模式和大小的时候,setMeasuredDimension()我们也经常用到,我们先看getDefaultSize()方法:
类 View

  public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

  在getDefaultSize()方法中根据模式来确定了大小,但是这个还不是最终确定的大小,还要受子View的影响。但是基本上这个结果是和最后的大小是一致的。最后通过setMeasuredDimension()方法把getDefaultSize()运算的值设置一下就成了我们重写onMeasure()方法的参数值。

二、View的onLayout()

  接下来看 performLayout():
类 ViewRootImpl

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {

       省略......

        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

       省略......

        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        mInLayout = false;
    }

类 View

    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;
        }

        省略......

        onLayout(changed, l, t, r, b);

        省略......

}


   protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

  原来performLayout()只是把左右上下的坐标传进来,啥都没干。值得注意的是,这里的左右上下的坐标是依据父布局的坐标而不是依赖于屏幕的坐标,这个得要很清楚。

三、View的onDraw()

类 ViewRootImpl

 private void performDraw() {

        省略......

         try {
            boolean canUseAsync = draw(fullRedrawNeeded);
            if (usingAsyncReport && !canUseAsync) {
                mAttachInfo.mThreadedRenderer.setFrameCompleteCallback(null);
                usingAsyncReport = false;
            }
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

         省略......

}


   private boolean draw(boolean fullRedrawNeeded) {

     省略......

     if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
                    return false;
                }

     省略......

}




 private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {

      省略......

      mView.draw(canvas);

     省略......

}

一路执行下来,直到 mView.draw(canvas)这行代码,这时候已经跳到View的方法里了:
类 View

 public void draw(Canvas canvas) {


        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

       // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }
      省略......
}


    //是否自动填充
    private void drawAutofilledHighlight(@NonNull Canvas canvas) {
        if (isAutofilled()) {
            Drawable autofilledHighlight = getAutofilledDrawable();

            if (autofilledHighlight != null) {
                autofilledHighlight.setBounds(0, 0, getWidth(), getHeight());
                autofilledHighlight.draw(canvas);
            }
        }
    }

  不知道你们看不看得懂这些源码注释?做IT的应该都会些英语吧?注释已经很清楚说明了绘制是按照这七个步骤进行的:
1、如果设置了背景,就先绘制背景: drawBackground(canvas);
2、检查是否设置了View的边缘化,也就是类似于渐变的效果,如果没有,则进去if里面;
3、进入if里面之后,开始绘制内容: onDraw(canvas);
4、绘制item view: dispatchDraw(canvas);
5、如果View是设置了自动填充的,则进行自动填充绘制:drawAutofilledHighlight(canvas);
6、绘制装饰,比如前景、滚动条等等: onDrawForeground(canvas);
7、绘制默认的焦点高亮显示:drawDefaultFocusHighlight(canvas)。
  在第二步骤判断的时候,如果没进去if里面,也是按照顺序调用上面几个方法的,只不过是比之前多了保存图存的操作。


View的draw流程.png

ViewGroup的绘制特点

  ViewGround和View的绘制流程是大同小异的,只是某些地方需要注意一下,比如经常使用到的LinearLayout布局控件,其继承的就是ViewGroup,按照上面的View的绘制解析,第一步是measure,所以先看一下LinearLayout的onMeasure()方法:
类 LinearLayout

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);     //竖直方向
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);   //横向方向
        }
    }


//可以先选择竖直方向来看一下
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {

        mTotalLength = 0;
        int maxWidth = 0;
        int childState = 0;
        int alternativeMaxWidth = 0;
        int weightedMaxWidth = 0;
        boolean allFillParent = true;
        float totalWeight = 0;

        final int count = getVirtualChildCount();

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        省略......

 // See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {     //注释1

         final View child = getVirtualChildAt(i);

        省略......
        
        f (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                 //由于是EXACTLY 模式,不需要measure子view了
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
           
            } else{     
                    //如果不是EXACTLY模式则需要循环measure每个子item
                   measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight();
                if (useExcessSpace) {
                    // Restore the original height and record how much space
                    // we've allocated to excess-only children so that we can
                    // match the behavior of EXACTLY measurement.
                    lp.height = 0;
                    consumedExcessSpace += childHeight;
                }
 
                 //把每次测量过后的子view的高度不断累加
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

              }

     }

           省略......

            //计算padding、margin......这些

          省略......

        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
         //最后确定了LinearLayout的高度和宽度
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

}

  由于源码太多,我抽出了关键的几句贴出来,详细的可以自己去看,但是流程都是递归measure子View来测量,然后累加起来,大概就是这样。就这样,在进入for循环递归的时候,会执行measureChildBeforeLayout()来调用子view的measure:
类 LinearLayout

 void measureChildBeforeLayout(View child, int childIndex,
            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
            int totalHeight) {
        measureChildWithMargins(child, widthMeasureSpec, totalWidth,
                heightMeasureSpec, totalHeight);
    }

继续进去看measureChildWithMargins()。
类 ViewGroup

 protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

  在最后一行代码执行了 child.measure()方法,也就是View的measure方法,剩下的和之前分析View的onMeasure()一摸一样。由此可以得出LinearLayout也确实是通过递归来测量子view的宽高,然后再累加来计算出实际高度的。
  刚刚看的是竖直方向的,那横向方向呢?按照上面的递归计算逻辑,应该也是由外而内传递下去,再从内而外计算累加起来,只不过计算的是宽度而已,而看源码也确实如此,有兴趣的可以看一下,这里就不多加述说了。
  接下来看下onLayout的方法实现
类 LinearLayout

protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

  onLayout()也是分为竖直和横向方向,我们可以随便看,这里看竖直好了:
类 LinearLayout

void layoutVertical(int left, int top, int right, int bottom) {

 for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {

               setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

             }

}

  查看setChildFrame()方法:
类 LinearLayout

private void setChildFrame(View child, int left, int top, int width, int height) {
        child.layout(left, top, left + width, top + height);
    }

  从上面贴出的代码看,就和之前的measure一样,也是利用递归来不断调用子view的。既然如此,那onDraw()是否也一样呢?不一定!
在上面分析View的draw()方法的源码的时候,要执行onDraw()方法是有一个判断的:
类 View

public void draw(Canvas canvas) {

        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
                       // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);
}

  从draw()方法可以看出,当dirtyOpaque = true的时候才会执行onDraw()方法,而ViewGroup在初始化的时候调用了 initViewGroup(),然后在里面调用了setFlags()方法:
类 ViewGroup

 private void initViewGroup() {
        // ViewGroup doesn't draw by default
        if (!debugDraw()) {
            setFlags(WILL_NOT_DRAW, DRAW_MASK);
        }
}

  setFlags()是View的一个方法,而ViewGroup继承于View,所以直接看View的实现方法:
类 View

 void setFlags(int flags, int mask) {
      
          省略......

       int old = mViewFlags;
        mViewFlags = (mViewFlags & ~mask) | (flags & mask);

        int changed = mViewFlags ^ old;

        省略......

       if ((changed & DRAW_MASK) != 0) {
            if ((mViewFlags & WILL_NOT_DRAW) != 0) {
                if (mBackground != null
                        || mDefaultFocusHighlight != null
                        || (mForegroundInfo != null && mForegroundInfo.mDrawable != null)) {
                    mPrivateFlags &= ~PFLAG_SKIP_DRAW;
                } else {
                    mPrivateFlags |= PFLAG_SKIP_DRAW;
                }
            } else {
                mPrivateFlags &= ~PFLAG_SKIP_DRAW;
            }
}

  ViewGroup在初始化的时候就把mPrivateFlags设置成了WILL_NOT_DRAW,也就是不去执行onDraw()方法,所以 if (!dirtyOpaque) 为false,那onDraw()方法自然也不会执行,除非设置了背景(如:#00000000)或者在自定义ViewGroup的时候在构造函数里面,调用setWillNotDraw(false),去掉其WILL_NOT_DRAW flag。这也是为什么我们在 自定义View的时候会自动调用onDraw(),而自定义ViewGroup则不会调用。显然LinearLayout是继承与ViewGroup的,默认也是不调用onDraw()方法的。
  但是很多时候,项目的需求是是千奇百怪的,为了迎合产品,我们往往需要自定义控件,但是在很多时候,我们可以继承现有的控件来拓展功能,而不必要全都要自己实现,这可以省下很多时间和功夫。那在继承ViewGroup这些类型的控件的时候,onDraw()方法又不被调用?除了设置重新设置flag或背景之外还有别的方法吗?答案:确实有一个。
在分析View的draw()方法源码的时候,有一个语句很是耐人寻味:
类 View

    public void draw(Canvas canvas) {

             省略......

          // Step 3, draw the content
           if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

              省略......

}

  虽然onDraw()方法不一定被执行,但是 dispatchDraw()肯定会被执行的,所以在自定义ViewGroup的时候,可以重写dispatchDraw()方法来代替onDraw()方法。

总结

  • 1、重写三个方法:在分析完了View的绘制流程之后,对于为什么要重写onMeasure()、onLayout()、onDraw()这三个方法有了清楚的了解,以及调用的顺序是怎么样的;
  • 2、了解View和ViewGroup的绘制流程的区别,以及注意事项:View会自己调用onDraw()方法,而ViewGroup则不会默认调用,要想ViewGroup也调用可以在构造函数中调用setWillNotDraw(false),去掉其WILL_NOT_DRAW的flag、设置背景色或者改为重写dispatchDraw()方法;
  • 3、布局优化、减少嵌套:在分析LinearLayout这些ViewGroup类型的控件的时候,发现在measure、layout的时候是采用递归不断循环绘制渲染的,如果布局嵌套太深,那花费的时候也就更久了,更别说还需要调用onDraw()方法的时候了,如果绘制时间较久,就会给用户造成卡顿的现象。所以需要在布局的时候多考虑嵌套问题,在遇到比较复杂的布局,不妨可以考虑自定义布局控件或者第三方的;
  • 4、尽量少设置背景:控件设置了背景的话,会让draw的时候花费更多的时间,特别是那种嵌套的情况下,各自设置了背景会导致过度绘制渲染问题,红了就不好看了;
  • 5、正确 获取控件的宽高size:应该老有人在onCreate()、onResume()这些方法中获取控件的大小吧,但无意外都是获取到了0或者-1,在View的绘制流程的时候直到,在调用onCreate()、onResume()这些方法的时候还没开始测量控件的宽高,也就无法获取,要想获取到真实的宽高大小,可以利用View.post(Runnable)的方法来获取,或者其他的方法,网上很多,这里就不一一列举了,总之就是必须要测量完成之后才可以获取到控件的宽高大小的。

View的绘制流程暂时分析到这里,如果有不正确的地方,希望可以评论或者私信给我。

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