Android自定义View专题一 UI绘制流程

一、从setContentView(R.layout.activity_main);入手了解UI的绘制起始过程

1.Activity.java

public void setContentView(@LayoutRes int layoutResID) {

getWindow().setContentView(layoutResID);//①

 initWindowDecorActionBar();

}

2.getWindow()拿到的是Window的实现类PhoneWindow

mWindow = new PhoneWindow(this, window);

PhoneWindow源码:

在 com.android.internal.policy包下面

1.看源码需要找到程序的入口,这个入口就是setContentView,找到入口了以后根据线索往下看。

@Override

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) {
      //首先看到了这个方法,就是初始化DecorView
      installDecor();//②
}

……
//这个方法是将我们的布局加载到mContentParent当中
//我们还需要知道mContentParent是如何加载的,以及mContentParent是什么?带着疑问往下看
mLayoutInflater.inflate(layoutResID,mContentParent
    //⑥最后将布局渲染到帧布局当中。
}

2.installDecor这个方法是初始化DecorView,以及mContentParent。

private void installDecor() {

if (mDecor == null) {

    mDecor = generateDecor();//③生成一个DecorView(继承的FrameLayout)
  mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);

  mDecor.setIsRootNamespace(true);

if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
      mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
  }
}
if (mContentParent == null) {
      mContentParent = generateLayout(mDecor);//④
}

3.generateLayout()这个方法就是根据我们设置的主题类型,来加载相应的布局,将状态栏以及标题栏以及一个加载我们布局的帧布局加载到DecorView当中。

    protected ViewGroup generateLayout(DecorView decor) {//⑤

        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
              layoutResource = R.layout.screen_swipe_dismiss;
      } 
.............
      上面判断加载哪一种布局,有的有actionBar 有的没有,还有其他种类的布局,然后加载添加到decor中。
下图中就是一种简单的布局。不含有ActionBar,但是有一个ViewStub方便以后添加标题栏

View in = mLayoutInflater.inflate(layoutResource, null);

decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

mContentRoot = (ViewGroup) in;

//然后将我们的布局填充到这个布局当中
        View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) in;
    }
}

1_Activity加载UI-类图关系和视图结构.png

mContentRoot 就是图中的FrameLayout。到此就将系统中的布局加载完成,然后回到上面调用mLayoutInflater.inflate(layoutResID,mContentParent{}这个方法,将我们的布局渲染到系统当中。

D:\Software\Android\SDK\platforms\android-23\data\res ,查看系统的资源文件

PS: 这里我们来看一下snackbar添加到布局中的原理,是不是很熟悉。

 private static ViewGroup findSuitableParent(View view) {
        ViewGroup fallback = null;
        do {
            if (view instanceof CoordinatorLayout) {
                // We've found a CoordinatorLayout, use it
                return (ViewGroup) view;
            } else if (view instanceof FrameLayout) {
                if (view.getId() == android.R.id.content) {
                     //见上图中的id/content 将SnackBar添加到DecorView中FrameLayout
                    // If we've hit the decor content view, then we didn't find a CoL in the
                    // hierarchy, so use it.
                    return (ViewGroup) view;
                } else {
                    // It's not the content view but we'll use it as our fallback
                    fallback = (ViewGroup) view;
                }
            }

            if (view != null) {
                // Else, we will loop and crawl up the view hierarchy and try to find a parent
                final ViewParent parent = view.getParent();
                view = parent instanceof View ? (View) parent : null;
            }
        } while (view != null);

        // If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
        return fallback;
    }

二、ViewRootImpl

View类中的三个方法比较重要

measure:测量,测量自己有多大,如果是ViewGroup的话会同时测量里面的子控件的大小
layout:摆放里面的子控件bounds(left,top,right,bottom)
draw:绘制 (直接继承了view一般都会重写onDraw)

但是这三个方法是在什么时候,在哪里被调用的呢?这个时候ViewRootImpl就登场了,首先看performTraversals()方法

1.performTraversals()

这里先贴一下伪代码

performTranversal(){
// Ask host how big it wants to be
    ·········
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    performDraw();
    ·········
}

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            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;
        ·······  
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        ·······    
        mInLayout = false;
    }

draw方法需要经过几个方法的调用,代码太多这里就不贴了,有兴趣的可以去看一下源码。

performTraversals方法控制View绘制流程图.jpg
这里们我们知道了ViewRootImpl如何调用View的几个方法,但是他们是如何绑定在一起的呢?
带着疑问我们接着看。

2.View与ViewRootImpl的绑定

另外View和ViewRootImpl是怎么绑定在一起的呢?通过view.getViewRootImpl可以获取到ViewRootImpl。

public ViewRootImpl getViewRootImpl() {
        if (mAttachInfo != null) {
            return mAttachInfo.mViewRootImpl;
        }
        return null;
    }

而这个AttachInfo则是View里面一个静态内部类,它的构造方法

AttachInfo(IWindowSession session, IWindow window, Display display,
                ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer) {
            mSession = session;
            mWindow = window;
            mWindowToken = window.asBinder();
            mDisplay = display;
            mViewRootImpl = viewRootImpl;
            mHandler = handler;
            mRootCallbacks = effectPlayer;
        }

可以看到viewRootImpl在它的构造方法里赋值了,那么这个方法肯定是在ViewRootImpl创建时创建的,而ViewRootImpl的创建是在调用WindowManagerGlobal.addView的时候

     root = new ViewRootImpl(view.getContext(), display);

而构造方法中

 public ViewRootImpl(Context context, Display display) {
        mContext = context;
        mWindowSession = WindowManagerGlobal.getWindowSession();
        ...
        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
        ...
    }

可以看到View与ViewRootImpl绑定一起了。
之后就可以通过view.getViewRootImpl获取到。这样我们就差不多了解到View是怎么绑定到ViewRootImpl,以及绘制的流程。

3.view的requestLayout()方法

这个方法大家肯定是非常的熟悉,因为在自定义控件的时候会经常调用这个方法。拿它是怎样刷新视图的呢?
先看一下代码

@CallSuper
    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            // Only trigger request-during-layout logic if this is the view requesting it,
            // not the views in its parent hierarchy
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }

        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }

我们看到了mParent.requestLayout();这个方法,看到这里肯定在想mParent是什么?我们看一下它的定义

    /**
     * The parent this view is attached to.
     * {@hide}
     * @see #getParent()
     */
    protected ViewParent mParent;

了解后发现ViewParent 是一个接口,需要知道它有哪些实现类,以及mParent是如何赋值的,阅读代码后发现ViewRootImpl正好是他的一个实现类,同时mParent只有一个赋值的方法:

    /*
     * Caller is responsible for calling requestLayout if necessary.
     * (This allows addViewInLayout to not request a new layout.)
     */
    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");
        }
    }

如果我们知道这个方法在哪调用,我们离知道requestLayout()是如何刷新视图的真相不远了,联想到ViewRootImpl是ViewParent的实现类,带着激动的心情马上去看一下。

  public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    view.assignParent(this);
 }     

原来那个mParent就是ViewRootImpl这个类。这一下就很清晰了。
然后我们再看看ViewRootImpl#requestLayout方法。看它是如何重汇整个视图的。
ViewRootImpl调用到requestLayout()来完成View的绘制操作,我们看下源码

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

View绘制,先判断当前线程

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

如果不是当前线程则抛出异常,这个异常是不是感觉很熟悉啊。没错,当你在子线程更新UI没使用handler的话就会抛出这个异常

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

抛出地方就是这里,一般在子线程操作UI都会调用到view.invalidate,而View的重绘会触发ViewRootImpl的requestLayout,就会去判断当前线程。

接着看,判断完线程后,接着调用scheduleTraversals()

  void scheduleTraversals() {
        if (!mTraversalScheduled) {
            ...
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
          ...
        }
    }

scheduleTraversals中会通过handler去异步调用mTraversalRunnable接口

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

接着

  void doTraversal() {
            ...
            performTraversals();
            ...
    }

可以看到,最后真正调用绘制的是performTraversals()方法。看到这里仿佛打通了任督二脉。太通畅了。终于明白整个绘制流程了。
到这里我们就知道了View的几个方法是如何被调用的,下面介绍一下View方法具体的执行流程。

4.View的measure方法

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

View的measure方法里面会调用onMeasure方法

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

        ............
        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
            resolveRtlPropertiesIfNeeded();
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
        }   
      .........
    }

onMeasure根据传入父类的MeasureSpec,得到自身的大小。

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

子类的MeasureSpec由父类和LayoutParams一起决定。
getChildMeasureSpec方法分析.png
下面我们再来看看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;
    }

结合上图可以知道如何测量出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;
    }
   public int getIntrinsicWidth() {
        return -1;
    }

这种情况是确保View设置了背景也能准确的测量出View的大小。

5 ViewGroup的测量流程

View树的源码measure流程图.png

在View的测量流程中,通过makeMeasureSpec来保存宽高信息,在其他流程通过getMode或getSize得到模式和宽高。那么问题来了,上面提到MeasureSpec是LayoutParams和父容器的模式所共同影响的,那么,对于DecorView来说,它已经是顶层view了,没有父容器,那么它的MeasureSpec怎么来的呢?
阅读源码的时候一定要带着疑问,这时候我们提出了问题,那么接下来就要在源码中找出答案。
MeasureSpec肯定是在测量之前就已经准备好,这时候我们就想什么时候开始测量的呢?上面已经提到过ViewRootImpl#PerformTraveals这个方法是UI绘制的起点,那我们就去那里看一下。果不其然,看到了我们想看的东西。

private void performTraversals() {
            ...

        if (!mStopped) {
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  // 1
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);       
            }
        } 
·······
}

/**
 * @param windowSize
 *            The available width or height of the window
 *
 * @param rootDimension
 *            The layout params for one dimension (width or height) of the
 *            window.
 *
 * @return The measure spec to use to measure the root view.
 */
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;
    //省略...

    }
    return measureSpec;
}

getRootMeasureSpec(desiredWindowWidth,lp.width)方法,其中desiredWindowWidth就是屏幕的尺寸,并把返回结果赋值给childWidthMeasureSpec成员变量(childHeightMeasureSpec同理),因此childWidthMeasureSpec(childHeightMeasureSpec)应该保存了DecorView的MeasureSpec。

下面我们来看一下ViewGroup的测量过程,ViewGroup是继承了View,调用的是View的measure方法,我们主要看一下ViewGroup的onMeasure方法,就以LinearLayout为例:

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

        boolean matchWidth = false;
        boolean skippedMeasure = false;

        final int baselineChildIndex = mBaselineAlignedChildIndex;        
        final boolean useLargestChild = mUseLargestChild;

        int largestChildHeight = Integer.MIN_VALUE;
        int consumedExcessSpace = 0;

        // See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }

            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);
               continue;
            }

            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            totalWeight += lp.weight;

            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                // Optimization: don't bother measuring children who are only
                // laid out using excess space. These views will get measured
                // later if we have space to distribute.
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                if (useExcessSpace) {
                    // The heightMode is either UNSPECIFIED or AT_MOST, and
                    // this child is only laid out using excess space. Measure
                    // using WRAP_CONTENT so that we can find out the view's
                    // optimal height. We'll restore the original height of 0
                    // after measurement.
                    lp.height = LayoutParams.WRAP_CONTENT;
                }

                // Determine how big this child would like to be. If this or
                // previous children have given a weight, then we allow it to
                // use all available space (and we will shrink things later
                // if needed).
                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                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;
                }

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }

            /**
             * If applicable, compute the additional offset to the child's baseline
             * we'll need later when asked {@link #getBaseline}.
             */
            if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
               mBaselineChildTop = mTotalLength;
            }

            // if we are trying to use a child index for our baseline, the above
            // book keeping only works if there are no children above it with
            // weight.  fail fast to aid the developer.
            if (i < baselineChildIndex && lp.weight > 0) {
                throw new RuntimeException("A child of LinearLayout with index "
                        + "less than mBaselineAlignedChildIndex has weight > 0, which "
                        + "won't work.  Either remove the weight, or don't set "
                        + "mBaselineAlignedChildIndex.");
            }

            boolean matchWidthLocally = false;
            if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
                // The width of the linear layout will scale, and at least one
                // child said it wanted to match our width. Set a flag
                // indicating that we need to remeasure at least that view when
                // we know our width.
                matchWidth = true;
                matchWidthLocally = true;
            }

            final int margin = lp.leftMargin + lp.rightMargin;
            final int measuredWidth = child.getMeasuredWidth() + margin;
            maxWidth = Math.max(maxWidth, measuredWidth);
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
            if (lp.weight > 0) {
                /*
                 * Widths of weighted Views are bogus if we end up
                 * remeasuring, so keep them separate.
                 */
                weightedMaxWidth = Math.max(weightedMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            } else {
                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            }

            i += getChildrenSkipCount(child, i);
        }

        if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
            mTotalLength += mDividerHeight;
        }

        if (useLargestChild &&
                (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);
                if (child == null) {
                    mTotalLength += measureNullChild(i);
                    continue;
                }

                if (child.getVisibility() == GONE) {
                    i += getChildrenSkipCount(child, i);
                    continue;
                }

                final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                        child.getLayoutParams();
                // Account for negative margins
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }
        }

        // Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;

        int heightSize = mTotalLength;

        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

        // Reconcile our calculated size with the heightMeasureSpec
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;

        // Either expand children with weight to take up available space or
        // shrink them if they extend beyond our current bounds. If we skipped
        // measurement on any children, we need to measure them now.
        int remainingExcess = heightSize - mTotalLength
                + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
        if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
            float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);
                if (child == null || child.getVisibility() == View.GONE) {
                    continue;
                }

                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final float childWeight = lp.weight;
                if (childWeight > 0) {
                    final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
                    remainingExcess -= share;
                    remainingWeightSum -= childWeight;

                    final int childHeight;
                    if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
                        childHeight = largestChildHeight;
                    } else if (lp.height == 0 && (!mAllowInconsistentMeasurement
                            || heightMode == MeasureSpec.EXACTLY)) {
                        // This child needs to be laid out from scratch using
                        // only its share of excess space.
                        childHeight = share;
                    } else {
                        // This child had some intrinsic height to which we
                        // need to add its share of excess space.
                        childHeight = child.getMeasuredHeight() + share;
                    }

                    final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            Math.max(0, childHeight), MeasureSpec.EXACTLY);
                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
                            lp.width);
                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

                    // Child may now not fit in vertical dimension.
                    childState = combineMeasuredStates(childState, child.getMeasuredState()
                            & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
                }

                final int margin =  lp.leftMargin + lp.rightMargin;
                final int measuredWidth = child.getMeasuredWidth() + margin;
                maxWidth = Math.max(maxWidth, measuredWidth);

                boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
                        lp.width == LayoutParams.MATCH_PARENT;

                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);

                allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }

            // Add in our padding
            mTotalLength += mPaddingTop + mPaddingBottom;
            // TODO: Should we recompute the heightSpec based on the new total length?
        } else {
            alternativeMaxWidth = Math.max(alternativeMaxWidth,
                                           weightedMaxWidth);


            // We have no limit, so make all weighted views as tall as the largest child.
            // Children will have already been measured once.
            if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
                for (int i = 0; i < count; i++) {
                    final View child = getVirtualChildAt(i);
                    if (child == null || child.getVisibility() == View.GONE) {
                        continue;
                    }

                    final LinearLayout.LayoutParams lp =
                            (LinearLayout.LayoutParams) child.getLayoutParams();

                    float childExtra = lp.weight;
                    if (childExtra > 0) {
                        child.measure(
                                MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
                                        MeasureSpec.EXACTLY),
                                MeasureSpec.makeMeasureSpec(largestChildHeight,
                                        MeasureSpec.EXACTLY));
                    }
                }
            }
        }

        if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
            maxWidth = alternativeMaxWidth;
        }

        maxWidth += mPaddingLeft + mPaddingRight;

        // Check against our minimum width
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

        if (matchWidth) {
            forceUniformWidth(count, heightMeasureSpec);
        }
    }

这个源码是比较复杂的,但是我们可以看出的是,如果自身大小是确定的,那么高度就是自身的大小,否则的话需要测量所有的子View,同时还有分割线之类的,将他们的高度叠加。设置自身的大小。

总结

一、measure的过程

如何去合理的测量一颗View树?

如果ViewGroup和View都是直接指定的宽高,我还要测量吗?

正是因为谷歌设计的自适应尺寸机制(比如Match_parent,wrap_content),造成了宽高不确定,所以就需要进程测量measure过程。

measure过程会遍历整颗View树,然后依次测量每一个View的真实的尺寸。(树的遍历–先序遍历)

MeasureSpec:测量规格

int 32位:010111100011100

拿前面两位当做mode,后面30位当做值。

1.mode:

1) EXACTLY: 精确的。比如给了一个确定的值 100dp

2) AT_MOST: 根据父容器当前的大小,结合你指定的尺寸参考值来考虑你应该是多大尺寸,需要计算(Match_parent,wrap_content就是属于这种)

3) UNSPECIFIED: 最多的意思。根据当前的情况,结合你制定的尺寸参考值来考虑,在不超过父容器给你限定的只存的前提下,来测量你的一个恰好的内容尺寸。

用的比较少,一般见于ScrollView,ListView(大小不确定,同时大小还是变的。会通过多次测量才能真正决定好宽高。)

2.value:宽高的值。

经过大量测量以后,最终确定了自己的宽高,需要调用:setMeasuredDimension(w,h)

写自定义控件的时候,我们要去获得自己的宽高来进行一些计算,必须先经过measure,才能获得到宽高—不是getWidth(),而是getMeasuredWidth()

也就是当我们重写onMeasure的时候,我们需要在里面调用child.measure()才能获取child的宽高。

从规格当中获取mode和value:

final int widthMode = MeasureSpec.getMode(widthMeasureSpec);

final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int heightSize = MeasureSpec.getSize(heightMeasureSpec);

反过来将mode和value合成一个规格呢:

MeasureSpec.makeMeasureSpec(resultSize, resultMode);

ViewGroup:

设计它的目的是什么?

1)作为容器处理焦点问题。

2)作为容器处理事件分发问题;

3)控制容器添加View的流程:addView(),removeView()

4)抽出了一些容器的公共的工具方法:measureChildren,measureChild,measureChildWidthMargins方法。

——————-重点:———————–

玩自定义控件的时候,需要进行测量measure,如何做好这件事?

两种情况:

1.继承自View的子类

只需要重写onMeasure测量好自己的宽高就可以了。

最终调用setMeasuredDimension()保存好自己的测量宽高。

套路:

    int mode = MeasureSpec.getMode(widthMeasureSpec);

    int Size = MeasureSpec.getSize(widthMeasureSpec);

    int viewSize = 0;

    switch(mode){
            case MeasureSpec.EXACTLY:
            viewSize = size;//当前view的尺寸就为父容器的尺寸
            break;
        case MeasureSpec.AT_MOST:
            viewSize = Math.min(size, getContentSize());//当前view的尺寸就为内容尺寸和费容器尺寸当中的最小值。
            break;

        case MeasureSpec.UNSPECIFIED:
            viewSize = getContentSize();//内容有多大,久设置多大尺寸。
            break;
        default:
            break;
    }

    //setMeasuredDimension(width, height);

    setMeasuredDimension(size);

2.继承自ViewGroup的子类:

不但需要重写onMeasure测量自己,还要测量子控件的规格大小。

可以直接使用ViewGroup的工具方法来测量里面的子控件,也可以自己来实现这一套子控件的测量(比如:RelativeLayout)

套路:

//1.测量自己的尺寸

ViewGroup.onMeasure();

//1.1 为每一个child计算测量规格信息(MeasureSpec)

ViewGroup.getChildMeasureSpec();

//1.2 将上面测量后的结果,传给每一个子View,子view测量自己的尺寸

child.measure();

//1.3 子View测量完,ViewGroup就可以拿到这个子View的测量后的尺寸了

child.getChildMeasuredSize();//child.getMeasuredWidth()和child.getMeasuredHeight()

//1.4ViewGroup自己就可以根据自身的情况(Padding等等),来计算自己的尺寸

ViewGroup.calculateSelfSize();

//2.保存自己的尺寸

ViewGroup.setMeasuredDimension(size);


二、layout的过程
测量完毕就是摆放的过程,View中没有Layout的方法。只有ViewGroup才会摆放子控件
三、draw的过程

ViewGroup的onDraw方法默认是不会调用的,因为在ViewGroup构造方法里面就默认设置了

setFlags(WILL_NOT_DRAW, DRAW_MASK);//原因是因为ViewGroup本来就没东西显示,除了设置了背景,这样就是为了效率。

如果需要它执行onDraw可以,设置背景或者如下:

setWillNotDraw(false);

2.自绘控件View/ViewGroup

主要重写onDraw方法绘制,还会要处理onMeasure,onLayout

3.组合控件。

比如封装通用的标题栏控件。

并不需要自己去绘制视图上面显示的内容,而只是用系统原生的控件就可以了。

但是我们可以将几个原生控件组合到一起,可以创建出的控件就是组合控件。

比如:在构造方法里面LayoutInflater.from(context).inflate(R.layout.title,this);然后再加上业务逻辑。

最后的话:有些地方还有点粗糙,后面会继续补充。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值