View的工作原理

本文深入解析了Android中View的绘制流程,包括测量、布局、绘制三个核心阶段,并详细阐述了MeasureSpec的作用及其在不同阶段的传递过程。此外,还介绍了如何在代码中准确地获取View的宽高。

RootView的三大过程(performMeasure,performLayout,performDraw)以及测量的传递

ViewRoot的实现类是ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是由ViewRoot来完成的。
在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将该
对像和DecorView关联。

  以上是对根View和其辅助类的一个介绍,具体会在下面进行展开。
   View的绘制过程是从ViewRoot的performTraversals方法开始的。在这个方法中调用了performMeasure,performLayout,performDraw方法。而这三个方法就拿performMeasure来说

...
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...


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

  首先会在getRootMeasureSpec中创建MeasureSpec,然后调用performMeasure方法。在这个方法中mView代表DecorView(在setView中被赋值,稍后分析)。也就是会对根view进行测量。DecorView继承自FrameLayout。在measure方法中会调用子view的onMeasure方法。因此View树的测量由此展开。performLayout和performDraw同理。

MeasureSpec

  MeasureSpec在根View的测量中就已经多次出现,足以说明它和View的测量有着密不可分的关系。事实上确实如此。
  MeasureSpec其实只是一个32位的整数。来看看其定义。

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    public static final int AT_MOST     = 2 << MODE_SHIFT;
    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
    public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }
    public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
        ......
}

  其主要常量和方法如上。下面来分析一下MeasureSpec的组成,观察上面代码不难发现,这个int类型的值由两部分组成,高2位代表mode,低30位代表size。也就是说用了一个int值来表示两个信息。
  还记得在performMeasure方法之前有一个获取MeasureSpec的过程,在那里就调用了makeMeasureSpec方法,拿到了返回的int值。回到上面的代码可以发现,其创建MeasureSpec时传入了两个值,屏幕宽度/高度和Layout.paramas的对应值。也就是说

根View的MeasureSpec由屏幕尺寸和其LayoutParamas决定

  在performMeasure方法中最后调用了ViewGroup的measure方法,从而实现测量的传递。并且入参有两个,分别是根view宽和高的MeasureSpec。那么我们去FrameLayout的measure方法中看一看。
  找了一下发现FrameLayout并没有重写measure方法,也就是说调用的是View的measure方法。经过观察发现在View的measure方法中调用了onMeasure方法。而FrameLayout重写了该方法。参数同样是两个MeasureSpec(可以理解成从根View传来)。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();

        ...

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                final int childWidthMeasureSpec;
                if (lp.width == LayoutParams.MATCH_PARENT) {
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }

                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }

                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

  经过分析可以知道这里的MeasureSpec是由父View传来。那么该方法中主要是对自身进行测量和遍历子View并对其测量。其中有一个点很重要。

传给子View的MeasureSpec由父容器的MeasureSpec和子View的LayoutParamas以及父容器的padding共同决定。

  总结一下MeasureSpec其实发现很简单。从根View开始,基于屏幕尺寸以及一些别的信息创建一个用于表示宽高和测量模式的int变量MeasureSpec。然后根view告诉子view,我最大只有这么大,我的除开我的padding和你要的margin,你最大能有多大。然后从根view开始递归传递。关于子view的测量模式,也是由父View及子view的LayoutParamas共同来决定的,看下面的代码:

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

MeasureSpec中的Mode

  由上一节可以知道,MeasureSpec是一个整形的变量。其中高两位表示mode,低30位表示size。那么我们来看一下分别有哪些mode和他们的区别。
  首先Mode的种类一共有三种,分别是UNSPECIFIED,EXACTLY,AT_MOST。

UNSPECIFIED

  MeasureSpec在根View的测量方法之前被创建,并且子View的MeasureSpec由父View和子View的LayoutParamas决定。
  先来看看getRootMeasureSpec方法。

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

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

  并没有发现UNSPECIFIED这种Mode,那么再去看看getChildMeasureSpec方法,代码在上一节已经贴出。可以发现确实有关于UNSPECIFIED这种Mode的判断,大概意思是如果父View的Mode是UNSPECIFIED,那么子view除非指定了宽高度,否则Mode就是UNSPECIFIED模式。
  看一下View默认会怎么处理UNSPECIFIED这种模式。

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

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

  当mode是UNSPECIFIED时,size就是传进来的size,也就是getSuggestedMinimumWidth方法的返回值(height同理)。在这个方法中判断了当前是否有背景,有的话就取android:minWidth属性的值作为size,否则取背景的宽度和minWidth值的最大值作为测量宽度。

EXACTLY

  同样分析上面的代码,当父View的MeasureSpec的Mode是EXACTLY时,子View的Mode可能有两种情况:若子View指定了宽或高,那么子View的Mode也是EXACTLY,否则若子View的宽或高是match_parent,那么子View的Mode也是EXACTLY。但是如果子View的宽或高是wrap_content,那么子View的Mode就是At_MOST。

AT_MOST

  同理,当父View的MeasureSpec-Mode是AT_MOST时,子View的Mode也有两种情况:若子View指定了宽或高,则子View的Mode是EXACTLY,若子View的宽高属性是match_parent,那么子View的mode此时会被赋值成AT_MOST。若子View的宽高属性是AT_MOST,那么和上一种情况一样,子View的Mode同样是AT_MOST。

layout和onLayout

  参考RootViewImpl中的performTraversals方法可以知道,在View树的measure之后就是layout了。和performMeasure方法类似,performLayout方法对根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;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());//layout
            mInLayout = false;
            int numViewsRequestingLayout = mLayoutRequesters.size();
            if (numViewsRequestingLayout > 0) {
                ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
                        false);
                if (validLayoutRequesters != null) {
                    mHandlingLayoutInLayoutRequest = true;
                    int numValidRequests = validLayoutRequesters.size();
                    for (int i = 0; i < numValidRequests; ++i) {
                        final View view = validLayoutRequesters.get(i);
                        Log.w("View", "requestLayout() improperly called by " + view +
                                " during layout: running second layout pass");
                        view.requestLayout();
                    }
                    measureHierarchy(host, lp, mView.getContext().getResources(),
                            desiredWindowWidth, desiredWindowHeight);
                    mInLayout = true;
                    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());//layout
                    mHandlingLayoutInLayoutRequest = false;
                    validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
                    ......
                }

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

  可以看到调用了DecorView的layout方法,并且参数是0,0,mView的测量宽度和测量高度。DecorView继承自FrameLayout,那么去ViewGroup中找找layout方法,发现调用的是View中的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);//setFrame,确定自身layout

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);//onLayout

            ......
        }
        ......
    }

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

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

  可以看到layout中主要是给mLeft,mRight,mTop,mBottom四个属性赋值,并且判断位置是否改变,并调用相应的方法。
  确定完自身的位置后,调用onLayout方法,参数是自身的left,top,right,bottom。事实上onLayout由具体的ViewGroup去实现,比如LinearLayout,作用是摆放子View,过程与测量类似,这里就不再赘述。

测量宽/高和实际宽/高

  细心的同学可能会发现,View中有两种返回控件宽/高的方法,getWidth(),getMeasuredWidth()。那么它们的区别是什么。现在已经可以解答了。

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

public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;//确保高2位都是0。
        //BufferedInputStram中read方法返回值和0xff做&运算原理是确保返回-1时代表读到流的末尾。
    }

  关于返回值做&运算看这里
  结合上面的measure,layout过程,再来看这两个方法就一目了然了。mRight和mLeft是在View的setFrame中被赋值,也就是说在layout后getWidth才能返回正确的宽度。而getMeasuredWidth中的mMeasuredWidth是在这里赋值的:

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

  这个方法在onMeasure中被调用。measuredWidth就是测量宽度。
  若出现以下情况,则测量宽/高可能和实际宽/高不等

public void layout(int l,int t,int r,int b){
    super.layout(l,t,r+100,b+100);
}

draw过程

  draw方法的传递是通过dispatchDraw来实现的。关于draw有以下几点:

1.绘制背景 background.draw(canvas);一般是Drawable
2.绘制自己onDraw(canvas);
3.绘制children(dispatchDraw);
4.绘制装饰(onDrawScrollBars);

  如果一个View不需要绘制任何内容,可以设置setWillNotDraw为true。
  下面是draw在DecorView中的调用和传递过程:

final Surface mSurface = new Surface();
......

private void performTraversals() {
    ......
    performDraw();
    ......
}

private void performDraw() {
    ......
    try {
            draw(fullRedrawNeeded);
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    ......
}

private void draw(boolean fullRedrawNeeded) {
    Surface surface = mSurface;
    ......
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                    return;
                }
}

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

        // Draw with software renderer.
        final Canvas canvas;
        try {
            final int left = dirty.left;
            final int top = dirty.top;
            final int right = dirty.right;
            final int bottom = dirty.bottom;

            canvas = mSurface.lockCanvas(dirty);//canvas从Surface里面来,Surface随着RootViewImpl一起初始化,canvas随着Surface一起初始化。

            // The dirty rectangle can be modified by Surface.lockCanvas()
            //noinspection ConstantConditions
            if (left != dirty.left || top != dirty.top || right != dirty.right
                    || bottom != dirty.bottom) {
                attachInfo.mIgnoreDirtyState = true;
            }

            // TODO: Do this in native
            canvas.setDensity(mDensity);
        } catch (Surface.OutOfResourcesException e) {
            handleOutOfResourcesException(e);
            return false;
        } catch (IllegalArgumentException e) {
            Log.e(mTag, "Could not lock surface", e);
            mLayoutRequested = true;    // ask wm for a new surface next time.
            return false;
        }

        try {
            if (DEBUG_ORIENTATION || DEBUG_DRAW) {
                Log.v(mTag, "Surface " + surface + " drawing to bitmap w="
                        + canvas.getWidth() + ", h=" + canvas.getHeight());
                //canvas.drawARGB(255, 255, 0, 0);
            }
            if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
            }

            dirty.setEmpty();
            mIsAnimating = false;
            mView.mPrivateFlags |= View.PFLAG_DRAWN;

            if (DEBUG_DRAW) {
                Context cxt = mView.getContext();
                Log.i(mTag, "Drawing: package:" + cxt.getPackageName() +
                        ", metrics=" + cxt.getResources().getDisplayMetrics() +
                        ", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo());
            }
            try {
                canvas.translate(-xoff, -yoff);
                if (mTranslator != null) {
                    mTranslator.translateCanvas(canvas);
                }
                canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
                attachInfo.mSetIgnoreDirtyState = false;

                mView.draw(canvas);//绘制递归的开始

                drawAccessibilityFocusedDrawableIfNeeded(canvas);
            } finally {
                if (!attachInfo.mSetIgnoreDirtyState) {
                    attachInfo.mIgnoreDirtyState = false;
                }
            }
        } finally {
            ......
        }
        return true;
    }

  可以看出这里其实整个View树都是在mSurface的canvas上绘制。关于Surface绘制,会在别的章节详述。

追踪setContentView()

  在Activity的onCreate方法中一般都有一句setContentView(R.layout.xxxx)。要了解View的工作原理,不妨从这个方法入手。
  首先追踪该方法,发现方法内部实际上是调用的mWindow.setContentView()。
  mWindow的实际类型是PhoneWindow。在其setContentView中初始化了DecorView和mContentParent。
  追踪mWindow,可以发现在Activity的attach()方法中赋值。

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);//window的实现类PhoneWindow
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        ...
        ...
    }

  也就是说setContentView的实现是在PhoneWindow类中,那么去看一看

@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) {//mContentParent是一个ViewGroup
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        ...
    }

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);//初始化布局

            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();

            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

            }
            ...
    }

protected DecorView generateDecor(int featureId) {
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext().getResources());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());//DecorView是FrameLayout的子类
    }

  以上代码是从PhoneWindow中setContentView()开始的一系列调用。大概做了这么几件事:

1.初始化DecorView。
2.初始化mContentParent。

  这里的DecorView对象代表了Activity的顶层View。是一个FrameLayout。mContentParent是我们用来展示内容的容器。在这同时还初始化了一个展示标题的容器titlebar,过程与mContentParent相同。它们都是DecorView的子布局。而我们创建的用来绘制activity的xml文件一般都是mContentParent的子view。

将DecorView添加至window

  根据Activity的初始化流程,在执行完onCreate之后会执行onResume,而这个过程都是由ActivityThread来管理的,resume的具体处理是在其handleResumeActivity方法中。

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        ActivityClientRecord r = mActivities.get(token);
        ...

        // TODO Push resumeArgs into the activity for consideration
        r = performResumeActivity(token, clearHide, reason);//追踪会发现调用了onResume()方法

        if (r != null) {
            final Activity a = r.activity;

            ...
            if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (r.mPreserveWindow) {
                    a.mWindowAdded = true;
                    r.mPreserveWindow = false;
                    // Normally the ViewRoot sets up callbacks with the Activity
                    // in addView->ViewRootImpl#setView. If we are instead reusing
                    // the decor view we have to notify the view root that the
                    // callbacks may have changed.
                    ViewRootImpl impl = decor.getViewRootImpl();
                    if (impl != null) {
                        impl.notifyChildRebuilt();
                    }
                }
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        wm.addView(decor, l);//实际调用的WindowManagerImpl中的addView()
                    } else {
                        // The activity will get a callback for this {@link LayoutParams} change
                        // earlier. However, at that time the decor will not be set (this is set
                        // in this method), so no action will be taken. This call ensures the
                        // callback occurs with the decor set.
                        a.onWindowAttributesChanged(l);
                    }
                }
    }

  在这个方法中调用了windowManager的addView方法,绑定了window和根view。因此看addView()方法:

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

            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.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }

  创建了ViewRootImpl后,调用ViewRootImpl的setView()方法,并把DecorView作为参数传递进去,在这个方法内部,会通过跨进程的方式向WMS(WindowManagerService)发起一个调用,从而将DecorView最终添加到Window上,在这个过程中,ViewRootImpl、DecorView和WMS会彼此关联。
  最后通过WMS调用ViewRootImpl#performTraverals方法开始View的测量、布局、绘制流程。

能够准确获取View宽高的办法

  有四个办法可以准确获取view宽高:

1.public void onWindowFocusChanged(boolean hasFocus)方法中。但是该方法可能会多次被调用。
2.view.post(Runnable run)在run方法里面获取宽高。
3.ViewTreeObserver。调用View.getViewTreeObserver()获取一个观察者,给这个观察者设置一个回调。可能也会被多次调用。
4.view.measure()。

说明 方法二:
  view.post()的原理。所有view都会获得一个由ViewRootImpl传递过来的AttachInfo实例。其中包含了一个主线程的handler。通过handler将测量宽高的代码post到主线程的消息队列中,并且排在测量任务之后。也就是说会先测量再执行获取宽高的代码。详情点击传送门
  这里还要注意一点,通过runOnUIThread方法post一个runnable并不能获取正确的宽高,而view.post就可以。是因为runOnUIThread判断当前线程是主线程就会直接执行run方法,不会将其post到消息队列中。

说明 方法四:
  当view的宽高属性是match_parent时,无法测量出具体宽高。因为此时需要知道父容器剩余容量。
  当view的宽高属性是具体数值时,如下测量:

        int width = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        int height = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        view.measure(width,height);

  当view的宽高属性是wrap_content时,如下测量:

        int width = View.MeasureSpec.makeMeasureSpec(1<<30-1, View.MeasureSpec.EXACTLY);//1<<30-1表示30位无符号整数的最大值
        int height = View.MeasureSpec.makeMeasureSpec(1<<30-1, View.MeasureSpec.EXACTLY);
        view.measure(width,height);

  看过源码后可能会知道,在ViewGroup中构造子View的MeasureSpec时,一般情况都是将size赋值为父容器最大可用空间,然后将mode赋值为AT_MOST。因此我们手动测量时用size的最大值去构造MeasureSpec是合理的。这样是不是说控件就会非常大呢?显然不是的。一般的View,例如TextView。若宽高属性是wrap_content,那么在测量时不会取父容器传进来的size,而是会自己计算自己的size。这是一个需要注意的地方。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值