android自定义view流程与源码解析

对于我们每一个android工程师来说,自定义view对我们来说都是开发中不得不面对的问题,而且面对现在五花八门的应用市场,而作为进阶高级工程师的我们,自定义view将成为我们不得不掌握的一大知识。而自定义view的整个流程又可以分为measure,layout与draw三个阶段。本文将从源码与实际应用的角度去解读自定义view的整个流程以及实现过程。

而view的绘制主要是从viewRootImpl的performTraversals()开始的,而viewRootImpl的定义是从view被加入到window开始的。首先让我们从源码角度来分析下:

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
    }
    if (display == null) {
        throw new IllegalArgumentException("display must not be null");
    }
    if (!(params instanceof WindowManager.LayoutParams)) {
        throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
    }

    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    if (parentWindow != null) {
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    } else {
        // If there's no parent, then hardware acceleration for this view is
        // set from the application's hardware acceleration setting.
        final Context context = view.getContext();
        if (context != null
                && (context.getApplicationInfo().flags
                        & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
            wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
        }
    }

    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {
        // Start watching for system property changes.
        if (mSystemPropertyUpdater == null) {
            mSystemPropertyUpdater = new Runnable() {
                @Override public void run() {
                    synchronized (mLock) {
                        for (int i = mRoots.size() - 1; i >= 0; --i) {
                            mRoots.get(i).loadSystemProperties();
                        }
                    }
                }
            };
            SystemProperties.addChangeCallback(mSystemPropertyUpdater);
        }

        int index = findViewLocked(view, false);
        if (index >= 0) {
            if (mDyingViews.contains(view)) {
                // Don't wait for MSG_DIE to make it's way through root's queue.
                mRoots.get(index).doDie();
            } else {
                throw new IllegalStateException("View " + view
                        + " has already been added to the window manager.");
            }
            // The previous removeView() had not completed executing. Now it has.
        }

        // If this is a panel window, then find the window it is being
        // attached to for future reference.
        if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
            final int count = mViews.size();
            for (int i = 0; i < count; i++) {
                if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                    panelParentView = mViews.get(i);
                }
            }
        }

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

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
    }

    // do this last because it fires off messages to start doing things
    try {
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        synchronized (mLock) {
            final int index = findViewLocked(view, false);
            if (index >= 0) {
                removeViewLocked(index, true);
            }
        }
        throw e;
    }
}

我们会在这里去创建我们的ViewRootImpl对象,并将传进来的view对象交给传给ViewRootImpl对象,即:

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

而在setView方法中,我们很快找到了熟悉的标记,requestLayout()方法,这个方法主要用于view的重新布局与重绘:

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

我们继续跟踪:

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

首先会进行线程判断checkThread():

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

这里我们可以看到,通过checkThread可以检测到,只有创建view的地方才可以操作view相关的内容,而我们的view是从window中添加进来的,所以这里的thread就是我们的主线程了,这就是为什么我们不能通过子线程更新UI的缘故。

接下来我们继续向下看scheduleTraversals()的实现:

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

我们主要看一下下面的方法实现,因为他跟我们接下来的绘制息息相关:

 mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

而这里的mTraversalRunnable最终会被执行,mTraversalRunnable是一个Runnable对象:

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

我们接下来看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()方法,我们挑重点来看:

private void performTraversals() {

...

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

...

performLayout(lp, mWidth, mHeight);

...

performDraw();

...

}

我们接着来看performMeasure()中的实现:

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

这个时候我们看到viewRootImpl将对控件的测量传递到了view中,而mView正是我们之前看到的setView()传过来的View对象。接下来我们继续跟踪measure()中的实现:

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

...

onMeasure(widthMeasureSpec, heightMeasureSpec);

...

}

到这里我们应该就比较熟悉了,我们在自定义view处理时经常会重写onMeasure()方法,我们来看看onMeasure()中的实现:

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

只有一行代码,根据方法名我们也可以看出,这里会直接将我们测量得到的结果设置给变量。而参数是一个方法名,那我们很有必要看一下它的方法实现:

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

到这里我们看到,通过specMode会为我们的result重新赋值。这里简单说一下measureSpec:

measureSpec是一个32位表示的整数,前两位代表了specMode的模式,通过MeasureSpec.getMode(measureSpec)可获取到;后30为表示它的测量大小,通过MeasureSpec.getSize(measureSpec)可得到。measureSpec一共有3种模式:

MeasureSpec.UNSPECIFIED:表示一种不确定状态,一般用于系统内部的测量过程,这种情况下的大小为getSuggestedMinimumWidth()或getSuggestedMinimumHeight()的值,这两个方法后面再说;

MeasureSpec.AT_MOST:这种状态下表示view会充满它的父控件的剩余空间;

MeasureSpec.EXACTLY: 这种状态表示一种确定的状态,即view的宽度或高度是一个确定值,或者充满其父控件的剩余空间。

接下来我们看一下getSuggestedMinimumWidth(),由于getSuggestedMinimumHeight()实现方式一样,所以这里只看getSuggestedMinimumWidth()中的实现:

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

首先会判断view是否设置有mBackground属性,如果没有设置就会返回mMinWidth的值,而mMinWidth对应minWidth属性,默认为0;如果设置有mBackground属性,那么会获取到minWidth与mBackground的宽度的最大值,我们看一下mBackground.getMinimumWidth()方法:

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

这里实现的是Drawable的getIntrinsicWidth()方法,表示Drawable的内在宽度,而Drawable是一个抽象类,它的实现主要依赖于其子类,比如默认情况下,ShapeDrawable,ColorDrawable是没有宽高的,而BitmapDrawable是有宽高的,其宽高是图片的宽高。

那么现在我们知道了,当specMode为UNSPECIFIED时,默认设置的是mMinWidth与mBackground的最大值。

这里是我们整个view的measure过程。

我们接下来看view的layout过程:

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

...

final View host = mView;

...

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

...

}

其实最终还是调用到了mView的layout方法,这里传进来的就是我们之前测量好的width和height了(并非最终的width和height),我们来看一下它的layout中的实现:

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

    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

这段代码就比较清晰了,其实通过view的四个边缘的坐标确定它的位置,而这里的实现主要是通过setFrame()方法完成的:

protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;

    if (DBG) {
        Log.d("View", this + " View.setFrame(" + 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;
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

        // Invalidate our old position
        invalidate(sizeChanged);

        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

        mPrivateFlags |= PFLAG_HAS_BOUNDS;


        if (sizeChanged) {
            sizeChange(newWidth, newHeight, oldWidth, oldHeight);
        }

        if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
            // If we are visible, force the DRAWN bit to on so that
            // this invalidate will go through (at least to our parent).
            // This is because someone may have invalidated this view
            // before this call to setFrame came in, thereby clearing
            // the DRAWN bit.
            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;
        if (mForegroundInfo != null) {
            mForegroundInfo.mBoundsChanged = true;
        }

        notifySubtreeAccessibilityStateChangedIfNeeded();
    }
    return changed;
}

这里会为view的mLeft,mTop,mRight,mBottom重新赋值为我们传进来的参数,而这里的赋值我们看到mLeft和mTop都为0,而mRight和mBottom是我们之前传进来的getMeasuredWidth()和getMeasuredHeight(),就是接下来会调用到onLayout中,然而onLayout并没有我们想象中那样有一堆实现,它是一个空方法,是系统提供给我们用来实现我们自定义view的一个接口。

通过以上的分析流程,我们就可以比较明确的知道,如果要获取一个view的宽度(或高度) ,通过getMeasuredWidth()和getWidth()获得的值是一样的:

public final int getWidth() {
    return mRight - mLeft;
}

那么为什么还要区分getMeasuredWidth()和getWidth()这两个方法呢?

首先,当view的measure过程完成时,那么就可以获取到它的getMeasuredWidth()了,但只有layout完成时(具体说是它的setFrame()完成时),才能通过getWidth()获取到它的值。

第二点就是由于在layout阶段系统为我们提供了onLayout回调,并传递了它的四个边界参数给我们,那么当我们通过在回调中调用onLayout(l, t, 1+50, t+50),那么我们就会为view的实际宽高增加了50px,这个时候它的实际宽高就会与测量值不一致。

这是我们view的layout流程。

我们接下来分析它的draw过程:

draw过程其实与measure,layout过程一样,开始于performTraversals的performDraw,我们接下来看performDraw的过程:

private void performDraw() {

...

draw(fullRedrawNeeded);

...

}

这里我们只看和绘制相关的流程,在performDraw()中调用了draw()方法,我们继续跟踪:

private void draw(boolean fullRedrawNeeded) {

...

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

...

}

这里会调用到它的drawSoftware()方法,而view的绘制也是从这里开始的:

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

...

mView.draw(canvas);

...

}

到这里我们就可以比较清楚的看到,最终还是调用了mView的draw()进行处理,而mView我们之前已经分析过了,它是通过WM通过addWindow()加载进来的view,最后通过ViewRootImpl的setView()设置给了mView.那我们继续来看view的draw()方法的实现:

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

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

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

    /*
     * Here we do the full fledged routine...
     * (this is an uncommon case where speed matters less,
     * this is why we repeat some of the tests that have been
     * done above)
     */

    boolean drawTop = false;
    boolean drawBottom = false;
    boolean drawLeft = false;
    boolean drawRight = false;

    float topFadeStrength = 0.0f;
    float bottomFadeStrength = 0.0f;
    float leftFadeStrength = 0.0f;
    float rightFadeStrength = 0.0f;

    // Step 2, save the canvas' layers
    int paddingLeft = mPaddingLeft;

    final boolean offsetRequired = isPaddingOffsetRequired();
    if (offsetRequired) {
        paddingLeft += getLeftPaddingOffset();
    }

    int left = mScrollX + paddingLeft;
    int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
    int top = mScrollY + getFadeTop(offsetRequired);
    int bottom = top + getFadeHeight(offsetRequired);

    if (offsetRequired) {
        right += getRightPaddingOffset();
        bottom += getBottomPaddingOffset();
    }

    final ScrollabilityCache scrollabilityCache = mScrollCache;
    final float fadeHeight = scrollabilityCache.fadingEdgeLength;
    int length = (int) fadeHeight;

    // clip the fade length if top and bottom fades overlap
    // overlapping fades produce odd-looking artifacts
    if (verticalEdges && (top + length > bottom - length)) {
        length = (bottom - top) / 2;
    }

    // also clip horizontal fades if necessary
    if (horizontalEdges && (left + length > right - length)) {
        length = (right - left) / 2;
    }

    if (verticalEdges) {
        topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
        drawTop = topFadeStrength * fadeHeight > 1.0f;
        bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
        drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
    }

    if (horizontalEdges) {
        leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
        drawLeft = leftFadeStrength * fadeHeight > 1.0f;
        rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
        drawRight = rightFadeStrength * fadeHeight > 1.0f;
    }

    saveCount = canvas.getSaveCount();

    int solidColor = getSolidColor();
    if (solidColor == 0) {
        final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

        if (drawTop) {
            canvas.saveLayer(left, top, right, top + length, null, flags);
        }

        if (drawBottom) {
            canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
        }

        if (drawLeft) {
            canvas.saveLayer(left, top, left + length, bottom, null, flags);
        }

        if (drawRight) {
            canvas.saveLayer(right - length, top, right, bottom, null, flags);
        }
    } else {
        scrollabilityCache.setFadeColor(solidColor);
    }

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

    if (drawTop) {
        matrix.setScale(1, fadeHeight * topFadeStrength);
        matrix.postTranslate(left, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(left, top, right, top + length, p);
    }

    if (drawBottom) {
        matrix.setScale(1, fadeHeight * bottomFadeStrength);
        matrix.postRotate(180);
        matrix.postTranslate(left, bottom);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(left, bottom - length, right, bottom, p);
    }

    if (drawLeft) {
        matrix.setScale(1, fadeHeight * leftFadeStrength);
        matrix.postRotate(-90);
        matrix.postTranslate(left, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(left, top, left + length, bottom, p);
    }

    if (drawRight) {
        matrix.setScale(1, fadeHeight * rightFadeStrength);
        matrix.postRotate(90);
        matrix.postTranslate(right, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(right - length, top, right, bottom, p);
    }

    canvas.restoreToCount(saveCount);

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

其实从整个view的draw()过程中,我们可以看到view的绘制顺序:

1.首先会进行背景的绘制,即drawBackground();
2.绘制内容,即onDraw();
3.绘制子类(如果存在的话),dispatchDraw();
4.绘制进度条,通过canvas.drawRect()实现;
5.绘制前景,即onDrawForeground()。

往往后绘制的内容会覆盖先绘制的内容,所以我们可以根据绘制顺序完成我们对目标view的绘制,这里面除了背景的绘制是私有方法我们不能重写外,其它的我们都可以自己实现。

另外,我们可以通过通过设置view不具备自身的绘制功能,这样可以大大提高我们的运行效率:

setWillNotDraw(true);

如果我们在自定义view中这样设置,系统会进行自我优化,不会对自身进行绘制。默认情况下,view是没有启用这个标志的,而viewGroup默认会启用。

关于自定义view的源码部分我们已经了解的差不多了,但很多时候并不是一个单单的自定义view就能搞定我们的需求,我们还有必要看一下自定义ViewGroup中的一些知识:

由于ViewGroup的实现基本都是由具体的子类实现,而针对于不同的布局其实现流程又差别太大,所以我们这里只讨论ViewGroup的一个实现过程,而对于ViewGroup的measure过程,其实就是对子view的一个遍历measure过程,这一点我们可以从下面的代码中看到:

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

我们就以width为例,继续来看childWidthMeasureSpec是如何获取的:

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

从参数我们就可以知道:
spec表示父view的容器大小,即parentWidthMeasureSpec;
padding表示父view的内间距与已使用空间与子view的margin之和;
childDimension表示子view在布局中设置的宽度。

从代码中我们可以看到,子view的measureSpec实际上由父view的MeasureSpec与子view的LayoutParams共同决定。

  1. 当父view为EXACTLY模式时,如果子view的size不小于0(具体数值),即childDimension不小于0,那么子view的模式也是EXACTLY,大小为childDimension;如果childDimension为LayoutParams.MATCH_PARENT,那么子view的模式为EXACTLY,大小为填满父view的剩余空间;如果childDimension为LayoutParams.WRAP_CONTENT,那么子view的模式为AT_MOST,大小为填满父view的剩余空间。

  2. 当父view为AT_MOST模式时,如果子view的size不小于0(具体数值),即childDimension不小于0,那么子view的模式是EXACTLY,大小为childDimension;如果childDimension为LayoutParams.MATCH_PARENT,那么子view的模式为AT_MOST,大小为填满父view的剩余空间;如果childDimension为LayoutParams.WRAP_CONTENT,那么子view的模式为AT_MOST,大小为填满父view的剩余空间。

  3. 当父view为UNSPECIFIED模式时,如果子view的size不小于0(具体数值),即childDimension不小于0,那么子view的模式是EXACTLY,大小为childDimension;如果childDimension为LayoutParams.MATCH_PARENT,那么子view的模式为UNSPECIFIED,大小取决于sUseZeroUnspecifiedMeasureSpec属性,默认为false,当targetSdkVersion < 23为true,此时大小为0,否则填满父view的剩余空间;如果childDimension为LayoutParams.WRAP_CONTENT,那么子view的模式为UNSPECIFIED,大小取决于sUseZeroUnspecifiedMeasureSpec属性,默认为false,当targetSdkVersion < 23为true,此时大小为0,否则填满父view的剩余空间。

上面三条便是view的MeasureSpec的创建规则,这个时候我们要注意,如果我们的自定义view在布局中设置为wrap_content时,并不能达到我们想要的效果,不管父view是什么模式,基本上都会充满父view的剩余空间,所以针对这种情况我们要做特殊处理。

当完成对子view的measureSpec的构造之后,最终会调用到子view的measure(),完成子view的一个measure过程。而要完成所有子view的measure过程,需要在viewGroup的子类中进行循环遍历,如果child不为gone,那么就会进行这样一个measure的过程。

ViewGroup的layout过程也是由不同的子类实现,除了完成自身的一个layout,还要完成所有子 view的一个layout过程。另外ViewGroup的onLayout()是一个抽象方法,我们如果需要自定义ViewGroup时,就必须实现它的onLayout()方法。

另外ViewGroup是没有自己的draw()实现的,它的draw过程主要是对子view的一个draw过程,类似于measure与layout的过程,ViewGroup主要实现了dispatchDraw()方法,来完成对所有子view的一个draw()过程。

ViewGroup的一个自定义过程到这里就结束了,有兴趣的同学可以针对具体的ViewGroup做具体分析。

感谢大家的阅读!

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

推荐阅读更多精彩内容