您当前的位置:首页 > 计算机 > 编程开发 > 安卓(android)开发

UI绘制流程及原理

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

首先需要了解我们的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()方法等。
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门