View的绘制(7)-View和ViewGroup的绘制原理源码分析

主目录见:Android高级进阶知识(这是总目录索引)
今天文章内容主要是承接《setContentView()的源码分析》的,上篇主要是讨论了view的加载,今天主要讨论view的绘制工作。以下内容会稍微导致身体不适,如果脑子还没放空,那么拖出去洗脑一小时。。。

我能怎么办,我也很绝望

一.目标

这篇的目标非常明确了,就是死磕到底,没办法,这块骨头是必须要啃的,权当磨牙:
1.了解测量,布局,绘制三个过程;
2.同时了解一些小坑:为什么在onCreate可以在线程中更新UI?等。。
3.了解一种在onCreate时候延迟加载的策略:

getWindow().getDecorView().post(new Runnable() { 
   @Override public void run() { 
     myHandler.post(mLoadingRunnable); 
 } });

当然第三种不会作为本篇重点,如果再讲这个本篇就说不完了,但是会稍微提一下,表示敬意,如果你是处女座可以留言,我可以再开一篇来讲。。。

二.源码分析

1.ActivityThread handleResumeActivity

分析源码的入口是非常关键的,不然就是一步错步步错,为什么要从这个地方开始分析呢?我们前面说过,我们setContentView完之后加载进来的布局都是在DecorView下面的,DecorView又是在PhoneWindow下面的,那么DecorView及DecorView里面的视图的绘制过程是怎样的呢?这个答案比较长,得需要从Activtiy的启动开始讲,那么我们这里直接给出答案:就是在Activity的onResume()之前会调用ActivityThread的handleResumeActivity方法,这里面会有一系列的操作。

    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
//防止身体不适,删除自认为跟讲解无关代码
     .......
            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 && !a.mWindowAdded) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                }

            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
            } else if (!willBeVisible) {
                if (localLOGV) Slog.v(
                    TAG, "Launch " + r + " mStartedActivity set");
                r.hideForNow = true;
            }

            // Get rid of anything left hanging around.
            cleanUpPendingRemoveWindows(r, false /* force */);

            // The window is now visible if it has been added, we are not
            // simply finishing, and we are not starting another activity.
            if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) {
                if (r.newConfig != null) {
                    performConfigurationChangedForActivity(r, r.newConfig, REPORT_TO_ACTIVITY);
                    if (DEBUG_CONFIGURATION) Slog.v(TAG, "Resuming activity "
                            + r.activityInfo.name + " with newConfig " + r.activity.mCurrentConfig);
                    r.newConfig = null;
                }
                if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward="
                        + isForward);
                WindowManager.LayoutParams l = r.window.getAttributes();
                if ((l.softInputMode
                        & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
                        != forwardBit) {
                    l.softInputMode = (l.softInputMode
                            & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
                            | forwardBit;
                    if (r.activity.mVisibleFromClient) {
                        ViewManager wm = a.getWindowManager();
                        View decor = r.window.getDecorView();
                        wm.updateViewLayout(decor, l);
                    }
                }
                r.activity.mVisibleFromServer = true;
                mNumVisibleActivities++;
                if (r.activity.mVisibleFromClient) {
//注意到这句!!这句!!这句!!
                    r.activity.makeVisible();
                }
            }

            if (!r.onlyLocalRequest) {
                r.nextIdle = mNewActivities;
                mNewActivities = r;
                if (localLOGV) Slog.v(
                    TAG, "Scheduling idle handler for " + r);
                Looper.myQueue().addIdleHandler(new Idler());
            }
            r.onlyLocalRequest = false;

            // Tell the activity manager we have resumed.
            if (reallyResume) {
                try {
                    ActivityManagerNative.getDefault().activityResumed(token);
                } catch (RemoteException ex) {
                    throw ex.rethrowFromSystemServer();
                }
            }

        } else {
            // If an exception was thrown when trying to resume, then
            // just end this activity.
            try {
                ActivityManagerNative.getDefault()
                    .finishActivity(token, Activity.RESULT_CANCELED, null,
                            Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
        }
    }

惨无人道呀,刚上来就发着一大段代码,吓唬人呀,其实我也很委屈,源码就是这么长,如果不想看就跟着我的文字走,效果一样,我们看到一个方法r.activity.makeVisible();从判断条件 if (r.activity.mVisibleFromClient) 我们也知道,这个地方的Acitivity快显示了,那显示之前得绘制布局好呀,所以我们看这个方法:

 void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

这个方法比较简单,第一句判断mWindowAdded是否被添加,显然还没添加,因为下面添加完才改了这个标志为true,所以我们看到第一句getWindowManager()方法会返回一个ViewManager对象,其实ViewManager是个接口,所以我们要去看getWindowManager()到底返回的是什么。

   public WindowManager getWindowManager() {
        return mWindowManager;
    }

其实就是返回mWindowManager对象,那么这个对象又是什么时候被赋值呢?这个偷偷告诉你,在handleResumeActivity()方法调用之前就已经赋值了,是在Activity的attach()方法设置的:

  mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);

可以很容易看出来就是调用window的setWindowManager(),所以我们找phoneWindow,但是没有找到,那么就从父类Window查找,我们找到了这个方法:

    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        mAppToken = appToken;
        mAppName = appName;
        mHardwareAccelerated = hardwareAccelerated
                || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

非常明确,看看这句mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);其实就是这句赋值的,我们看到vm强制转换成WindowManagerImpl,其实WindowManagerImpl就是继承的WindowManager,我们这里直接看WindowManagerImpl的createLocalWindowManager()方法:

 public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

我们这里找到wm对象是WindowManagerImpl类的实例了,所以我们回到makeVisible()方法,继续看下一句wm.addView(mDecor, getWindow().getAttributes()),我们看看WindowManagerImpl的addView这一句做了些什么鬼:

  @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

心很累呀,到处跳来跳去。。。。坚强地继续看到 mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow)这句,这个mGlobal又是什么鬼?这里的mGloabal是WindowManagerGlobal对象,主要是window的处理类,所以我们看WindowManagerGlobal的addView()方法。

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
//省略一些判空操作及无关讲解内容
 ....
            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被实例化出来了,大家欢迎!!欢迎!!欢迎!!,实例化出来然后被加到mRoots(这是一个存放ViewRootImpl实例的Arraylist)中,同时decorView被添加进mViews中,最后我们看到调用了root.setView()方法,所以我们看ViewRootImpl的setView里面做了啥?

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
  synchronized (this) {
            if (mView == null) {
                mView = view;
            }
.......
            requestLayout();
..........
    }
}

我们看到这个方法主要是讲decorView设置给了ViewRootImpl,同时调用了requestLayout()方法,这个方法大家用的不少了。我们看requestLayout()方法的具体实现是啥:

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

我们看到第一个方法checkThread(),这个方法正好说明了为什么在onCreate()方法里面可以在子线程中更新UI了,因为在onCreate时候还没有检查线程呀,我们看下这个方法就会豁然开朗:

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

那个侥幸在子线程更新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()的调用里面有个参数传进去mTraversalRunnable,其实地方会回调mTraversalRunnable(这个是Runnable对象)里面的run方法,我们来看这个run执行了啥?

   @Override
        public void run() {
            doTraversal();
        }

一句话,很喜欢这么清爽的方法,我们直接跟踪进去:

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

这个地方勇敢地删除所有的方法,就是为了凸显这个方法!!!这个方法是在ViewRootImpl里面的,我们以后的测量,布局,绘制都在这个方法里面,也就是这几个方法的入口。Are you kidding?玩了这么久才找到入口???


我读书多不会骗你的

2.入口ViewRootImpl performTraversals

我们终于找到真正的入口了,兴奋不已,赶紧往下看:

private void performTraversals() {
.......
        // Execute enqueued actions on every traversal in case a detached view enqueued an action
        getRunQueue().executeActions(mAttachInfo.mHandler);
........
      int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
      int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    // Ask host how big it wants to be
//1.绘制
    performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
.........
//2.布局
     performLayout(lp, mWidth, mHeight);
.........
//3.绘制
     performDraw();
}

这个方法其实巨长,如果一个字一个字往下看,那今天这篇就不用写了,所以为了正本溯源,我们就留下我们要一一讲解的步骤。首先我们看下第一个留下来的方法getRunQueue().executeAction(mAttachInfo.mHandler),这个方法为什么留下来呢,其实原因就是前面说到的:

getWindow().getDecorView().post(new Runnable() { 
   @Override public void run() { 
     myHandler.post(mLoadingRunnable); 
 } });

这个地方post的action,到这里才会执行。所以我们知道等我们绘制完毕后才会执行mLoadingRunnable,这就是为什么要两次post的原因。为了讲绘制之前把障碍都扫清楚,我们再来看下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;
    }

这个方法很简单,其实就是设置宽高为屏幕的大小,这也就是为什么根视图decorView为啥是屏幕宽高的原因。好啦现在我们可以开始讲我们的performMeasure方法了。

3.performMeasure

View树的源码measure流程图.png

在讲之前先贴图镇贴,此图如果见过请忽略,也不是我画的。。。我们知道绘制其实就是调用的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);
        }
    }

我们看到这里调用了 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec)这个方法,那么这里的mView是什么东西呢,当然是从根视图开始measure嘛,那就是decorView了,我们知道decorView是个FrameLayout本质上(当然如果你子视图有Linearlayout或者其他就又会调用不同的Measure方法了),所以我们就看FrameLayout的measure方法,可是我们在FrameLayout并没有找到measure方法呀,其实我们是在View里面的measure方法里面才又调用了FrameLayout里面的onMeasure方法:

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

        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        // Account for padding too
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

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

        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }

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

我们看到程序里面遍历了父视图下面的子视图,然后调用measureChildWithMargins方法来进行测量子视图,我们看下这个方法做了什么?

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

我们看到这个其实是根据父视图的大小还有mode,以及自己的Padding和Margin还有LayoutParams来测量,我们先看下这里面的方法getChildMeasureSpec()干了什么:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//获取当前Parent View的Mode和Size
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
 //获取Parent size与padding差值(也就是Parent剩余大小),若差值小于0直接返回0
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;
//默认Root View的Mode就是EXACTLY
        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
//如果子视图给的宽高是>0且是确切值则直接设置
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
  //如果child的layout_width属性在xml或者java中给予MATCH_PARENT
  //设置child的size为size,mode为EXACTLY
                // 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.
//如果child的layout_width属性在xml或者java中给予WRAP_CONTENT
//设置child的size为size,mode为AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
//省略其他的mode和size的代码
.......
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

可以看到根据mode和size测量出自己的大小之后,会把mode和size调用makeMeasureSpec继续合成。
然后我们继续看measureChildWithMargins方法最后会在调用View的measure,如果这个View还是个ViewGoup的话,那就会递归调用measure方法。如果最后是个View呢?那就会调用View的measure呀,也就是会调用到onMeasure(),这个也是我们自定义View会重写的方法:

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

我们看到这个方法里面代码就一句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);
    }

好吧这个方法又调用了setMeasuredDimensionRaw方法,我们就跟进去:

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

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

简单呀,就是将这个宽高保存,所以我们只有测量过后才能获取到这个测量的宽高。也就是自定义控件onMeasure之后一定要调用setMeasuredDimension方法的原因,你要保存一下呀。到这里我们的测量工作就完成啦。
测量总结:由于测量整个过程比较复杂,我这里做个总结
1.MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中specMode只有三种值:

MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;
MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定;

2.View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。

3.最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。

4.ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算

5.View的布局大小由父View和子View共同决定

6.使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值.

4.performLayout

我们看完测量的过程了,我们就看看怎么把这些控件布局到界面了,布局也是一个递归调用的过程,我们来看下这个方法。

  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");
        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
......
    }

我们看到这个地方调用mView的layout方法,同样第一次进来还是调用的根视图的layout,所以我们看到DecorVIew(FrameLayout)的layout方法,这个方法在Framelayout的父类ViewGroup中才有,我们找到它了:

   @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 {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }

我们看到这个地方调用了super.layout方法,即调用了View的layout方法,所以我们直接进入View的这个方法:

    public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
        }
...........
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

我们看到这个layout方法显示保存了上下左右这些的数值,又调用了onLayout方法:

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

额。。。。View的这个方法是空实现,其实是合理的如果是View的话,那就自己实现,如果是ViewGroup那就调用他的子类去实现他,因为我们DecorView是FrameLayout,所以我们再去看FrameLayout的onLayout:

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

跟我们预想一样,让子类自己去布局自己,所以这也是一个递归的过程,如果是View则自己实现布局,如果是ViewGoup则递归调用layout:

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();

        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;

                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }

                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

我们看到这个方法也是遍历子视图,然后分别去调用子视图的layout方法。
到这里我们的布局也讲解完毕了,我们这里也做个总结吧。
总结:
1.View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。

2.measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。

3.凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的,这个在LayoutInflater.inflater方法里面可以找到答案。

5.performDraw

好啦,我们终于到达最后一步绘制啦,到这里身体是不是开始不适。。。其实我也开始不适,只能用图来缓解缓解,盗图可耻,但是可爱呀!!!

draw流程

看完图之后我们继续看我们的performDraw方法:

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

我们看到这里又调用了ViewRootImpl的draw方法,所以我们直接看到draw方法:

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

这里我们调用drawSoftware方法:

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

        ...
        mView.draw(canvas);
        ...
        return true;
    }

几经波折,我们终于找到熟悉的代码了mView.draw(canvas),我们知道ViewGroup是没有重写draw方法的,所以我们直接看View的draw方法。

 @CallSuper
    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的流程
         * 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;
        }
//底下省略verticalEdges ,horizontalEdges不为false的绘制流程
.......
    }

我们看到,这个地方如果verticalEdges ,horizontalEdges都为false的时候我们会跳进进行下一步的绘制,我们这个地方就分析这种情况的绘制流程,其实另外下面的绘制流程是差不多的,顶多多了边缘的绘制。

5.1).现在我们这里讲第一步:drawBackground
    private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
 //根据layout过程确定的View位置来设置背景的绘制区域
        setBackgroundBounds();

        // Attempt to use a display list if requested.
        if (canvas.isHardwareAccelerated() && mAttachInfo != null
                && mAttachInfo.mHardwareRenderer != null) {
            mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

            final RenderNode renderNode = mBackgroundRenderNode;
            if (renderNode != null && renderNode.isValid()) {
                setBackgroundRenderNodeProperties(renderNode);
                ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
                return;
            }
        }

        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
//调用Drawable的draw()方法来完成背景的绘制工作
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

我们看到前面先把mBackground设置给了backgroud局部变量,接着设置了背景的bound范围,然后最后调用background.draw(canvas)来将背景绘制进画布。

5.2).现在我们这里讲第三步:对View的内容进行绘制

onDraw这个方法ViewGroup里面也是没有的,所以我们还是来看View

 protected void onDraw(Canvas canvas) {
    }

可以看到这是一个空的方法,这是交给我们的自定义view自己来实现的,因为系统不知道我们的View是长啥样的,所以要我们自己决定。

5.3).现在我们这里讲第四步:对所有子View的进行绘制

这个方法因为是对所有子View的绘制,所以View里面这个方法是空的,ViewGroup里面才有。

 @Override
    protected void dispatchDraw(Canvas canvas) {
        ......
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        ......
        for (int i = 0; i < childrenCount; i++) {
            ......
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ......
        // Draw any disappearing views that have animations
        if (mDisappearingChildren != null) {
            ......
            for (int i = disappearingCount; i >= 0; i--) {
                ......
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ......
    }

我们看到这边调用了drawChild方法,我们直接进去看下:

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

我们看到其实就是调用View的draw方法,也就是上面说的,交给View自己去决定要画什么。

5.4).现在我们这里讲第六步:绘制一些滚动条,前景图片等
 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);
        }
    }

我们看到这个方面前面就绘制了ScrollIndicators,和ScrollBars,然后获取前景图片,如果不为空则最后调用Drawbale的draw方法绘制进canvas( foreground.draw(canvas))。到这里我们的draw流程都说清楚了,总结就是那张图了,说明的非常清楚,所以就不总结了。
不是说不总结了怎么马上又总结了:这是整片文章的总结,整篇说了View的绘制流程,过程还是比较复杂的,当然有些问题可能不能全部覆盖,所以希望如果有什么知识点可以留言提出来,我会加一篇文章进行解答,如果没有那就算了,你可以上天了。。。

无感

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

推荐阅读更多精彩内容