View的绘制和工作流程

View在Android中可谓是无处不在,我们看到的每个界面都是View绘制出来的,系统给我们提供的各种控件,比如TextView,Button等他们的本质都是一个个的View,但是这些控件可能并不能满足日常开发的需求,这时候就需要自定义view去绘制一些特定的界面。因此了解View的绘制流程就显得更加重要。

1.ViewRoot和DecorView

我们首先了解一下ViewRoot和DecorView,ViewRoot对应的是ViewRootImp,我们启动一个Activity所展示出来的界面是由window管理的。Window在Activity中对应着PhoneWindow这个实现类。DecorView则是每个Activity的根视图,Activity在被创建后就会被添加到window中去。同时创建ViewRootImpl,并将ViewRootImpl与DecorView建立关系。DecorView本质上是个frameLayout,里面会包含一个LinearLayout有个TitleBar,下面是一个id为content的内容栏。这个样式会根据你设置的主题内容不同而发生改变,但一定会有一个id为content的内容栏。

网上有个图展示的很清楚,借来学习一下图片来自https://www.jianshu.com/p/c151efe22d0d

在这里插入图片描述

2.绘制流程

View的绘制从ViewRoot的performTraversals方法开始,经历measure,layout和draw最终将一个view 绘制出来。performTraversals的方法很长,但是里面主要的就是performMeasure,performLayout,performDraw这三个核心代码如下

private void performTraversals(){
	...
	// 测量
  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
  	...
	// 布局
  performLayout(lp, mWidth, mHeight);
  	...
	// 绘制
  performDraw()
}


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


    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        mLayoutRequested = false;
        mScrollMayChange = true;
        mInLayout = true;

        final View host = mView;
        if (host == null) {
            return;
        }
        if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
            Log.v(mTag, "Laying out " + host + " to (" +
                    host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
        }

        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
        try {
        	//掉用view的layout
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
		.......
	}


  private void performDraw() {
    boolean canUseAsync = draw(fullRedrawNeeded);
  }

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

MeasureSpec

分析三大流程之前我们先了解下MeasureSpec这个类,MeasureSpec是View的一个静态内部类。MeasureSpec字面上理解是测量的规格。MeasureSpec是一个32位的int值,高两位代表测量模式,低30位表示测量规格大小。将模式和大小打包成一个int值目的是避免过多的内存分配。

我们看到为了方便使用,MeasureSpec提供了makeMeasureSpec,makeSafeMeasureSpec打包方法,和getMode,getSize解包的方法。
在这里插入图片描述

我们来分析一下SpecMode的三种类型,源码注释如下

 /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
		 * 父容器不对view大小做限制,view的可以想多大就多大
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         * 父容器检测出view所需的精确大小,对于view来说大的大小就是SpecSize的大小。
         * 对应LayoutParams中的mach_parent和具体数值
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         * 父容器指定一个大小,view最大不能超过大于这个值
         *  *对应LayoutParams中的warp_content这个模式
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;
MeasureSpec与LayoutParams

系统通过MeasureSpec进行测量,但是我们也可以通过给view设置LayoutParams。在view测量的时候系统会将layoutParams在父布局的约束下转换成对应的MeasureSpec。因此决定MeasureSpec的值需要LayoutParams和父容器一起,从而决定view的宽高。

  1. 对于顶级的DecorView它的MeasureSpec是由窗口尺寸和自身的LayoutParams决定的。

  2. 普通View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定。

具体见ViewGroup中的getChildMeasureSpec()

  public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

《Android开发艺术探索》中关于View的MeasureSpec的创建规则总结了一个表格。这里copy来学习一下
在这里插入图片描述

小结一下

1.当View固定大小的时候,无论父布局是什么模式,View的MeasureSpec都是精准模式。大小按照LayoutParam中指定的来。
2.当view是match_parent时,父布局是精准模式view就是精准模式,父布局是最大模式,view就是最大模式。尺寸大小最大不能超过父布局。
3.当view是warp_content时,无论父布局是精准还是最大,view总是最大模式。

一, measure过程

measure分为View和ViewGroup两种情况。ViewGroup除了测量完自己的宽高还会还要去遍历调用所有子元素的measure方法。

(1)View的measure

view的measure会调用onMeasure()方法

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

setMeasuredDimension会设置view宽高的测量值。我们看下getDefaultSize是怎么处理的

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

从代码中我们看到MeasureSpec.AT_MOST,MeasureSpec.EXACTLY情况下。getDefaultSize返回的就是measureSpec中的SpecSize。SpecSize就是测量的大小。

MeasureSpec.UNSPECIFIED模式下view的大小返回的就是getSuggestedMinimumWidth()的大小。

  protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }


  public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

getSuggestedMinimumWidth()中的逻辑为,如果view没有设置背景则返回minWidth这个值,若如果设置了背景,则返回minWidth和背景最小宽度的的最大值。

(1)ViewGroup的measureViewGroup

ViewGroup提拱了一个measureChildren方法

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

遍历调用measureChild方法去测试子view的大小

   protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

measureChild中获取child的Layoutparam,在调用getChildMeasureSpec去获取子view的MeasureSpec。最终调用measure的方法去测量出view的大小

二, layout过程

Layout的作用是确定布局的最终位置。view的layout方法用来确定view的位置。ViewGroup的onLayout方法用来确定每个子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;
        }

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

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

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

        final boolean wasLayoutValid = isLayoutValid();

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        if (!wasLayoutValid && isFocused()) {
            mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
            if (canTakeFocus()) {
                // We have a robust focus, so parents should no longer be wanting focus.
                clearParentsWantFocus();
            } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
                // This is a weird case. Most-likely the user, rather than ViewRootImpl, called
                // layout. In this case, there's no guarantee that parent layouts will be evaluated
                // and thus the safest action is to clear focus here.
                clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                clearParentsWantFocus();
            } else if (!hasParentWantsFocus()) {
                // original requestFocus was likely on this view directly, so just clear focus
                clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
            }
            // otherwise, we let parents handle re-assigning focus during their layout passes.
        } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
            mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
            View focused = findFocus();
            if (focused != null) {
                // Try to restore focus as close as possible to our starting focus.
                if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
                    // Give up and clear focus once we've reached the top-most parent which wants
                    // focus.
                    focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                }
            }
        }

        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }

layout方法通过setFrame设定View的四个顶点位置。父布局位置确定后调用onlayout方法。

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

onLayout方法是个空方法,没有具体的实现,需要子view自己去处理自己的位置逻辑。

三, draw过程

Draw的过程就是把确定好大小和位置的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;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

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

      。。。。。
    }

源码上注释也写的很清楚。总共分以下几步。

1. 绘制背景 drawBackground(canvas);
2. 绘制的内容 onDraw(canvas)
3. 绘制children dispatchDraw(canvas);
4. 绘制装饰 onDrawForeground(canvas);
5. 绘制默认焦点高亮 drawDefaultFocusHighlight

总结

1.想要自己绘制ViewGroup,需要实现onMeasure,在onMeasure方法里面遍历测量子元素。同理onlayout方法也一样。
2.绘制view需要在onMeasure方法里面处理下warp_content情况。不需要处理onlayout

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值