Android进阶 View的工作原理:源码分析View的三大工作流程

Android View 深度分析requestLayout、invalidate与postInvalidate 前言前几篇文章中,笔者对View的三大工作流程进行了详细分析,而这篇文章则详细讲述与三大工作流程密切相关的两个方法,分别是requestLayout和invalidate,如果对Viwe的三个工作流程不熟悉的读者,可以先看看前几篇文章,以便能更容易理解这篇文章的内容。 阅读详情

Android进阶 View的工作原理:源码分析View的三大工作流程

Demo👿

导语

想象一下,如果我们要画画,把画画的流程拆分的话,那么应该分几个部分呢?一般来说,专业的画师会进行提前构图,然后再进行绘画最后上色。实际上Android系统把各种控件显示到屏幕也相当于是系统在进行绘画,也需要把绘画的过程分为几个大的步骤,这就是View的工作流程。

View的工作流程可以分为三个步骤:

  1. 测量(确定各个控件的大小尺寸和位置)
  2. 放置(将控件放置到指定位置和大小)
  3. 绘制(将控件渲染显示出来,这之后我们才能看到)

本篇文章就是主要介绍系统如何绘制各种各样的View控件的三大工作流程。

View的工作流程入口

在我的前几篇文章中,介绍到了Activity的基本构成(可以看这篇文章Android进阶 View事件体系(二):从源码解析View的事件分发),简要可以看下面这幅图片:
在这里插入图片描述
实际上,当DecorView被创建完成之后的一段时间内还不能被显示,因为还没被加载到Window中,这个过程是通过ViewRoot为桥梁连接Window和DecorView的。所以我们先来了解这个过程是怎样开始的。

1.DecorView被加载入Window中

当DecorView创建完毕,要加载到Window中时,需要先了解Activity的创建过程。当我们调用Activity的startActivity方法时,最终是调用了ActivityThread类的handleLaunchActivity方法,我们来看看它的源码:

public Activity handleLaunchActivity(ActivityClientRecord r,
            PendingTransactionActions pendingActions, Intent customIntent) {
        .......
        final Activity a = performLaunchActivity(r, customIntent);//1

        if (a != null) {
            r.createdConfig = new Configuration(mConfigurationController.getConfiguration());
            reportSizeConfigurations(r);
            if (!r.activity.mFinished && pendingActions != null) {
                pendingActions.setOldState(r.state);
                pendingActions.setRestoreInstanceState(true);
                pendingActions.setCallOnPostCreate(true);
            }
        } else {
            // If there was an error, for any reason, tell the activity manager to stop us.
            ActivityClient.getInstance().finishActivity(r.token, Activity.RESULT_CANCELED,
                    null /* resultData */, Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
        }

        return a;
    }

上面代码处的的performLaunchActivity方法使用来创建Activity,里面会调用Activity的onCreate方法,从而完成DecorView的创建,具体我们看performLaunchActivity方法的源码:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
      			......
                // Assigning the activity to the record before calling onCreate() allows
                // ActivityThread#getActivity() lookup for the callbacks triggered from
                // ActivityLifecycleCallbacks#onActivityCreated() or
                // ActivityLifecycleCallback#onActivityPostCreated().
                r.activity = activity;
                if (r.isPersistable()) {
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);//1
                } else {
                    mInstrumentation.callActivityOnCreate(activity, r.state);//2
                }
                if (!activity.mCalled) {
                    throw new SuperNotCalledException(
                        "Activity " + r.intent.getComponent().toShortString() +
                        " did not call through to super.onCreate()");
                }
                mLastReportedWindowingMode.put(activity.getActivityToken(),
                        config.windowConfiguration.getWindowingMode());
            }
            r.setState(ON_CREATE);

        } catch (SuperNotCalledException e) {
            throw e;

        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to start activity " + component
                    + ": " + e.toString(), e);
            }
        }

        return activity;
    }

我们依然来关注关键的方法,上面注释一二处调用到了mInstrumentation.callActivityOnCreate方法,而这个callActivityOnCreate方法就会调用Activity的performCreate方法:

    public void callActivityOnCreate(Activity activity, Bundle icicle) {
        prePerformCreate(activity);
        activity.performCreate(icicle);
        postPerformCreate(activity);
    }

performCreate方法最终调用到了Activity的onCreate回调方法,在之前的文章里我们也提到过,onCreate方法中的setContentView方法就创建得到了DecorView,所以onCreate方法完成了DecorView的创建。

同理,onResume方法也是在ActivityThread中的handleResumeActivity方法中被调用的,我们之前提到的DecorView就是在这个方法里被添加进Window中的,我们接下来看这个handleResumeActivity的源码:

@Override
    public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
            boolean isForward, String reason) {
     	.....
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();//1
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();//2
            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);//3
                } 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);
                }
            }

			.....
    }

源码较长,我们这里只选取我们需要关注的几行,先看注释一处,获得了我们创建的Activity中的DecorView对象,注释二处则是获得了WindowManager对象,这个对象是用来将DecorView添加到Window中的,紧接着第三行注释处就调用了WindowManger的addView方法,将DecorView加载进入了Window中,这个具体的过程我们就不展开了,以后的博客中将会介绍到。总之只要知道是在onResume方法中将DecorView加载进入Window即可。

2.ViewRootImpl的performTraversals

实际上,当我们详细了解到DecorView是如何加载到Window中,就可以发现实际上是通过ViewRootImpl的setView加载的:

	root.setView(view, wparams, panelParentView, userId);

实际上,也正是通过ViewRootImpl的performTraversal方法开始View的工作流程的,详细见下面的源码:

private void performTraversals() {
			....
          
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//1
                    
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();
                    boolean measureAgain = false;

                    if (lp.horizontalWeight > 0.0f) {
                        width += (int) ((mWidth - width) * lp.horizontalWeight);
                        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    if (lp.verticalWeight > 0.0f) {
                        height += (int) ((mHeight - height) * lp.verticalWeight);
                        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }

                    if (measureAgain) {
                        if (DEBUG_LAYOUT) Log.v(mTag,
                                "And hey let's measure once more: width=" + width
                                + " height=" + height);
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    }

                    layoutRequested = true;
                }
            }
        } else {
            maybeHandleWindowMove(frame);
        }

        if (surfaceSizeChanged || surfaceReplaced || surfaceCreated || windowAttributesChanged) {
            prepareSurfaces();
        }

        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
        if (didLayout) {
            performLayout(lp, mWidth, mHeight);//2

            // By this point all views have been sized and positioned
            // We can compute the transparent area

            if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
                // start out transparent
                // TODO: AVOID THAT CALL BY CACHING THE RESULT?
                host.getLocationInWindow(mTmpLocation);
                mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1],
                        mTmpLocation[0] + host.mRight - host.mLeft,
                        mTmpLocation[1] + host.mBottom - host.mTop);

                host.gatherTransparentRegion(mTransparentRegion);
                if (mTranslator != null) {
                    mTranslator.translateRegionInWindowToScreen(mTransparentRegion);
                }

                if (!mTransparentRegion.equals(mPreviousTransparentRegion)) {
                    mPreviousTransparentRegion.set(mTransparentRegion);
                    mFullRedrawNeeded = true;
                    SurfaceControl sc = getSurfaceControl();
                    if (sc.isValid()) {
                        mTransaction.setTransparentRegionHint(sc, mTransparentRegion).apply();
                    }
                }
            }

            if (DBG) {
                System.out.println("======================================");
                System.out.println("performTraversals -- after setFrame");
                host.debug();
            }
        }
       .....
	   .....	
       .....

        if (!isViewVisible) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).endChangingAnimations();
                }
                mPendingTransitions.clear();
            }

            if (mSyncBufferCallback != null) {
                mSyncBufferCallback.onBufferReady(null);
            }
        } else if (cancelAndRedraw) {
            // Try again
            scheduleTraversals();
        } else {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }
            if (!performDraw() && mSyncBufferCallback != null) {//3
                mSyncBufferCallback.onBufferReady(null);
            }
        }
		....
    }

这个方法比较长,我们仍然截取我们需要的部分,可以看到在注释1,2,3处分别调用了performMeasure,performLayout,performDraw三个方法,这三个方法会会调用对应的measure,layout和draw三个方法,而这三个方法又会调用对应的onMeasure,onLayout,onDraw三个方法,具体如下图所示:
在这里插入图片描述

对于ViewGroup来说,performTraversals方法会依次调用performMeasure,performLayout和performDraw三个方法,这三个方法会依次执行对应的工作方法,最后会调用onMeasure等对应方法,在对应的onMeasure等方法中又会依次调用子View的measure等流程,总之就是会递归执行,和事件分发的思想其实很类似,归根到底还是View的树结构。

理解MeasureSpec

MeasureSpec可以理解为测量规格尺寸,里面存储了View的测量得到的宽和高的信息。这个测量规格尺寸很大程度地决定了一个View实际的规格尺寸。之所以说是很大程度是因为在测量过程中测量过程受到父容器的MeasureSpec影响。不过一个View本身的MeasureSpec又会受到View的布局参数LayoutParams和父容器的MeasureSpec影响:
在这里插入图片描述
在onMeasure方法中,将会根据这个MeasureSpec来确定View的宽和高,接下来我们来看MeasureSpec类的主要内容:

 public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        public @interface MeasureSpecMode {}
        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 makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

  
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

		...
    }

这个MeasureSpec类是View的内部类,从它的常量可以看出来,它代表了32位的int值,其中高2位代表specMode,低30位代表specSize,即一个模式,一个尺寸。关于测量模式,有三种状态:

模式含义
UNSPECIFIED未指定模式,View想多大就多大,父容器不做限制,一般用于系统内部的测量,我们也不关注这个
EXACTLY精确模式,对应于match_parent属性 和 指定了具体的数值的情况。这个时候父容器已经检测出了View所需要的精确大小,View的最终大小就是SpecSize所指定的值
AT_MOST最大模式,对应wrap_content属性。父容器制定了一个可用大小,View的大小不能大于这个值,具体是什么要看不同的View的实现

说到这里,就得看看我们之前所说的DecorView的MesureSpect了,对于DecorView来说,它就是顶级的View了,那么它的测量是怎样的呢,这个测量是在ViewRootImpl中实现的:

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

所以又是调用getRootMeasureSpec方法的,我们简单了解一下即可:

    private static int getRootMeasureSpec(int windowSize, int measurement, int privateFlags) {
        int measureSpec;
        final int rootDimension = (privateFlags & PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT) != 0
                ? MATCH_PARENT : measurement;
        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;
    }

其中,windowSize参数指的是窗口的尺寸,所以对于DecorView来说,它的MeasureSpect是由自身的LayoutParams参数决定的。

上面说的是DecorView,而对于普通的View来说(ViewGroup是没有performMeasure方法的),View的measure过程是由ViewGroup的measureChild等一系列方法而开启的。接下来我们来看View的measure流程。

View的Measure流程

先来看View的onMeasure方法:

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

在 Android 源码中,setMeasureDimension() 方法用于设置 View 的测量尺寸。具体而言,它用于设置 View 在进行测量过程中计算得出的宽度和高度。当一个 View 被测量时,系统会调用其 onMeasure() 方法来计算它的宽度和高度。在 onMeasure() 方法内部,会调用 setMeasureDimension() 方法来设置测量尺寸,将计算得出的宽度和高度保存起来。

总而言之,setMeasureDimension() 方法完成的操作是将计算得出的宽度和高度保存为 View 的测量尺寸,在布局过程中起到重要的参考作用。

所以默认设置的尺寸是什么呢?是通过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());
		}

	protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
   		 }

在getDefaultSize方法中,size参数是尺寸,measureSpec是测量模式,最终返回的参数就是得到的测量尺寸measureSpecSize,在这方法中我们可以知道,默认的测量方法下得到的尺寸大小是不区分EXACTLY模式和AT_MOST模式的,因为他们最终都是将measureSpecSize中的参数给直接返回,所以说如果我们想要实现自己的View的话就需要重写这个方法。

而getSuggestedMinimumWidth/Height方法得到的UNSOECUFUED模式下的尺寸返回值,它的取值取决于View有没有设置背景,如果没有设置背景的话,那么它的值将会是mMinWidth属性或者mMinHeight的值;设置了背景的话,它的取值就是背景的宽和高。其实总的来说我们也没必要深究这个参数了。

ViewGroup的measure流程

对于ViewGroup来说,它不只要测量自身的尺寸,还需要测量子View的尺寸,ViewGroup中没有定义onMeasure方法,但却定义了measureChildren方法:

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

	protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

显然这是会遍历子View并开启它们的measure流程的,具体获得子View的MeasureSpec参数是在measureChild方法中,通过getChildMeasureSpec得到了子View的参数,传入的第一个参数是父容器的MeasureSpec,之前提到过子View的MeasureSpec的转化会受到父容器的影响就是在这里体现,第二个参数是已经使用的空间,具体来说就是Padding参数(还有个方法是也考虑到Margin参数的),第三个参数是子View的布局参数LayoutParams。接下来我们看看这个方法的转化规则:

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

根据上面的逻辑,我们可以总结出以下表格:

子View布局参数 \ 父容器模式EXACTLYAT_MOSTUNSPECIFIED
dp/px(精确给定了数值)EXACTLY-childSizeEXACTLY-childSizeEXACTLY-childSize
match_parentEXACTLY-parentSizeAT_MOST-parentSizeUNSPECIFIED-0
wrap_contentAT_MOST-parentSizeAT_MOST-parentSizeUNSPECIFIED-0

如果我们在Android绘制布局过程中经常会尝试各种布局的话,相信一定会对这些规则深有体会,实际上我们使用中得到的各种效果大多数也是遵循这种规则的,当然我们也可定义自己的规则。这些得到的参数最终会传递到我们之前的measure方法中并调用setDimension方法设置到View的参数中去。

ViewGroup并没有提供onMeasure方法(不过ViewGroup也是View,View有omMeasure方法但会有缺陷),而是让各种子类去实现测量方法,究其原因就是ViewGroup有其布局的不同需要,很难统一

以LinearLayout为例,分析其measure方法

为了帮助我们更加深刻地理解这个过程,我们以我们最常用到的LinearLayout为例,来分析它的measure过程。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

LinearLayout的onMeasure主要就是根据朝向来确定测量方式的,这也和使用过程中指定的朝向参数相对应,接下来我们看看朝向是纵向时的测量流程:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        mTotalLength = 0;
        int maxWidth = 0;
        int childState = 0;
        int alternativeMaxWidth = 0;
        int weightedMaxWidth = 0;
        boolean allFillParent = true;
        float totalWeight = 0;

        final int count = getVirtualChildCount();

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

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

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

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

            totalWeight += lp.weight;

            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                if (useExcessSpace) {
                    lp.height = LayoutParams.WRAP_CONTENT;
                }
                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight();
                if (useExcessSpace) {
                    lp.height = 0;
                    consumedExcessSpace += childHeight;
                }

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

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }
            if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
               mBaselineChildTop = mTotalLength;
            }
            if (i < baselineChildIndex && lp.weight > 0) {
                throw new RuntimeException("A child of LinearLayout with index "
                        + "less than mBaselineAlignedChildIndex has weight > 0, which "
                        + "won't work.  Either remove the weight, or don't set "
                        + "mBaselineAlignedChildIndex.");
            }

            boolean matchWidthLocally = false;
            if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
                matchWidth = true;
                matchWidthLocally = true;
            }

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

            allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
            if (lp.weight > 0) {
                weightedMaxWidth = Math.max(weightedMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            } else {
                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            }

            i += getChildrenSkipCount(child, i);
        }

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

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

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

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

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

        mTotalLength += mPaddingTop + mPaddingBottom;

        int heightSize = mTotalLength;

        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
        int remainingExcess = heightSize - mTotalLength
                + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
        if (skippedMeasure
                || ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) {
            float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

            mTotalLength = 0;

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

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

                    final int childHeight;
                    if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
                        childHeight = largestChildHeight;
                    } else if (lp.height == 0 && (!mAllowInconsistentMeasurement
                            || heightMode == MeasureSpec.EXACTLY)) {
                        childHeight = share;
                    } else {
                        childHeight = child.getMeasuredHeight() + share;
                    }

                    final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            Math.max(0, childHeight), MeasureSpec.EXACTLY);
                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
                            lp.width);
                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
                    childState = combineMeasuredStates(childState, child.getMeasuredState()
                            & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
                }

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

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

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

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

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }
            mTotalLength += mPaddingTop + mPaddingBottom;
        } else {
            alternativeMaxWidth = Math.max(alternativeMaxWidth,
                                           weightedMaxWidth);

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

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

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

        if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
            maxWidth = alternativeMaxWidth;
        }
        maxWidth += mPaddingLeft + mPaddingRight;
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

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

调用 measureVertical() 方法时,它会在垂直方向上测量 LinearLayout 中的子 View。首先,一些变量被初始化,包括总长度(mTotalLength)、最大宽度(maxWidth)、子 View 的状态(childState)等。

接下来,通过调用 getVirtualChildCount() 方法获取子 View 的数量,并使用 MeasureSpec.getMode() 方法获取宽度和高度的测量模式。这些信息将在后续的测量过程中使用。在测量过程中,遍历 LinearLayout 中的每个子 View。对于每个子 View,首先检查其可见性。如果子 View 不可见(View.GONE),则跳过后续的测量过程。

对于可见的子 View,首先判断是否在其前面需要添加分隔线,如果需要则增加分隔线的高度到总长度(mTotalLength)中。

然后获取子 View 的布局参数(LayoutParams),并根据布局参数中的权重(weight)来累加总权重(totalWeight)。
接下来根据高度的测量模式和子 View 的布局参数,判断是否需要优化测量过程。如果子 View 的高度为 0 且权重大于 0,且高度测量模式为 MeasureSpec.EXACTLY,则说明子 View 只使用额外的空间进行布局,此时可以跳过测量过程(skippedMeasure 标志设为 true)。

如果不需要跳过测量过程,则根据子 View 的布局参数确定子 View 的高度。如果子 View 的高度为 0 且不需要保持一致的测量结果(mAllowInconsistentMeasurement 为 false),或者高度测量模式为 MeasureSpec.EXACTLY,则将子 View 的高度设置为 LayoutParams.WRAP_CONTENT,以便测量子 View 的最佳高度。

然后通过调用 measureChildBeforeLayout() 方法测量子 View 的尺寸,并获取测量后的子 View 的高度。
如果子 View 的高度需要使用额外的空间进行布局,则将子 View 的高度重置为 0,并将额外使用的空间累加到 consumedExcessSpace 变量中。

在测量完一个子 View 后,更新总长度(mTotalLength)的值,将当前子 View 的高度、上下边距以及下一个子 View 的位置偏移量加到总长度中。如果需要基于某个子 View 计算基线位置,记录该子 View 的位置为 mBaselineChildTop。如果当前子 View 的索引小于基线对齐的子 View 索引(baselineChildIndex),并且布局参数的权重大于 0,抛出异常。这是因为基线对齐的子 View 必须在没有权重的子 View 之后。然后判断是否需要匹配宽度。如果需要匹配宽度,则将最大宽度(maxWidth)设置为当前子 View 的测量宽度,并记录当前子 View 的状态(childState)。

在遍历所有子 View 完成后,根据测量模式和测量结果,计算 LinearLayout 的最终宽度和高度。最后调用 setMeasuredDimension() 方法设置 LinearLayout 的测量尺寸。

总结来说,一般情况下就是将子View纵向的长度累加作为自身的长度,将子View中最大的宽度作为自身的宽度。到此我们就分析完measure流程了,这也是三大流程中最复杂的流程。

View和ViewGroup的layout流程

layout的作用就是确定元素的位置。ViewGroup中的layout是用来确定子View的位置的,而View的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);//1
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);//2
            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);
                }
            }
        }
		......
    }

传入的四个参数分别是View的左边界,上边界,右边界和下边界,在之前的文章中有提到:Android进阶 View事件体系(一):概要介绍和实现View的滑动 不过这里也给出图片加以说明:
在这里插入图片描述
注释1处的:

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

就是用来具体设置这四个边界值的,这两行代码做的就是判断是否为光学布局,如果是光学布局就调用setOpticalFrame方法设置边界,否则就调用setFrame方法设置边界值。至于什么是光学布局,这里就不展开了,总之只要知道通过setFrame成功设置了四个边界值就好。

完成了边界值的设置后,将会调用注释2处的onLayout方法正式进行布局,不过这个onLayout方法默认是一个空方法,这和onMeasure类似,反正都是需要我们进行重写的。我们还是用LinearLayout为例子来说明。

以LinearLayout为例,分析其onLayout方法

老规矩,先上源码:

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

还是和onMeasure差不多的结构,分为纵向布局和横向布局,这里我们仍然以它的朝向为纵向时为例:

void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;

        int childTop;
        int childLeft;

        // Where right end of child should go
        final int width = right - left;
        int childRight = width - mPaddingRight;

        // Space available for child
        int childSpace = width - paddingLeft - mPaddingRight;

        final int count = getVirtualChildCount();

        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;

               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;

           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }

        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

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

                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

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

                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

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

首先会根据Gravity属性确定第一个子View的起始位置,所以说这个childTop就是用来设置子View的起始上边界,而childLeft确定了子View的起始左边界值,有了这两个和View的宽度和高度就可以确定View的边界值了。而View的宽度和高度是用过getMeasuredWidth/Height得到的,最终会依次调用setChildFrame方法设置子View的边界值,传入的正是起始点和宽高度。同时LinearLayout将会累积高度值,这样就可以实现自View纵向排列而不重叠的效果。这样我们就比较简要地分析了Linearlayout的layout过程了。

这里我们也可以发现,LinearLayout的实际尺寸就是MeasureSpec中传入的值:

final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
......
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);

一般情况下View是遵守MeasureSpec中的尺寸等于实际的尺寸这个规则的,不过这不是决定的,我们也可以自定义View使其不相等,比如:

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

这种情况下,实际的尺寸就和测量尺寸不一致了,可以说实际放置View时只是参考了测量尺寸,只不过绝大情况下都将测量尺寸设置为实际尺寸。测量尺寸和实际尺寸也可以通过getMeasureWidth/Height和getWidth/Height方法分别获得,读者可以自行体验。

View和ViewGroup的Draw流程

至于Draw流程就比较简单了,之前两步已经确定了View的尺寸和摆放位置,Draw要做的就是将View绘制到屏幕上。关于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)
* 7. If necessary, draw the default focus highlight
*/

这是一段注释,描述了绘制遍历(draw traversal)的执行顺序和步骤。绘制遍历是指在进行绘制时按照一定的顺序遍历视图层次结构的过程。以下是每个步骤的解释:

  1. 绘制背景:如果需要的话,首先绘制视图的背景。

  2. 如有需要,保存画布的图层以准备进行淡入淡出效果。

  3. 绘制视图的内容:绘制视图自身的内容,例如文本、图像等。

  4. 绘制子视图:遍历并绘制视图的子视图,按照它们在视图层次结构中的顺序绘制。

  5. 如有需要,绘制渐变边缘效果并恢复图层:绘制视图的边缘效果,例如渐变阴影或边框,并在完成后恢复之前保存的图层。

  6. 绘制装饰(例如滚动条):绘制视图的装饰元素,例如滚动条。

  7. 如有需要,绘制默认的焦点高亮效果:绘制视图的默认焦点高亮效果,以指示当前获取焦点的视图。

我们就按照上边的步骤,这里挑重点的几个步骤进行分析

步骤一:绘制背景

绘制背景的话将会调用到View的drawBackground方法,如下所示:

    private void drawBackground(Canvas canvas) {
		......

        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

主要看最后这一段,出现了我们很熟悉的scroll参数,具体来说,在绘制背景时会考虑当前画布是否有偏移,如果有偏移的话,就会在先将画布移动到指定位置再进行绘制,绘制完毕之后再将画布移动回原位

步骤三:绘制视图内容

这一步实际上就是绘制自身View的内容,调用到的是View的onDraw方法,不过这个方法也是一个空方法:

    protected void onDraw(Canvas canvas) {
    }

这需要我们自己实现。

步骤四:绘制子视图

步骤四调用的是View的dispatchDraw方法,这个方法也是一个空方法:

    protected void dispatchDraw(Canvas canvas) {

    }

不过这个方法在ViewGroup中进行了重写,我们来看ViewGroup中的内容:

    protected void dispatchDraw(Canvas canvas) {
    ......
        for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }

            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        while (transientIndex >= 0) {
            // there may be additional transient views after the normal views
            final View transientChild = mTransientViews.get(transientIndex);
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                break;
            }
        }
        if (preorderedList != null) preorderedList.clear();

        // Draw any disappearing views that have animations
        if (mDisappearingChildren != null) {
            final ArrayList<View> disappearingChildren = mDisappearingChildren;
            final int disappearingCount = disappearingChildren.size() - 1;
            // Go backwards -- we may delete as animations finish
            for (int i = disappearingCount; i >= 0; i--) {
                final View child = disappearingChildren.get(i);
                more |= drawChild(canvas, child, drawingTime);
            }
        }
	......
    }

整个ViewGroup的dispatchDraw方法很长,这里截取了最重要的部分,ViewGroup会依次遍历所有子View然后通过drawChild方法绘制子视图,自顶向下不断传递,从而完成了整个View树的绘制。

步骤六:绘制装饰

绘制装饰是调用到View的onDrawForeground方法,从方法的名字也可以看出来,这个方法是用来绘制在视图内容的上层的,主要是用来绘制ScrollBar及其其他装饰的,这里我们也给出源码,不过就不详细分析了:

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

它是将一个drawable对象创建加载出来,最后调用其draw方法来实现绘制到顶层的。

总结

到此为止,我们就分析完了整个View的工作流程,主要是三大部分:

  • 测量
  • 放置(设置边界)
  • 绘制
    熟悉这些工作流程不仅能帮助我们理解Android系统的绘制过程,同时为我们以后使用自定义View也有很大帮助。关于自定义View的内容,将在下一篇文章介绍。

下面是一张我总结的流程图,省略了一些细节:
在这里插入图片描述

View的加载和更新流程 本文主要讲解View的完整加载过程和View的更新流程。 阅读详情

相关推荐

安卓基础知识之View篇(三):源码分析 View 事件分发机制

安卓基础知识系列旨在简明扼要地提供面试或工作中常用的基础知识,让对安卓还不太熟悉的小伙伴更快地入门。同时自己在工作中,也没法完全记住所有的基础细节,写这样的系列文章,可以让自己形成一个更完备的知识体系,同时给自己日后留个知识参考。 开始的开始 本篇文章基于 Android 12.0 版本,会从源码角度来体会事件从顶层 ViewGroup 往下传递的过程,以及 View 是如何处理传递过来的事件的。 正文 还记得备忘录里记的结论吗,如果你还不是很了解 View 事件传递机制的原理,直接看备忘录的结论可能会.

qq_40987010的博客 1878

android view绘制机制和加载过程,以及整个流程

android view绘制机制和加载过程,以及整个流程

QYgujingjing的专栏 975

View绘制的源码解读、自定义View和ViewGroup的注意点总结

一直想要总结一下view绘制的一些经验,近期正好在看《Android内核剖析》这本书,借这个机会,结合源码&一些经验,对View的绘制源码进行一些学习、总结,过程中结合实践demo对自定义View的一些经验进行总结。 1.View绘制的源码解读 View整体绘制流程 当一个应用启动时,会启动一个主Activity,Android系统会根据Activity的布局来对它进行绘制。绘制会从根视图ViewRootImpl的performTraversals()方法开始,是一个树形的递归过程,从上到下遍.

itbird的专栏 622

View的绘制流程

view的绘制流程主要为三个阶段View与window的逻辑结构如图所示:ViewRootImpl(替代ViewRoot)类,是连接WindowMannager和DecorView的纽带,View的三大流程均是通过ViewRoot完成的,当activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联,在ViewRootImpl里的performTraversals()进行分发。

xiaowang_lj的博客 3608

苹果开发者账号( Apple Developer)登录出现:你的 Apple ID 暂时不符合使用此应用程序的条件(您的apple账户不符合资格)

Apple开发对于新手来说确实可能会遇到各种问题,在遇到 Apple Developer 账号登录问题时,除了检查账号的安全性设置和协议确认外,还需要考虑账号的具体配置问题,比如主邮箱的设置。

Winter_Sun灬的博客 4134

销售预测的方法与模型(三)丨安全库存与再订货(补货)

较高的 Z 值对应较高的服务水平(例如,服务水平 99% 对应 Z 值 2.33),意味着企业希望更低的缺货风险,因此需要更高的安全库存。在再订货点的管理中,考虑到供应商交货的不确定性,通常需要结合安全库存,以防止在交货期间库存用尽。要对L开根号,根号L表示交货周期越长,企业需要的安全库存越多,因为在较长的供应期内,需求波动的累积效应更明显。举例:供应商的交期服从标准差为2件/天的正态分布,顾客需求为50件/天的正态分布,若要客户服务水平达到 90%,安全库存量应该是多少?下面介绍几种核心的模型与公式。

weixin_45675709的博客 1534

Android View的加载流程

要说Android view加载流程,首先,我们需要清楚的理解什么是View、Window、Activity 以及三着有着什么样的关系。一、什么是Activity?看看源码中对Activity的描述: * An activity is a single, focused thing that the user can do. Almost all * activities interact wi...

王恩龙的博客 2906

别再手动调参了!用MATLAB+改进PSO算法,5步搞定机械臂轨迹优化(附完整代码)

本文介绍如何利用MATLAB和改进PSO算法快速优化机械臂轨迹规划,仅需5步即可完成传统耗时的手动调参过程。通过混沌初始化、自适应权重等创新方法,改进PSO算法显著提升优化效率,实现全局最优解搜索,适用于工业自动化中的机械臂控制。文章包含完整MATLAB代码实现,帮助工程师轻松应用于实际生产场景。

weixin_29232121的博客 354

Carson带你学Android:深入解析自定义View工作流程

今天,我将全面总结自定义View 的全工作流程,我能保证这是市面上的最全面、最清晰、最易懂的

Carson带你学Android 8811

《LabVIEW ZYNQ实战指南》第6章>>案例解析:基于USB WiFi模块的ZYNQ无线通信部署与Linux RT开发

本文详细解析了如何为ZYNQ开发板部署USB WiFi模块,实现无线通信。通过LabVIEW Linux RT环境,利用封装好的驱动VI,开发者可轻松配置ZYNQ连接无线网络,摆脱网线束缚,实现程序无线部署与远程调试,为工业物联网应用奠定基础。

app77的博客 755

【CANoe】CAPL_UDS安全算法dll制作

使用UDS进行刷写时,需要27服务解密后,才能正确执行刷写,本文提供两种制作27解密dll的方法,用于CANoe的CAPL脚本中。

星Yeah 的博客 1万+

View工作原理及源码分析

View工作流程分析

linwq8的博客 1300

软考「系统规划与管理师(第2版)」通关全攻略(2026年下半年)

它考的是一个IT管理者能不能用体系化的语言,把"我们为什么要建这个系统→怎么规划→怎么管好服务→怎么合规"讲圆。所以备考的核心策略也很明确——教材建框架 → 真题磨考点 → 案例练结构 → 论文备好真实项目素材 → 机考练打字。按计划走,不贪全、不盲刷、不死背,稳稳把三科都推过45分那条线。祝你通关 🎯。

软件行业技术文化交流。 3045

android View绘制源码分析

在开发过程中我们经常要进行view的自定义。如果熟练掌握自定义技巧的话就能做出很多控件出来。这篇博客来讲讲view绘制背后发生的那些事。view的基础知识view的绘制概括首先先说说view绘制的整体过程。 View绘制的源码分析 ,它的三大流程都是在ViewRootImpl中完成的,从ViewRootImpl中的performTraversals开始,有三个方法performMeasure,pe

u014486880的博客 1392

测风数据垂直外推(原理及Windowgrapher操作示范)

垂直外推

weixin_46713695的博客 896

SAP ABAP Web Service 的创建与消费:保姆级教程

SOAP 定义了消息的格式和传输规则,使得 Web Service 可以在不同的操作系统和网络环境中通信。通过 WSDL,服务的提供者可以明确地定义服务的功能,而服务的使用者则可以通过解析 WSDL 文件,了解如何正确地调用该服务。它封装了与 Web 服务通信的所有复杂性,如消息格式的构建、传输协议的选择、请求的发送和响应的处理等。除了本文介绍的 ABAP 编程语言之外,我们还可以通过 Java,Node.js,C++,C#,Python 等其他编程语言来消费本文创建的 ABAP Web Service.

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 713

Android 开源库WheelView源码分析:View的工作流程

View的工作流程

牛角尖 534

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

Android:View体系之UI 的绘制流程及原理 (源码分析+掌握,看这一篇就足够!)

转载请标明出处! 目录 一 概述 1.1 Android的View继承关系图 1.2 Android的视图坐标 1.2.1安卓坐标系 1.2.2 视图坐标系 1.2.3 常用方法 二 View的事件分发 三 View的工作流程 3.1 概述 3.2测量 3.2.1 预备知识MeasureSpec(重要) 3.2.2 MeasureSpec的SpecMode分类(模式)...

许进进的博客 6046

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

Android高级进阶——View的工作原理Measure过程详解

开篇 本篇主要讲解的是 View 工作原理之 measure 过程,主要是以源码的形式进行分析,源码来源 API 26。 从 ViewRootImpl 的 #performTraversals 方法开始说起 performTraversals 方法是整个工作流程的核心,它里面分别取执行了 measure、layout 以及 draw 三个过程,看一下代码: private void...

AkaiC的博客 614

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

Android View 的工作原理

Android View 的工作原理 文章主要参考书籍《Android 开发艺术探索》(任玉刚 著),与书籍主要区别: - 源代码基于 Android 19,Android 版本变化导致的变化会标注说明 - 追加大量流程图,源代码追加更详细注释,以方便理解与记忆 - 扩展了一些书籍上未说明的源代码、流程说明

jefshi的博客 630

Android View(四)——View的工作原理

文章目录一.初识ViewRoot 和 DecorView二.View的工作流程概述View绘制过程中的一些方法介绍三.理解MeasureSpec1.MeasureSpec是干什么的?2.MeasureSpec的定义3.MeasureSpec和LayoutParams的对应关系3.1 顶级View(DecorView) 一.初识ViewRoot 和 DecorView DecorView DecorView是整个Window界面的最顶层View。DecorView只有一个子元素为LinearLayout。代

haazzz的博客 573

安卓入门四十九 View布局过程

如:底部导航条中的条目,一般都是上图标(ImageView)、下文字(TextView),那么这两个就可以用自定义ViewGroup组合成为一个Veiw,提供两个属性分别用来设置文字和图片,使用起来会更加方便。此处需复写ViewGroup的onLayout(),其具体实现逻辑是指:计算子View的位置,即计算四顶点位置 = 计算Left、Top、Right和Bottom。在非人为设置的情况下,View的最终宽/高(getWidth() / getHeight())那么,“非一般”情况是什么?

weixin_52735932的博客 1414

Android View 的工作流程和原理

前言 在日常开发中,我们每天都在和各种 View 打交道,比如TextView,Button等,我们直接拿过来就可以使用,那么 Android 是怎么把 View 绘制到屏幕上呢,接下来我们结合源码来具体分析。 在具体结合源码分析前,先了解一个比较重要的概念 ViewRoot ViewRoot 先看一张图 Android 窗口构成图解 Android View 的工作流程和原理 ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 根布局 DecorView(看上

dongxiaogang5_5的博客 215

Android源码分析之View的绘制流程

Android View的绘制流程详解(包括View的来龙去脉) 对于Android开发者来说,View无疑是开发中经常接触的,包括它的事件分发机制、测量、布局、绘制流程等,如果要自定义一个View,那么应该对以上流程有所了解、研究。本系列文章将会为大家带来View的工作流程详细解析。在深入接触View的测量、布局、绘制这三个流程之前,我们从Activity入手,看看从Activity创建后到V...

博由心生,福泽一生 1477

Android应用层View绘制流程与源码分析

【工匠若水 http://blog.youkuaiyun.com/yanbober 转载烦请注明出处,尊重分享成果】1 背景还记得前面《Android应用setContentView与LayoutInflater加载解析机制源码分析》这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原理,记不记得最终分析结果就是下面的关系:看见没有,如上图中id为content的内容就是整个View树的结构,所

工匠若水 14万+

Android进阶 :实现自定义View

有时候我们会想要实现一些复杂或者是独特的组件效果,这时候系统提供的组件可能不能满足我们的需求,这个时候我们一般就会有两个解决办法,**一是上网查找开源的控件库**,一些流行的开源库可以满足我们的绝大部分需求。不过这些开源库也具有时效性,因为它们的更新得不到保证。二就是我们自己实现我们需要的控件,开源库本质也是自定义View。 不过实现自定义View需要我们对View的事件分发以及View的工作流程有一定了解,可以查看之前的博客: 1. [Android进阶 View事件体系(一):概要介绍和实现View的

Tai_Monster的博客 1487
上一篇: Android进阶 View事件体系(三):典型的滑动冲突情况和解决策略
下一篇: Android进阶 :实现自定义View
Tai_Monster
博客等级 码龄5年 225粉丝 91原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值