2025年3月21日 星期五 甲辰(龙)年 月廿 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > 安卓(android)开发

UI绘制流程及原理

时间:03-18来源:作者:点击数:51

首先需要了解我们的view是如何被添加到窗口中

点击activity中setContentView会进入系统源码中

  • public void setContentView(@LayoutRes int layoutResID) {
  • getWindow().setContentView(layoutResID);
  • initWindowDecorActionBar();
  • }

这里的geWindow()返回的事Window对象,而Window是一个抽象类,它的唯一实现类就是PhoneWindow,所以直接进入PhoneWindow的setContentView(id)方法

  • @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) {
  • installDecor(); //1
  • } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
  • mContentParent.removeAllViews();
  • }
  • if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
  • final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
  • getContext());
  • transitionTo(newScene);
  • } else {
  • mLayoutInflater.inflate(layoutResID, mContentParent);//2
  • }
  • mContentParent.requestApplyInsets();
  • final Callback cb = getCallback();
  • if (cb != null && !isDestroyed()) {
  • cb.onContentChanged();
  • }
  • mContentParentExplicitlySet = true;
  • }

方法中一开始mContentParent是为空的,这个mContentParent是什么呢?后面会讲到,首先进入1处注释的方法installDecor(),点击进入

  • private void installDecor() {
  • mForceDecorInstall = false;
  • if (mDecor == null) {
  • mDecor = generateDecor(-1);//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);//2
  • ...
  • }
  • }

从源码中看到mDecor初始化视为空的,所以会进入1处的方法,这个方法会为我们创建一个DecorView,而这个DecorView是继承自FrameLayout,所以它是一个布局容器,其实它就是我们布局的最外层容器,是所有窗口的根view。所以1处方法的执行就是为我们创建了一个顶层布局容器。

  • protected DecorView generateDecor(int featureId) {
  • // System process doesn't have application context and in that case we need to directly use
  • // the context we have. Otherwise we want the application context, so we don't cling to the
  • // activity.
  • Context context;
  • if (mUseDecorContext) {
  • ...
  • } else {
  • context = getContext();
  • }
  • return new DecorView(context, featureId, this, getAttributes());
  • }

再看第二处方法,首先会根据不同的style对feature进行设置,注意1处,定义了一个layoutResource,下面会根据这个feature对layoutResouce进行赋值。其实就是根据activity的theme样式去加载不同的布局样式,并把它赋值給layoutResource。

  • protected ViewGroup generateLayout(DecorView decor) {
  • // Apply data from current theme.
  • //...根据不同的style设置不同的feature ,此处省略
  • // Inflate the window decor.
  • int layoutResource;//1
  • int features = getLocalFeatures();
  • // System.out.println("Features: 0x" + Integer.toHexString(features));
  • //根据不同的feature样式,加载不同的系统布局
  • if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
  • layoutResource = R.layout.screen_swipe_dismiss;
  • ...
  • } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
  • && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
  • // Special case for a window with only a progress bar (and title).
  • // XXX Need to have a no-title version of embedded windows.
  • layoutResource = R.layout.screen_progress;
  • // System.out.println("Progress!");
  • } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
  • // Special case for a window with a custom title.
  • // If the window is floating, we need a dialog layout
  • if (mIsFloating) {
  • ...
  • layoutResource = res.resourceId;
  • } else {
  • layoutResource = R.layout.screen_custom_title;
  • }
  • // XXX Remove this once action bar supports these features.
  • removeFeature(FEATURE_ACTION_BAR);
  • } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
  • // If no other features and not embedded, only need a title.
  • // If the window is floating, we need a dialog layout
  • if (mIsFloating) {
  • ...
  • layoutResource = res.resourceId;
  • } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
  • layoutResource = a.getResourceId(
  • R.styleable.Window_windowActionBarFullscreenDecorLayout,
  • R.layout.screen_action_bar);
  • } else {
  • layoutResource = R.layout.screen_title;
  • }
  • // System.out.println("Title!");
  • } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
  • layoutResource = R.layout.screen_simple_overlay_action_mode;
  • } else {
  • // Embedded, so no decoration is needed.
  • layoutResource = R.layout.screen_simple;
  • // System.out.println("Simple!");
  • }
  • mDecor.startChanging();
  • mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);//2
  • ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);//3
  • if (contentParent == null) {
  • throw new RuntimeException("Window couldn't find content container view");
  • }
  • ...
  • return contentParent;
  • }

在看第2处注释,点击进去继续跟踪。首先会把我们的系统布局解析成view,并把它作为基础布局容器添加到decorview之上

  • void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
  • ...
  • final View root = inflater.inflate(layoutResource, null);//1
  • if (mDecorCaptionView != null) {
  • ...
  • } else {
  • // Put it below the color views.
  • addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));//2
  • }
  • ...
  • }

回到上面generateLayout()方法,第3处,我们可以任意打开一个系统layout文件可以看到,都会有一个id为content的FrameLayout容器,在3处对它解析。并且系统布局文件中有一个viewStub控件,其实就是系统为我们添加的默认标题栏,解析完以后就把contentParent进行返回,所以上面提到的mContentParent就是它。

  • <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  • android:layout_width="match_parent"
  • android:layout_height="match_parent"
  • android:fitsSystemWindows="true"
  • android:orientation="vertical">
  • <ViewStub android:id="@+id/action_mode_bar_stub"
  • android:inflatedId="@+id/action_mode_bar"
  • android:layout="@layout/action_mode_bar"
  • android:layout_width="match_parent"
  • android:layout_height="wrap_content"
  • android:theme="?attr/actionBarTheme" />
  • <FrameLayout
  • android:id="@android:id/content"
  • android:layout_width="match_parent"
  • android:layout_height="match_parent"
  • android:foregroundInsidePadding="false"
  • android:foregroundGravity="fill_horizontal|top"
  • android:foreground="?android:attr/windowContentOverlay" />
  • </LinearLayout>

再次回到我们的初始方法,总结一下,注释1主要做了两件事情

第一件:创建我们的顶层布局容器decorView,

第二件:根据activity不同的主题样式会解析不同的系统布局文件,并将它添加到decorview之上,作为基础布局容器。另外,它会解析布局容器中id为conetent的viewgroup容器,并将其返回,赋值给mContentParent。

  • @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) {
  • installDecor(); //1
  • } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
  • mContentParent.removeAllViews();
  • }
  • if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
  • final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
  • getContext());
  • transitionTo(newScene);
  • } else {
  • mLayoutInflater.inflate(layoutResID, mContentParent);//2
  • }
  • mContentParent.requestApplyInsets();
  • final Callback cb = getCallback();
  • if (cb != null && !isDestroyed()) {
  • cb.onContentChanged();
  • }
  • mContentParentExplicitlySet = true;
  • }

再看第二处,就比较容易理解了,就是把我们acticity的布局文件layoutResID解析到mContentParent之上。所以我们用户看到的界面其实就是decorView,那么我们的decorView对象是如何被添加到窗口之上的呢?

首先进入ActivityThread源码类,找到handleMessage()方法,该方法是所有activity调用都会触发的,在里面会调用handlerResumeActivity()方法。最总跟踪到windowManagerImpl ,找到addView()方法

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

会调用到mGlobal()的addView()方法,继续跟踪

  • public void addView(View view, ViewGroup.LayoutParams params,
  • Display display, Window parentWindow) {
  • ...
  • final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
  • if (parentWindow != null) {
  • parentWindow.adjustLayoutParamsForSubWindow(wparams);
  • } else {
  • // If there's no parent, then hardware acceleration for this view is
  • // set from the application's hardware acceleration setting.
  • final Context context = view.getContext();
  • if (context != null
  • && (context.getApplicationInfo().flags
  • & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
  • wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
  • }
  • }
  • ViewRootImpl root;
  • View panelParentView = null;
  • ...
  • 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) {
  • ...
  • }
  • }
  • }

最后调用root.setView(...)将三个参数进行关联,以上流程实现了decorView添加到窗口Window的过程,但是此时界面仍然不会显示出来,因为view的工作流程还没有执行完,还要经过measure(),layout(),draw()三步操作,才会把view绘制出来。参考下面一张图

其次需要我们了解view的绘制流程

点击进入root.setView(...)方法

  • /**
  • * We have one child
  • */
  • public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
  • synchronized (this) {
  • if (mView == null) {
  • mView = view;
  • ...
  • requestLayout();
  • ...
  • }
  • }
  • }

点击进入requestLayout()方法

  • @Override
  • public void requestLayout() {
  • if (!mHandlingLayoutInLayoutRequest) {
  • //判断是否在主线程中
  • checkThread();
  • mLayoutRequested = true;
  • //1
  • scheduleTraversals();
  • }
  • }

点击1处方法,继续跟踪

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

发现会调用mTraversalRunnable方法,找到raversalRunnable的run方法

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

点击1处继续跟踪

  • void doTraversal() {
  • if (mTraversalScheduled) {
  • ...
  • //1
  • performTraversals();
  • ...
  • }
  • }

进入1处方法,发现view的绘制3个方法都会在这里执行

  • void performTravelsals(){
  • ...
  • performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
  • ...
  • performLayout(lp, mWidth, mHeight);
  • ...
  • performDraw();
  • ...
  • }

(1)测量

在分析测量之前,我们首先需要了解MeasureSpec类,它决定view测量规格,封装了view的宽高,绘制一个view,首先需要确定他的测量规格。

这个测量规格分为两部分 即 view = mode + size ->MeasureSpec 表示32位的int值

mode:测量模式 取前2位 命名SpecMode

size:测量尺寸 取后30位 命名SpecSize

再看,我们的View类中提供了三个方法,分别进行打包和解包的操作

  • //对measureSpec进行打包操作
  • 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);
  • }
  • }
  • //对measureSpec进行解包操作
  • public static int getMode(int measureSpec) {
  • //noinspection ResourceType
  • return (measureSpec & MODE_MASK);
  • }
  • public static int getSize(int measureSpec) {
  • return (measureSpec & ~MODE_MASK);
  • }

另外,MeasureSpec中定义了3个静态常量

  • public static final int UNSPECIFIED = 0 << MODE_SHIFT; 00000000000000000000000000000000
    父容器不对View做任何限制,一般都是系统内部在设置ListView或者是ScrollView的时候才会用到
  • public static final int EXACTLY = 1 << MODE_SHIFT; 01000000000000000000000000000000
    父容器检测出View的大小,Vew的大小就是SpecSize LayoutPamras match_parent 固定大小。(也可以这样理解:该模式其实对应的场景就是match_parent或者是一个具体的数据(50dp或80px),父视图为子View指定一个确切的大小,无论子View的值设置多大,都不能超出父视图的范围)
  • public static final int AT_MOST = 2 << MODE_SHIFT; 10000000000000000000000000000000
    父容器指定一个可用大小,View的大小不能超过这个值,LayoutPamras wrap_content。

大家可能会有一个疑问,AT_MOST模式不是和EXACTLY模式一样了吗,都是给个最大值来限制View的范围?其实不一样,EXACTLY模式是一个固定的模式,也就是说它是没得商量的,你只能按照这个模式既有的规律来执行而AT_MOST模式是可以根据开发者自己的需求来定制的,我们写自定义View的时候所写的测量其实也就是在写这个模式的测量逻辑,他是根据子控件来灵活的测量尺寸的。

回到源码中继续分析:

进入performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);方法中,源码如下

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

在注释1处,会调用mView的measure方法,其中mView就是我们的顶层view即decorView,继续进入measure()方法

  • public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
  • //前面会做一些绘制的缓存处理,提高绘制效率
  • ...
  • if (cacheIndex < 0 || sIgnoreMeasureCache) {
  • // measure ourselves, this should set the measured dimension flag back
  • //1
  • onMeasure(widthMeasureSpec, heightMeasureSpec);
  • mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
  • } else {
  • ...
  • }
  • ...
  • }

继续点击1处的方法进入

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

继续点击进入setMeasuredDimension()方法

  • protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
  • ...
  • setMeasuredDimensionRaw(measuredWidth, measuredHeight);
  • }

继续点击进入setMeasuredDimensionRaw()方法

  • private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
  • mMeasuredWidth = measuredWidth;
  • mMeasuredHeight = measuredHeight;
  • mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
  • }

在这里对我们顶层view即decorView的宽高进行了赋值操作,说明我们的decorView的宽高是和measuredWidth,measuredHeight相关的。那么我们需要分析这两个值

回到我们的performMeasure(...)方法原始调用的地方,在上面找到了两个方法,进行了宽高的计算

  • void performTravelsals(){
  • ...
  • //初始化了宽高
  • int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
  • int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
  • ...
  • performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
  • ...
  • performLayout(lp, mWidth, mHeight);
  • ...
  • performDraw();
  • ...
  • }

进入getRootMeasureSpec()方法中,这里传入了两个参数,windowSize即窗口尺寸 ,rootDimension为lp.width或lp.height,这里的lp为我们decorView的布局属性对象。

注:所以decorView的MeasureSpec由窗口大小和自身的LayoutParams决定

  • 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;
  • //固定大小,精确模式,大小由LayoutParams自身大小决定
  • 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的测量过程,下面分析viewGroup和view的测量过程,进入FrameLayout源码类中,找到onMeasure(...)方法

  • @Override
  • protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  • int count = getChildCount();
  • ...
  • for (int i = 0; i < count; i++) {
  • final View child = getChildAt(i);
  • if (mMeasureAllChildren || child.getVisibility() != GONE) {
  • //1
  • measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
  • ...
  • }
  • }
  • ...
  • }

方法接受了两个参数,分别表示父容器的测量规格。看到有个for循环遍历子view进行测量 ,点击1处进入方法

  • protected void measureChildWithMargins(View child,
  • int parentWidthMeasureSpec, int widthUsed,
  • int parentHeightMeasureSpec, int heightUsed) {
  • //1
  • final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
  • //2
  • 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);
  • //3
  • child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  • }

注释1处获取子view的布局属性对象, 2处根据父容器的测量规格和自身的布局属性, 计算自身的view测量规格,3处往后执行和decorView流程一致,最终会为我们的子view保存一个宽高。完成子view的测量

针对2处查看如何获取子view的测量规格

  • //从上面我们知道spec 是parent的MeasureSpec,padding是
  • //已被使用的大小,childDimension为child的大小
  • public static int getChildMeasureSpec(
  • int spec, int padding, int childDimension) {
  • //1、获取parent的specMode
  • int specMode = MeasureSpec.getMode(spec);
  • //2、获取parent的specSize
  • int specSize = MeasureSpec.getSize(spec);
  • //3、size=剩余的可用大小
  • int size = Math.max(0, specSize - padding);
  • int resultSize = 0;
  • int resultMode = 0;
  • //4、通过switch语句判断parent的集中mode,分别处理
  • switch (specMode) {
  • // 5、parent为MeasureSpec.EXACTLY时
  • case MeasureSpec.EXACTLY:
  • if (childDimension >= 0) {
  • //5.1、当childDimension大于0时,表示child的大小是
  • //明确指出的,如layout_width= "100dp";
  • // 此时child的大小= childDimension,
  • resultSize = childDimension;
  • //child的测量模式= MeasureSpec.EXACTLY
  • resultMode = MeasureSpec.EXACTLY;
  • } else if (childDimension == LayoutParams.MATCH_PARENT) {
  • //5.2、此时为LayoutParams.MATCH_PARENT
  • //也就是 android:layout_width="match_parent"
  • //因为parent的大小是明确的,child要匹配parent的大小
  • //那么我们就直接让child=parent的大小就好
  • resultSize = size;
  • //同样,child的测量模式= MeasureSpec.EXACTLY
  • resultMode = MeasureSpec.EXACTLY;
  • } else if (childDimension == LayoutParams.WRAP_CONTENT) {
  • //5.3、此时为LayoutParams.WRAP_CONTENT
  • //也就是 android:layout_width="wrap_content"
  • // 这个模式需要特别对待,child说我要的大小刚好够放
  • //需要展示的内容就好,而此时我们并不知道child的内容
  • //需要多大的地方,暂时先把parent的size给他
  • resultSize = size;
  • //自然,child的mode就是MeasureSpec.AT_MOST的了
  • resultMode = MeasureSpec.AT_MOST;
  • }
  • break;
  • // 5、parent为AT_MOST,此时child最大不能超过parent
  • case MeasureSpec.AT_MOST:
  • if (childDimension >= 0) {
  • //同样child大小明确时,
  • //大小直接时指定的childDimension
  • resultSize = childDimension;
  • resultMode = MeasureSpec.EXACTLY;
  • } else if (childDimension == LayoutParams.MATCH_PARENT) {
  • // child要跟parent一样大,resultSize=可用大小
  • resultSize = size;
  • //因为parent是AT_MOST,child的大小也还是未定的,
  • //所以也是MeasureSpec.AT_MOST
  • resultMode = MeasureSpec.AT_MOST;
  • } else if (childDimension == LayoutParams.WRAP_CONTENT) {
  • //又是特殊情况,先给child可用的大小
  • resultSize = size;
  • resultMode = MeasureSpec.AT_MOST;
  • }
  • break;
  • // 这种模式是很少用的,我们也看下吧
  • case MeasureSpec.UNSPECIFIED:
  • if (childDimension >= 0) {
  • // 与前面同样的处理
  • 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;
  • }
  • //通过传入resultSize和resultMode生成一个MeasureSpec.返回
  • return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
  • }

总结一下:

  • 当子View采用具体数值(dp / px)时,无论父容器的测量模式是什么,子View的测量模式都是EXACTLY且大小等于设置的具体数值;
  • 当子View采用match_parent时
    • 子View的测量模式与父容器的测量模式一致
    • 若测量模式为EXACTLY,则子View的大小为父容器的剩余空间;若测量模式为AT_MOST,则子View的大小不超过父容器的剩余空间
  • 当子View采用wrap_parent时
    如果父容器测量模式为UNSPECIFIED,子View也为UNSPECIFIED,否则子View为AT_MOST且大小不超过父容器的剩余空间。

注:子View的MeasureSpec由子view的LayoutParams和父容器的MeasureSpec确定,如下图:

回到FrameLayout类中onMeasure()方法中

  • @Override
  • protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  • int count = getChildCount();
  • ...
  • for (int i = 0; i < count; i++) {
  • final View child = getChildAt(i);
  • if (mMeasureAllChildren || child.getVisibility() != GONE) {
  • //1
  • measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
  • ...
  • }
  • }
  • // 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());
  • }
  • //2
  • 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);
  • }
  • }
  • }

注释2处会根据子view占据的最大宽高计算出父容器的宽高。继而完成viewGroup的绘制,view的绘制可以进入view的measure()方法继续跟踪

大致流程如下:

ViewGroup measure --> onMeasure(测量子控件的宽高)--> setMeasuredDimension -->setMeasuredDimensionRaw(保存自己的宽高)

View measure --> onMeasure --> setMeasuredDimension -->setMeasuredDimensionRaw(保存自己的宽高)

注意:在进行子view进行测量时会调用如下方法

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

进入getDefaultSize()方法,发现view的测量过程中MeasureSpec.AT_MOST和MeasureSpec.EXACTLY下,view的大小都赋值为specSize 大小,即父容器的剩余大小,所以当我们自定义view的时候如果没有重写onMeasure方法,无论设置成wrap_content还是match_parent,效果是一样的。也就是说当我们是自定义viewGroup时需要确定子view的宽高,而我们自定义View的时候需要在onMeasure中重新确定自己的宽高

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

(2)布局

进入performLayout(lp, mWidth, mHeight);方法中,其中第一个参数为我们的顶层view即decorView的布局属性,第二,三个参数为它的宽高。源码如下

  • private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
  • int desiredWindowHeight) {
  • mLayoutRequested = false;
  • mScrollMayChange = true;
  • mInLayout = true;
  • //1
  • final View host = mView;
  • if (host == null) {
  • return;
  • }
  • if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
  • Log.v(mTag, "Laying out " + host + " to (" +
  • host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
  • }
  • Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
  • try {
  • //2
  • host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
  • }
  • ...
  • }

1处将我们的decorView赋值给host ,2处调用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);//1
  • if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
  • onLayout(changed, l, t, r, b);//2
  • ...
  • }
  • ...
  • }

调用1处setFrame(...)方法实际上就是为我们的view进行上下左右的赋值,确定其位置。2处调用onLayout()方法,点击进入

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

发现该方法是一个方法,针对于viewGroup来说,就需要在这个方法中确定子view的位置,比如FrameLayout,我们找到这个onLayout()方法,对于view来说不需要重写

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

继续进入layoutChildren(...)方法

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

方法中使用循环,找到每个子view的位置。最后会调用child.layout()方法,进行递归操作,实现子view的摆放。

(3)绘制

进入performDraw(...)方法中

  • private void performDraw() {
  • ...
  • try {
  • boolean canUseAsync = draw(fullRedrawNeeded);
  • ...
  • } finally {
  • ...
  • }
  • ...
  • }
  • }

找到draw()方法,点进去

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

找到drawSoftware()方法,点进去

  • /**
  • * @return true if drawing was successful, false if an error occurred
  • */
  • private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
  • boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
  • ...
  • mView.draw(canvas);
  • ...
  • return true;
  • }

找到mView.draw(canvas);其中mView就是我们的decorView,再次点进draw(...)方法

  • public void draw(Canvas canvas) {
  • final int privateFlags = mPrivateFlags;
  • final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
  • (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
  • mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
  • /*
  • * Draw traversal performs several drawing steps which must be executed
  • * in the appropriate order:
  • *
  • * 1. Draw the background
  • * 2. If necessary, save the canvas' layers to prepare for fading
  • * 3. Draw view's content
  • * 4. Draw children
  • * 5. If necessary, draw the fading edges and restore layers
  • * 6. Draw decorations (scrollbars for instance)
  • */
  • // Step 1, draw the background, if needed
  • int saveCount;
  • if (!dirtyOpaque) {
  • drawBackground(canvas);
  • }
  • // skip step 2 & 5 if possible (common case)
  • final int viewFlags = mViewFlags;
  • boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
  • boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
  • if (!verticalEdges && !horizontalEdges) {
  • // Step 3, draw the content
  • if (!dirtyOpaque) onDraw(canvas);
  • // Step 4, draw the children
  • dispatchDraw(canvas);
  • drawAutofilledHighlight(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);
  • // Step 7, draw the default focus highlight
  • drawDefaultFocusHighlight(canvas);
  • if (debugDraw()) {
  • debugDrawFocus(canvas);
  • }
  • // we're done...
  • return;
  • }

可以看到decorView的绘制经过了6个步骤。

  • 如果需要,则绘制背景。
  • 保存当前canvas层。
  • 绘制View的内容。
  • 绘制子View。
  • 如果需要,则绘制View的褪色边缘,这类似于阴影效果。
  • 绘制装饰,比如滚动条。

这里着重分析几个关键点

(1)绘制View的内容

  • protected void onDraw(Canvas canvas) {
  • }

发现是一个空方法,需要我们自定义view自己实现具体的绘制内容

(2)绘制子view

  • protected void dispatchDraw(Canvas canvas) {
  • }

发现也是一个空方法,对于自定义view来说,不需要重写改法,对于自定义viewGroup就需要在里面进行子view的绘制,进入它的dispatchDraw(...)方法

  • @Override
  • protected void dispatchDraw(Canvas canvas) {
  • ...
  • if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
  • final boolean buildCache = !isHardwareAccelerated();
  • for (int i = 0; i < childrenCount; i++) {
  • final View child = children[i];
  • if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
  • final LayoutParams params = child.getLayoutParams();
  • attachLayoutAnimationParameters(child, params, i, childrenCount);
  • bindLayoutAnimation(child);
  • }
  • }
  • final LayoutAnimationController controller = mLayoutAnimationController;
  • if (controller.willOverlap()) {
  • mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
  • }
  • controller.start();
  • mGroupFlags &= ~FLAG_RUN_ANIMATION;
  • mGroupFlags &= ~FLAG_ANIMATION_DONE;
  • if (mAnimationListener != null) {
  • mAnimationListener.onAnimationStart(controller.getAnimation());
  • }
  • }
  • ...
  • // 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);//1
  • }
  • }
  • ...
  • }

该方法对子view进行了遍历,并且调用了1处的drawChild(...)方法,继续跟踪

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

继续进入child.draw(...)方法,会发现又回到view的draw方法,执行6个步骤。实现view的递归绘制。

以上就完成了view的绘制流程。总结一下:

执行view的绘制三大步开始的地方是ViewRootImpl类中的performIntervals()方法,里面进行了performMeasure(),performLayout()和performDraw()。

view的measure:

  • 首先会执行到decorView的measure(childWidthMeasureSpec, childHeightMeasureSpec)方法,然后会在view类中执行onMeasure(childWidthMeasureSpec, childHeightMeasureSpec)-->setMeasureDimenssion(childWidthMeasureSpec, childHeightMeasureSpec)-->onMeasureDimenssionRaw(childWidthMeasureSpec, childHeightMeasureSpec)最终在该方法中对decorView的宽高进行赋值。其中childWidthMeasureSpec, childHeightMeasureSpec 分别表示测量规格的宽高,这两个值是由窗口window的size尺寸和自身的LayoutParams布局属性决定。
  • 然后,ViewGroup 的 onMeasure() 是抽象方法,但其提供了measureChildren(),这之中会遍历子View然后循环调用measureChild() 这之中会用 getChildMeasureSpec()+父View的MeasureSpec+子View的LayoutParam一起获取本View的MeasureSpec,然后调用子View的measure()到View的onMeasure()-->setMeasureDimension(getDefaultSize(),getDefaultSize()),getDefaultSize()默认返回measureSpec的测量数值,所以继承View进行自定义的wrap_content需要重写,在getDefaultSize()方法总可以看到原因。子view的MeasureSpec是由父容器的MeasureSpec和自身的LayoutParams决定。

view的layout:

  • 执行decorView的layout(l,t,r,b)方法,然后会调用setFrame()方法确定本view的4个顶点位置。然后会调用onLayout()方法,它是一个抽象方法。如果是viewGroup容器我们需要重写onLayout方法,确定子view的位置。比如FrameLayout我们需要调用layoutChild(l,t,r,b)方法遍历子view。并调用子view的child.layout(l,t,r,b)方法,回到View类的layout(l,t,r,b)方法,对子view布局。

view的draw:

  • 执行view的6绘制6大步骤。其中绘制内容部分需要我们自己去实现重写。绘制子view调用dispatchDraw()方法等。
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门