阅读 304

Android进阶知识:绘制流程(中)

1、前言

Android进阶知识:绘制流程(上)中主要是关于绘制流程中会遇到的基础知识,这一篇开始来看具体View绘制的流程。前篇中讲过View分为ViewGroup和一般的ViewViewGroup中可以包含其他ViewViewGroup,并且ViewGroup继承了View,所以绘制流程中ViewGroup相比一般View除了要绘制自身还要绘制其子ViewView的绘制流程分为三个阶段:测量measure、布局layout、绘制draw,接下来就开始对ViewViewGroup绘制流程的这三个阶段一个个开始研究。

2、View的绘制流程

2.1 View的measure流程

View的测量流程从Viewmeasure方法开始,所以先来看它的measure方法。

   public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        //optical用来判断是否使用了光学边界布局的ViewGroup
        boolean optical = isLayoutModeOptical(this);
        //判断当前View的mParent是否与自己一样使用了光学边界布局的ViewGroup
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }
        //生成一个key作为测量结果缓存的key
        // Suppress sign extension for the low bytes
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        //缓存为空就new一个新的LongSparseLongArray
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
        //是否强制layout
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        //上次测量和这次结果是否相同
        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        //宽高测量模式是否皆为EXACTLY模式
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        //MeasureSpec中的size测量大小是否相等
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        //是否需要layout
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
        //判断是否需要layout或者强制layout
        if (forceLayout || needsLayout) {
            //清除已测量的flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
            resolveRtlPropertiesIfNeeded();
            //测量之前还有先判断缓存如果是强制layout则为-1,否则去缓存中根据之前生成的key找对应的value
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            //如果cacheIndex小于0即强制layout或者忽略缓存
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                //就调用onMeasure方法测量宽高
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                //onMeasure测量结束后再修改状态flag
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                //否则就用缓存中的值
                long value = mMeasureCache.valueAt(cacheIndex);
                //通过setMeasuredDimensionRaw方法将缓存值赋给成员变量
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            //这里对复写onMeasure方法却未调用setMeasuredDimension方法做了校验抛出异常
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }
            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }
        //更新mOldWidthMeasureSpec,mOldHeightMeasureSpec
        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
        //更新缓存
        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }
复制代码

注释已经很详细了,这里再梳理总结一下measure方法做了哪些事。其实measure方法主要功能就是调用onMeasure方法测量大小,但是不是每次都调用,这里做了优化。先要判断是否是需要layout或者是强制layout。判断通过之后还要再判断是否是强制测量或者忽略缓存,如果强制测量或者忽略缓存才会去调用onMeasure方法区测量,否则直接用缓存中上一次测量的缓存就行。

接下来看onMeasure测量方法:

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

onMeasure方法里就调用了setMeasuredDimension方法。

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
复制代码

setMeasuredDimension方法里又调用了setMeasuredDimensionRaw方法,这里就和之前从缓存中取数据调用方法是一样的了,这里将测量好的宽高赋给成员变量。回头再看onMeasure方法里的setMeasuredDimension方法传参,这里传参都先调用了getDefaultSize方法,而getDefaultSize方法又调用了getSuggestedMinimumWidthgetSuggestedMinimumHeight方法。一个一个来看,这里WidthHeight方法是类似的,所以就看getSuggestedMinimumWidth方法即可。

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

这个方法里判断了View是否有mBackground,没有背景就返回View的最小宽度,有背景就从View的最小宽度和背景的最小宽度中选较大的返回。这里的最小宽度可以在xml里通过相关属性设置或者调用setMinimumWidth方法设置。

  <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:minWidth="100dp"
        android:minHeight="200dp"
       />
复制代码
  public void setMinimumWidth(int minWidth) {
        mMinWidth = minWidth;
        requestLayout();
    }
复制代码

接下来看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;
    }
复制代码

这里传递的参数size就是getSuggestedMinimumWidth方法获得View自身想要的大小,measureSpecView的父布局的MeasureSpec信息。然后根据父布局MeasureSpec信息中的specModespecSize来返回测量结果result。父布局specModeUNSPECIFIED即不限制大小所以result就为View想要的大小sizespecModeAT_MOST或者EXACTLY,则都返回父布局的specSize大小.

从这里就可以看出View中默认AT_MOST或者EXACTLY模式返回测量大小一样的,所以在自定义View的时,单单只继承了View之后,默认宽高设置对应的MATCH_PARENTWRAP_CONTENT属性返回的结果也是一样的,即默认的ViewWRAP_CONTENT是无效的,需要我们自己复写onMeasure方法设置AT_MOST模式下的最小测量值。至此Viewmeasure流程就结束了,下面用一张流程图来梳理下View的测量流程。

View的measure流程

2.2 View的layout流程

接下来看View的布局流程,从layout方法开始。

public void layout(int l, int t, int r, int b) {
        //先判断在layout之前是否需要先调用onMeasure测量
        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;
        //这里还是根据isLayoutModeOptical方法判断mParent是否使用光学边界布局,分别调用setOpticalFrame和setFrame方法
        //setOpticalFrame方法中计算Insets后同样调用了setFrame方法
        //最终返回一个布尔值changed表示布局是否发生变化
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        //如果发生了改变或者需要进行layout
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //就调用onLayout方法
            onLayout(changed, l, t, r, b);
            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
            //layout之后调用onLayoutChange方法
            //获取ListenerInfo 
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                //获取ListenerInfo不为空且mOnLayoutChangeListeners就克隆一份OnLayoutChangeListener集合
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                //循环调用OnLayoutChangeListener的onLayoutChange方法
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }
        final boolean wasLayoutValid = isLayoutValid();
        //修改强制layout的flag
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
        //最后是一些关于焦点的处理
        if (!wasLayoutValid && isFocused()) {
            mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
            if (canTakeFocus()) {
                //如果自身能获取焦点就清除父母的焦点
                clearParentsWantFocus();
            } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
                //ViewRootImpl为空或者ViewRootImpl不在layout
                //这里是一种奇怪的情况,可能是用户而不是ViewRootImpl调用layout方法,最安全的做法在这里还是清除焦点
                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()) {
                    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方法返回一个布局是否发生变化的结果,判断发生了变化就会调用onLayout方法重新计算布局位置,其它还有一些特殊情况的处理、监听的调用和焦点的处理。下面就来看setFrame方法。

 protected boolean setFrame(int left, int top, int right, int bottom) {
        //默认change为false
        boolean changed = false;
        if (DBG) {
            Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }
        //比较新旧left、top、right、bottom
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            //只要有一个不相等就说明有改变
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;
            //分别计算旧的宽高和新的宽高
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            //新旧有不相等sizeChanged就为true
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
            // Invalidate our old position
            invalidate(sizeChanged);
            //更新新的left、top、right、bottom的值
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            //设置更新视图显示列表
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
            mPrivateFlags |= PFLAG_HAS_BOUNDS;
            //如果发生了改变调用sizeChange方法
            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                //如果是可见的,就强制加上PFLAG_DRAWN状态位
                //防止在调用setFrame方法前PFLAG_DRAWN状态为被清除
                //保证invalidate方法执行
                mPrivateFlags |= PFLAG_DRAWN;
                invalidate(sizeChanged);
                // parent display list may need to be recreated based on a change in the bounds
                // of any child
                invalidateParentCaches();
            }
            // Reset drawn bit to original value (invalidate turns it off)
            mPrivateFlags |= drawn;
            mBackgroundSizeChanged = true;
            mDefaultFocusHighlightSizeChanged = true;
            if (mForegroundInfo != null) {
                mForegroundInfo.mBoundsChanged = true;
            }
            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
        return changed;
    }
复制代码

setFrame方法中会比较新旧mLeftmTopmRightmBottom,这四个值就是第一篇文章中提到的View的四个get方法获取的值,表示自己相对于父布局的位置。只要有一个不同就说明发生了变化,然后调用mRenderNode.setLeftTopRightBottom方法设置更新视图显示列表,返回change结果为true。接下来进入查看onLayout方法。

  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
  }
复制代码

ViewonLayout方法是一个空方法。因为单个普通View是没有子View的,他自身的位置计算在layout方法中已经计算好了。如果是ViewGroup则必需要复写onLayout方法,根据自身要求设置子View的位置。

View的layout流程

2.3 View的draw流程

同样先从Viewdraw方法开始阅读。

 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遍历执行的几个步骤:
         *
         *      1. 绘制背景
         *      2. 如果有必要,保存Canvas图层
         *      3. 绘制内容
         *      4. 绘制子元素
         *      5. 如果有必要,绘制边缘和恢复图层
         *      6. 绘制装饰 (例如滚动条scrollbars)
         */
        // Step 1, draw the background, if needed
        int saveCount;
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
        ......
        // Step 2, save the canvas's layers
        int paddingLeft = mPaddingLeft;
        final boolean offsetRequired = isPaddingOffsetRequired();
        if (offsetRequired) {
            paddingLeft += getLeftPaddingOffset();
        }
        ......
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
        // Step 4, draw the children
        dispatchDraw(canvas);
        ......
        // Step 5, draw the fade effect and restore layers
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;
        ......
        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
        }
复制代码

View中的draw方法注释和逻辑就比较清晰了,一共主要分为六个步骤绘制:

  1. 绘制背景
  2. 如果有必要,保存Canvas图层
  3. 绘制内容
  4. 绘制子元素
  5. 如果有必要,绘制边缘和恢复图层
  6. 绘制装饰 (例如滚动条scrollbars)

首先是drawBackground方法:

 private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        //设置背景边界
        setBackgroundBounds();
        ......
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            //直接绘制背景
            background.draw(canvas);
        } else {
            //如果 mScrollX 和 mScrollY 有值 则将画布canvas平移
            canvas.translate(scrollX, scrollY);
            //之后在调用draw方法绘制背景
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }
复制代码

drawBackground方法中主要是做了两个操作,一个是设置背景边界,另一个是绘制背景。如果有mScrollXmScrollY有值说明有发生滚动,就先移动画布之后再进行绘制。接下来看一下onDraw方法:

  protected void onDraw(Canvas canvas) {
  }
复制代码

onLayout一样也是一个空方法,不同View需要根据需求实现不同内容的绘制。再下来是dispatchDraw方法。

  protected void dispatchDraw(Canvas canvas) {
  }
复制代码

dispatchDraw同样还是空方法,还是因为单独普通的View是没有子View的所以不需要绘制子View。最后来看onDrawForeground方法绘制装饰。

 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);
        }
    }
复制代码

onDrawForeground方法中主要是绘制了滚动指示器、滚动条和前景相关内容。Viewdraw流程就是这些,流程图如下。

View的draw流程

3、ViewGroup的绘制流程

3.1 ViewGroup的measure流程

ViewGroup的测量流程的起始和一般的View是一样的,也是从measure开始,但是ViewGroup类中并没有复写measure方法,所以调用的也就是其父类View中的measure方法,所以这一部分ViewGroup与普通View是相同的。之后我们知道在measure方法中会调用onMeasure方法进行测量。但是默认ViewGroup类中依旧没有复写onMeasure方法,这是因为每个ViewGroup的特性不同需求不同,需要根据要求自己复写onMeasure方法。onMeasure方法的复写逻辑是这样的:首先在onMeasure方法中需要测量每个子View的大小,接着计算所有子View总的大小,最后通过setMeasuredDimension方法,将得到的总大小设置保存到成员变量中,完成测量。

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        //所有子View数组
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                //测量每个子View大小,传入子View和父布局大小
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }
复制代码

ViewGroup提供有measureChildren这个用来测量子View的方法。在measureChildren方法中获得了所有子View的数组,然后调用measureChild方法,测量每个子View

 protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
         //获得子View的LayoutParams
        final LayoutParams lp = child.getLayoutParams();
        // 根据父布局的MeasureSpec和子View的LayoutParams,计算子View的MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        //调用子View的measure方法
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
复制代码

measureChild方法中获取了子ViewLayoutParam布局参数。然后通过getChildMeasureSpec方法传入父布局的MeasureSpec和子ViewLayoutParams获取到子ViewMeasureSpec,最后调用了子Viewmeasure方法,完成最后的测量。那么下面进入getChildMeasureSpec方法,看一下实现。

  /**
   * 这三个参数分别为:
   * spec:父布局的MeasureSpec
   * padding:内边距
   * childDimension:子View的宽高尺寸
   */
  public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //获取父布局的specMode和specSize
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        //计算父布局大小:用父布局的specSize减去内边距
        int size = Math.max(0, specSize - padding);
        int resultSize = 0;
        int resultMode = 0;
        //根据父布局的specMode判断
        switch (specMode) {
        case MeasureSpec.EXACTLY:
            //父布局的specMode为EXACTLY时
            if (childDimension >= 0) {
                //childDimension大于0即子View的宽或高有具体的值
                //返回的大小就为childDimension大小,mode为EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //如果子View宽或高为MATCH_PARENT
                //返回的大小就为父布局大小,mode为EXACTLY
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //如果子View宽或高WRAP_CONTENT
                //返回的大小就由子View自己确定,最大不能超过父布局大小,mode所以为AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        //父布局的specMode为AT_MOST时
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                //childDimension大于0即子View的宽或高有具体的值
                //返回的大小就为childDimension大小,mode为EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //如果子View宽或高为MATCH_PARENT
                //返回的大小就为父布局大小,mode为AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //如果子View宽或高WRAP_CONTENT
                //返回的大小就由子View自己确定,最大不能超过父布局大小,mode所以为AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        //父布局的specMode为UNSPECIFIED时
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                //childDimension大于0即子View的宽或高有具体的值
                //返回的大小就为childDimension大小,mode为EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //如果子View宽或高为MATCH_PARENT
                //返回的大小根据View.sUseZeroUnspecifiedMeasureSpec判断,mode为UNSPECIFIED
                //sUseZeroUnspecifiedMeasureSpec是View类的成员变量由targetSdkVersion版本决定
                //sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < Build.VERSION_CODES.M
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //如果子View宽或高为WRAP_CONTENT
                //返回的大小根据View.sUseZeroUnspecifiedMeasureSpec判断,mode为UNSPECIFIED
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //调用makeMeasureSpec方法创建一个MeasureSpec返回
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
复制代码

从这个方法可以看出来一个子ViewMeasureSpec是由其父布局的MeasureSpec和自身的LayoutParams共同决定的,根据不同的组合最终返回不同的结果。在测量完所有的子View之后,需要实现计算ViewGroup总大小,最后调用setMeasuredDimension方法存储测量的结果就可以了。ViewGroupmeasure流程图如下。

ViewGroup的measure流程

3.2 ViewGroup的layout流程

和单个普通View一样,ViewGrouplayout流程同样从layout方法开始。

 @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b);
        } else {
            mLayoutCalledWhileSuppressed = true;
        }
    }
    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);
复制代码

ViewGroup中复写了layout方法,添加了一些动画过度效果的判断,核心还是调用了super.layout方法,继而会调用onLayout方法,onLayout方法是一个抽象方法,这是因为ViewGroup同样要根据自身要求特性实现不同的layout逻辑,所以需要由不同的ViewGroup自身来实现onLayout方法。接下来看一下具体的一个ViewGroup中的onLayout方法,LinearLayout中的onLayout方法。

  @Override
    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);
        }
    }
复制代码

根据不同方向分为两个方法,这里来看layoutVertical方法:

void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;
        int childTop;
        int childLeft;
        // 计算子View最右的位置
        final int width = right - left;
        int childRight = width - mPaddingRight;
        // 子View的空间
        int childSpace = width - paddingLeft - mPaddingRight;
        // 获得子View个数
        final int count = getVirtualChildCount();
        ......
        // 循环遍历子View
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                // 可见性不为GONE的子View获得其宽高
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
                // 获得子View的LayoutParams
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();
                ......
                //计算topMargin
                childTop += lp.topMargin;
                //调用setChildFrame方法,该方法中调用了子View的layout方法来确定位置
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                // childTop增加子View的高度和bottomMargin,使下一个子View垂直在下方放置
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

复制代码

这里省略了Gravity属性的相关代码,来看主要的流程,首先是去除内边距计算好子View的放置空间,然后遍历子View获得子View的宽高,再通过setChildFrame方法确定位置,然后递增childTop,这样接计算好了子View的位置,使子View一个个垂直排列下去。LinearLayoutonLayout方法主要实现方法就是这样,其他不同的ViewGroup也都实现了自己的onLayout方法。

ViewGroup的layout流程

3.3 ViewGroup的draw流程

ViewGroup绘制流程同样会调用Viewdraw方法,同样会绘制背景,绘制自身内容等,不同的是dispatchDraw方法,View中该方法是一个空方法,但是ViewGroup中就需要复写这个方法,来看ViewGroup中的dispatchDraw方法。

  @Override
    protected void dispatchDraw(Canvas canvas) {
        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
        //获得所有子View个数和子View数组
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        int flags = mGroupFlags;

        ......
        
        //循环遍历子View
        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) {
                    // 调用drawChild方法绘制子View
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }

       ......
    }
复制代码

dispatchDraw方法里核心逻辑还是循环遍历了子View,然后调用了drawChild绘制子View,而drawChild中又调用了子Viewdraw方法绘制子View

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

ViewGroup的draw流程

4、总结

这一篇主要讲了ViewViewGroup的绘制流程,无论ViewViewGroup都经历了measurelayoutdraw这三个阶段。之间主要的区别就是View没有子View的问题,而ViewGroup的绘制流程中还需要调用其子View的对应流程,完成子View的绘制。那么在了解了ViewViewGroup的绘制流程之后,新的问题又来了。View树中,子View的了measurelayoutdraw方法是由其父级的ViewGroup调用的,父ViewGroup绘制又是由它的父级调用的,那么根节点的绘制流程是由谁调用开启的呢?绘制好的界面又是怎样显示的呢?最后一篇文章就来看看这些内容。

关注下面的标签,发现更多相似文章
评论