Android View的绘制流程详解(包括View的来龙去脉)

对于Android开发者来说,View无疑是开发中经常接触的,包括它的事件分发机制、测量、布局、绘制流程等,如果要自定义一个View,那么应该对以上流程有所了解、研究。本系列文章将会为大家带来View的工作流程详细解析。在深入接触View的测量、布局、绘制这三个流程之前,我们从Activity入手,看看从Activity创建后到View的正式工作之前,所要经历的步骤,包括Activity、DecorView、Window的关联部分,解释View怎么从Window->DecorView->Activity->View的绘制整个线性流程。如下图,需要绘制的View需要在Acvitity的View创建起来之后才开始走测量、布局、绘制等生命流程,Activity的View实际上就是图示的ContentView,即Activity调用setContentView(layoutID)设置的View。ContentView属于DecorView的子View,DecorView属于PhoneWindow的子View,PhoneWindow是Window的一个实例,用于显示一个窗体信息,整个流程显示的明明白白,下面将会详细分析。

在这里插入图片描述

1、Window

Window表示一个窗口的概念,Android手机中所有的视图都是通过Window来呈现的,像常用的Activity,Dialog,PopupWindow,Toast,他们的视图都是附加在Window上的,所以可以这么说:Window是View的直接管理者。

分类

  • Window有三种类型,分别是应用Window、子Window、系统window。应用Window对应着一个Activity。子Window不能单独存在,他需要附属在特定的父Window之中,比如常见的一些Dialog。系统Window是要声明权限才能创建的Window,比如Toast和系统状态栏。
  • Window是分层的,每个Window都有对应的z-ordered,层级大的会覆盖在层级小的Window上面。在三类window中,应用Window的层级范围是1-99,子Window的层级范围是1000-1999,系统Window的层级范围是2000-2999,这些层级范围对应这WindowManager.LayoutParam的type参数。当我们需要使用系统Window时,需要声明权限。

PhoneWindow

  • Window的唯一实现类是PhoneWindow ,在启动Activity的attach方法中被创建,Activity中setContentView实际上是调用 PhoneWindow 的setContentView 方法。并且 PhoneWindow 中包含着成员变量 DecorView。如上图所以的页面,实际上你看到的是一个PhoneWindow,它内部负责显示的View就是DecorView,下面我们将揭开这个DecorView的真面目。

2、DecorView

通过上面的解释我们知道,在Activity创建过程中,调用setContentView(layoutID)设置的View,最后将会以子View的形式设置给DecorView的某个子View,这三个DecorView究竟是什么样的结构?什么是DecorView?

什么是DecorView

  • DecorView是整个ViewTree的最顶层View,它是一个FrameLayout布局,代表了整个应用的界面,一般包括TitleBar和mContentParent。
  • DecorView的子元素mContentParent就是ActivitysetContentView(layoutID)设置的View。
  • PhoneWindow中创建了一个DecroView,其中创建的过程中可能根据Theme不同,加载不同的布局格式,例如有没有Title,或有没有ActionBar等,然后再向mContentParent中加入子View,即Activity中设置的布局。

DecorView的布局

我们先从源码级别分析Activity的setContentView(layoutID)都做了啥?

一、setContentView的显示过程

1、Activity的setContentView调用了PhoneWindow的setContentView

//PhoneWindow  --> setContentView() 
@Override 
public void setContentView(int layoutResID) { 
    if (mContentParent == null) { 
		//1.初始化 , 创建DecorView对象和mContentParent对象  
        installDecor(); 
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { 
        mContentParent.removeAllViews();//Activity转场动画相关 
    } 
	//2.填充Layout 
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { 
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,getContext()); 
        transitionTo(newScene);//Activity转场动画相关 
    } else { 
    	//将Activity设置的布局文件,加载到mContentParent中 
        mLayoutInflater.inflate(layoutResID, mContentParent); 
    } 
	...
}

2、installDecor():创建DecorView对象和mContentParent对象

//PhoneWindow --> setContentView()  --> installDecor() 
private void installDecor() { 
  if (mDecor == null) { 
    	//调用该方法创建new一个DecorView 
        mDecor = generateDecor(); 
    }else { 
        mDecor.setWindow(this); 
    } 
    if (mContentParent == null) { 
	    //根据主题theme设置对应的xml布局文件以及Feature(包括style,layout,转场动画,属性等)到DecorView中。 
	    //并将mContentParent绑定至id为ID_ANDROID_CONTENT(com.android.internal.R.id.content)的ViewGroup 
	    //mContentParent在DecorView添加的xml文件中 
	    mContentParent = generateLayout(mDecor); 
	    ...     
    } 
} 

3、generateDecor()—创建DecorView

//PhoneWindow --> setContentView()  --> generateDecor() 
protected DecorView generateDecor(){
    return new DecorView(getContext(), -1);
}

4、generateLayout(mDecor);—创建mContentParent

//PhoneWindow --> setContentView()  -->generateLayout()  
 protected ViewGroup generateLayout(DecorView decor) { 
   //获取当前的主题,加载默认资源和布局 
    TypedArray a = getWindowStyle(); 
    ... 
    //根据theme的设定,找到对应的Feature(包括style,layout,转场动画,属性等) 
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) { 
        requestFeature(FEATURE_NO_TITLE);//无titleBar 
    }  
    ... 
    if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) { 
        setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));//设置全屏 
    } 
    if (a.getBoolean(R.styleable.Window_windowTranslucentStatus,false)) { 
        setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS & (~getForcedWindowFlags()));//透明状态栏 
    } 
    //根据当前主题,设定不同的Feature 
    ... 
    int layoutResource; 
    int features = getLocalFeatures(); 
    ...
    if ((features & (1 << FEATURE_NO_TITLE)) == 0) { 
         layoutResource = R.layout.screen_title; 
    }  else if(){
...
} else {//无titleBar 
        layoutResource = R.layout.screen_simple; 
    } 
    mDecor.startChanging(); 
    //将布局layout,添加至DecorView中 
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); 
    //从布局中获取`ID_ANDROID_CONTENT`,并关联至contentParent 
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); 
    ... 
    //配置完成,DecorView根据已有属性调整布局状态 
    mDecor.finishChanging(); 
    return contentParent; 
}

二、过程分析

从上面的分析我们知道,DecorView会根据每个Activity都是有自己资源ID形式的布局。在填充资源layout时候,会根据不同的feature来选择不同的布局。
大概有如下几种。

R.layout.screen_title_icons
R.layout.screen_progress
R.layout.screen_custom_title
R.layout.screen_action_bar
R.layout.screen_simple_overlay_action_mode;
R.layout.screen_simple

下面我们拿两个布局参考进行分析

扫描二维码关注公众号,回复: 9636310 查看本文章
  • screen_simple.xml
<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>
  • screen_simple_overlay_action_mode.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">
    
    <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" />
         
    <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>

无一例外, 无论看几个都有一个id为content 的 FrameLayout。ViewStub用于懒加载actionBar,而id为@android:id/content的FrameLayout,此FrameLayout就是contentView。我们在Activity中调用setContentView方法,设置布局,最终就是添加到该FrameLayout中。分析到这里,整个Activity需要显示的View创建好了,那么如何显示?

  • ActivityThread的handleResumeActivit
if (r.window == null && !a.mFinished && willBeVisible) {
	r.window = r.activity.getWindow();
	View decor = r.window.getDecorView();
	decor.setVisibility(View.INVISIBLE);
	ViewManager wm = a.getWindowManager();
	WindowManager.LayoutParams l = r.window.getAttributes();
	a.mDecor = decor;
	l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
	l.softInputMode |= forwardBit;
	if (a.mVisibleFromClient) {
	   a.mWindowAdded = true;
	   wm.addView(decor, l);
	}
}

我们可以看到DecorView通过wm添加到系统中显示出来。到此,DecorView的分析就完了。看到这里,不知道你有没有一种豁然开朗或者有没有一些想法?比如下面的代码我们可以拿到视图最顶级的View,也知道contentView 实际上是个FrameLayout,我们可以给他添加各种View,实现类似Dialog的逻辑,不过动画要自己实现。这种做法是可以替代重量型的Dialog的,因为Dialog使用崩溃概率会增加,维护难度会有所困难。目前大部分开源框架StatusBar的沉浸方案就是使用contentView 中add一个状态栏的方案实现最终效果。更多的想法,需要自己去挖掘了~~

ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
ViewGroup contentView = activity.findViewById(android.R.id.content);        

3、View的绘制

由上面分析可知:id为“content”的FrameLayout是我们的布局文件加载显示的区域,更确切地说是我们activity的setContentView()方法设置的视图显示的区域。他是个ViewGroup,是个ViewTree,它的绘制将依赖每个子View的绘制。Android中的任何一个布局、任何一个控件包括我们自定义的控件其实都是直接或间接继承自View实现的,所以说这些View应该都具有相同的绘制流程与机制才能显示到屏幕上(可能每个控件的具体绘制逻辑有差异, 但是主流程都是一样的)。每一个View的绘制过程都必须经历三个最主要的过程,也就是measure()、layout()和draw()。那么,整个Android的UI绘制机制是从哪里开始的即入口在哪里呢?答案就是ViewRootImpl类的performTraversals()方法。

回顾DecorView的添加流程

  • Activity的attach方法会构造一个PhoneWindow实例
  • 我们在onCreate里通过setContentView将我们的xml添加到了DecorView
  • ActivityThread在后续过程中会将DecorView添加到Activity的窗口中,也就是添加到PhoneWindow
  • WindowManagerGlobal通过ViewRootImpl的setView方法将DecorView传递到ViewRootImpl进行绘制

我们从源码的角度来看下setView方法

//遍历的接口回调
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
//ViewRootImpl  ->  setView()
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
     synchronized (this) {
         if (mView == null) {
             mView = view;
             ……
             requestLayout();;
         }
     }
 }
 
 @Override
 public void requestLayout() {
     if (!mHandlingLayoutInLayoutRequest) {
         checkThread();
         mLayoutRequested = true;
         scheduleTraversals();
     }
 }
 
 //遍历Activity的根布局DecorView里的每一个View。
 void scheduleTraversals() {
     if (!mTraversalScheduled) {
         mTraversalScheduled = true;
         mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
         mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
         if (!mUnbufferedInputDispatch) {
             scheduleConsumeBatchedInput();
         }
         notifyRendererOfFramePending();
         pokeDrawLockIfNeeded();
     }
 }

可以看到这里post了一个mTraversalRunnable,我们看看这个runnable做了啥事

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
//开始遍历
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }
        //正式绘制的入口
        performTraversals();
        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

performTraversals()

首先我们看这个方法名,perform是执行的意思,而Traversals是遍历循环的意思;所以这个方法看方法名就知道他是在遍历Activity的根布局DecorView里(或者其它窗口比如Dialog)的每一个View。

private void performTraversals() {
    ...
    if (!mStopped) {
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  // 1
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); 
    } 
    if (didLayout) {
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ...
    }
    if (!cancelDraw && !newSurface) {
        if (!skipDraw || mReportNextDraw) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }
            performDraw();
        }
    } 
    ...
}

我们看到它里面主要执行了三个方法,分别是performMeasure、performLayout、performDraw这三个方法,在这三个方法内部又会分别调用measure、layout、draw这三个方法来进行不同的流程。我们先来看看performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)这个方法,它传入两个参数,分别是childWidthMeasureSpec和childHeightMeasure,那么这两个参数代表什么意思呢?要想了解这两个参数的意思,我们就要先了解MeasureSpec。

MeasureSpec

官方文档对MeasureSpec类的描述:A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode.它的意思就是说,该类封装了一个View的规格尺寸,包括View的宽和高的信息。

要注意,MeasureSpec并不是指View的测量宽高,是根据MeasueSpec而测出测量宽高。在Measure流程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure方法中根据这个MeasureSpec来确定View的测量宽高。MeasureSpec代表一个32位的int值,高2位表示SpecMode,低30位表示SpecSize,而SpecSize是指在某种SpecMode下的规格大小。

SpecMode三种模式

  • UNSPECIFIED = 0 << MODE_SHIFT:即: 00000000 00000000 00000000 00000000 父容器不对子View有任何限制,子View要多大给多大,也就是说子View的大小可以超过父容器的大小,例如ListView、ScrollView。
  • EXACTLY =1<< MODE_SHIFT:即: 01000000 00000000 00000000 00000000父容器已经测量出子View所需要的固定大小,不会再变了,即MeasureSpec中封装的SpecSize,对应于LayoutParams中的match_parent属性和设置的固定dp值。
  • AT_MOST =2 << MODE_SHIFT:即: 10000000 00000000 00000000 00000000父窗口限定了一个最大值给子View即SpecSize,对应于LayoutParams中的wrap_content,自适应大小。
//用户自定义View - > 获取SpecSize和SpecMode
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //宽度的模式
    int mWidthModle = MeasureSpec.getMode(widthMeasureSpec);
    //宽度大小
    int mWidthSize = MeasureSpec.getSize(widthMeasureSpec);
    //高度的模式
    int mHeightModle = MeasureSpec.getMode(heightMeasureSpec);
    //高度大小
    int mHeightSize = MeasureSpec.getSize(heightMeasureSpec);
    //如果明确大小,直接设置大小
    if (mWidthModle == MeasureSpec.EXACTLY) {
        ...
    }
    ...
}

View的测量流程(Measure)

我们在使用View时是直接设置LayoutParams,但是在View测量的时候,系统会将LayoutParams在父容器的约束下进行相对应的MeasureSpec,然后在根据这个MeasureSpec来确定View的测量后的宽高,由此可见,MeasureSpec不是LayoutParams唯一决定的,还需要父容器一起来决定,在进一步决定View的宽高。但是顶级View,也就是上文我们分析到的DecorView和普通的View的MeasureSpec计算有些区别,对于DecorView,其MeasureSpec是由屏幕的尺寸和LayoutParams决定的,而DecorView的默认LayoutParams就是match_parent(在初始化DecorView时可知),对于普通View来说,其MeasureSpec是由父容器的MeasureSpec和自身的LayoutParams决定。在 performTraversals() 方法中有如下一段:

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

我们再来看下getRootMeasureSpec方法的实现。

/**
 * @param windowSize  The available width or height of the window
 * @param rootDimension The layout params for one dimension (width or height) of the window.
 * 
 * @return The measure spec to use to measure the root view.
 */
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
	    case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
    }
    return measureSpec;
}

通过上述代码,对于DecorView来说就是走第一个case,也就是屏幕的尺寸。对于普通View来说,也就是我们Activity显示布局的根View是一个ViewGroup,我们再来看下performMeasure方法。

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

方法很简单,直接调用了mView.measure,这里的mView就是DecorView,也就是说,从顶级View开始了测量流程,那么我们直接进入measure流程。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
	boolean optical = isLayoutModeOptical(this);
	if (optical != isLayoutModeOptical(mParent)) {
		...
		if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
		    widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) {
			...
			if (cacheIndex < 0 || sIgnoreMeasureCache) {
			   // measure ourselves, this should set the measured dimension flag back
			   onMeasure(widthMeasureSpec, heightMeasureSpec);
			   mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
			} 
			...
		}
	}
}

可以看到,它在内部调用了onMeasure方法,由于DecorView是FrameLayout子类,因此它实际上调用的是DecorView#onMeasure方法。在该方法内部,主要是进行了一些判断,这里不展开来看了,到最后会调用到super.onMeasure方法,即FrameLayout#onMeasure方法。由于不同的ViewGroup有着不同的性质,那么它们的onMeasure必然是不同的,因此这里选择了FrameLayout的onMeasure方法来进行分析。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //获取当前布局内的子View数量
    int count = getChildCount();
    //判断当前布局的宽高是否是match_parent模式或者指定一个精确的大小,如果是则置measureMatchParent为false.
    final boolean measureMatchParentChildren =
            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
            MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    mMatchParentChildren.clear();
    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;
    //遍历所有类型不为GONE的子View
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            //对每一个子View进行测量
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //寻找子View中宽高的最大者,因为如果FrameLayout是wrap_content属性,那么它的大小取决于子View中的最大者
            maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            //如果FrameLayout是wrap_content模式,那么往mMatchParentChildren中添加
            //宽或者高为match_parent的子View,因为该子View的最终测量大小会受到FrameLayout的最终测量大小影响
            if (measureMatchParentChildren) {
                if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
            }
        }
    }
    // 最大最小宽高还要加上padding的值
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
    // 检查我们的最小高度和宽度
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
    // 检查我们前景的最小高度和宽度
    final Drawable drawable = getForeground();
    if (drawable != null) {
        maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
        maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
    }
    //保存测量结果
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
    //子View中设置为match_parent的个数
    count = mMatchParentChildren.size();
    //只有FrameLayout的模式为wrap_content的时候才会执行下列语句
    if (count > 1) {
        for (int i = 0; i < count; i++) {
            final View child = mMatchParentChildren.get(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            //对FrameLayout的宽度规格设置,因为这会影响子View的测量
            final int childWidthMeasureSpec;
            /**
              * 如果子View的宽度是match_parent属性,那么对当前FrameLayout的MeasureSpec修改:
              * 把widthMeasureSpec的宽度规格修改为:总宽度 - padding - margin,这样做的意思是:
              * 对于子View来说,如果要match_parent,那么它可以覆盖的范围是FrameLayout的测量宽度
              * 减去padding和margin后剩下的空间。
              * 以下两点的结论,可以查看getChildMeasureSpec()方法:
              * 如果子View的宽度是一个确定的值,比如50dp,那么FrameLayout的widthMeasureSpec的宽度规格修改为:
              * SpecSize为子View的宽度,即50dp,SpecMode为EXACTLY模式
              * 如果子View的宽度是wrap_content属性,那么FrameLayout的widthMeasureSpec的宽度规格修改为:
              * SpecSize为子View的宽度减去padding减去margin,SpecMode为AT_MOST模式
              */
            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);
            }
            //同理对高度进行相同的处理,这里省略...
            //对于这部分的子View需要重新进行measure过程
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

这里简单总结一下:首先,FrameLayout根据它的MeasureSpec来对每一个子View进行测量,即调用measureChildWithMargin方法。对于每一个测量完成的子View,会寻找其中最大的宽高,那么FrameLayout的测量宽高会受到这个子View的最大宽高的影响(wrap_content模式),接着调用setMeasureDimension方法,把FrameLayout的测量宽高保存。最后则是特殊情况的处理,即当FrameLayout为wrap_content属性时,如果其子View是match_parent属性的话,则要重新设置FrameLayout的测量规格,然后重新对该部分View测量。最后执行代码:child.measure方法,然后在measure方法,会调用onMeasure方法。我们自定义View需要操作的就是这个onMeasure。到此,绘制流程已经从ViewGroup转移到子View中了,具体的测量过程大同小异,读者自定查看源码。
最后简单概括一下整个流程:测量始于DecorView,通过不断的遍历子View的measure方法,根据ViewGroup的MeasureSpec及子View的LayoutParams来决定子View的MeasureSpec,进一步获取子View的测量宽高,然后逐层返回,不断保存ViewGroup的测量宽高。

View的布局流程(Layout)

前面提到,三大流程始于ViewRootImpl#performTraversals方法,在该方法内通过调用performMeasure、performLayout、performDraw这三个方法来进行measure、layout、draw流程,那么我们就从performLayout方法开始说,我们先看它的源码。

ViewRootImpl --> performLayout

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
    mLayoutRequested = false;
    mScrollMayChange = true;
    mInLayout = true;
    final View host = mView;
    if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
        Log.v(TAG, "Laying out " + host + " to (" +host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
    try {
    	//DecorView开始layout,从左上角(0,0)坐标开始
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); 
        //省略...
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    mInLayout = false;
}

由上面的代码可以看出,直接调用了host.layout方法,host也就是DecorView,属于FrameLayout,那么对于DecorView来说,调用layout方法,就是对它自身进行布局,最终确定自身的位置以及子View(如果有)的位置。显然,DecorView的左上位置为0,然后宽高为它的测量宽高。DecorView继承View,最终走的是View的layout逻辑。

View --> layout

/**
 * 通过这个方法为View及其所有的子View分配位置
 * 派生类不应该重写这个方法,而应该重写onLayout方法
 * 并且应该在重写的onLayout方法中完成对子View的布局
 * @param l Left position, relative to parent
 * @param t Top position, relative to parent
 * @param r Right position, relative to parent
 * @param b Bottom position, relative to parent
 */
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;
    // ① 通过setOpticalFrame或setFrame为View设置坐标,并判断位置是否发生改变
    boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        // ② 如果位置发生了改变,就调用onLayout方法完成布局逻辑
    	onLayout(changed, l, t, r, b);
        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);
            }
        }
    }
    .......
}

①号代码,调用了setFrame方法(setOpticalFrame最终也是调用setFrame),并把四个位置信息传递进去,这个方法用于确定View的四个顶点的位置,即初始化mLeft,mRight,mTop,mBottom这四个值,这个结束之后,DecorView就确定位置了,下一步要确定子View的位置了,也就是onLayout的逻辑。

View --> setFrame

protected boolean setFrame(int left, int top, int right, int bottom) {
    //省略...
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;
    mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
    //省略...
    return changed;
}

可以看出,它对mLeft、mTop、mRight、mBottom这四个值进行了初始化,对于每一个View,包括ViewGroup来说,以上四个值保存了View的位置信息,所以这四个值是最终宽高,也即是说,如果要得到View的位置信息,那么就应该在layout方法完成后调用getLeft()、getTop()等方法来取得最终宽高,如果是在此之前调用相应的方法,只能得到0的结果,所以一般我们是在onLayout方法中获取View的宽高信息。

onLayout

View基类的layout方法和measure不同,并没有使用final修饰,但注释中也清清楚楚地写着View的派生类不应该重写这个方法,而应该重写onLayout方法,来处理子View的布局方式。如果子类是View则可以不用处理onLayout,交给父类处理即可。但是如果子类是ViewGroup,是必须重写的onLayout方法中完成对子View的布局逻辑,因为ViewGroup中的onLayout是abstract修饰的。

  • View --> onLayout
/**
     * 当此视图应为其每个子视图指定大小和位置时,从布局调用
     * 带有子级的派生类应重写此方法并对其每个子级调用布局。
     * @param changed This is a new size or position for this view
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     */
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

当派生View集成的是View的话,onLayout是空方法,无需处理子View的布局,因为没有子View。

  • ViewGroup --> onLayout
//abstract 修饰,让派生类必须要重写这个方法,在内部处理子View的布局
@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);

当派生View继承的是ViewGroup的话,onLayout是abstract 方法,必须要重写而且处理子View。由于不同的布局容器的onMeasure方法均有不同的实现,因此不可能对所有布局方式都说一次,另外上一篇文章是用FrameLayout#onMeasure进行讲解的,那么现在也对FrameLayout#onLayout方法进行讲解。

//FrameLayout.java
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    //把父容器的位置参数传递进去
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    final int count = getChildCount();
    //以下四个值会影响到子View的布局参数
    //parentLeft由父容器的padding和Foreground决定
    final int parentLeft = getPaddingLeftWithForeground();
    //parentRight由父容器的width和padding和Foreground决定
    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();
            //获取子View的测量宽高
            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;
            //当子View设置了水平方向的layout_gravity属性时,根据不同的属性设置不同的childLeft
            //childLeft表示子View的 左上角坐标X值
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                /* 水平居中,由于子View要在水平中间的位置显示,因此,要先计算出以下:
                 * (parentRight - parentLeft -width)/2 此时得出的是父容器减去子View宽度后的
                 * 剩余空间的一半,那么再加上parentLeft后,就是子View初始左上角横坐标(此时正好位于中间位置),
                 * 假如子View还受到margin约束,由于leftMargin使子View右偏而rightMargin使子View左偏,所以最后
                 * 是 +leftMargin -rightMargin .
                 */
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                    lp.leftMargin - lp.rightMargin;
                    break;
                //水平居右,子View左上角横坐标等于 parentRight 减去子View的测量宽度 减去 margin
                case Gravity.RIGHT:
                    if (!forceLeftGravity) {
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    }
                //如果没设置水平方向的layout_gravity,那么它默认是水平居左
                //水平居左,子View的左上角横坐标等于 parentLeft 加上子View的magin值
                case Gravity.LEFT:
                default:
                    childLeft = parentLeft + lp.leftMargin;
            }
            //当子View设置了竖直方向的layout_gravity时,根据不同的属性设置同的childTop
            //childTop表示子View的 左上角坐标的Y值
            //分析方法同上
            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;
            }
            //对子元素进行布局,左上角坐标为(childLeft,childTop),右下角坐标为(childLeft+width,childTop+height)
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

先梳理一下以上逻辑:首先先获取父容器的padding值,然后遍历其每一个子View,根据子View的layout_gravity属性、子View的测量宽高、父容器的padding值、来确定子View的布局参数,然后调用child.layout方法,把布局流程从父容器传递到子元素。那么到目前为止,View的布局流程就已经全部分析完了。

View的绘制流程(Draw)

前面提到,三大工作流程始于ViewRootImpl#performTraversals,在这个方法内部会分别调用performMeasure,performLayout,performDraw三个方法来分别完成测量,布局,绘制流程。那么我们现在先从performDraw方法看起。

//ViewRootImpl.java
private void performDraw() {
	...
	final boolean fullRedrawNeeded = mFullRedrawNeeded;
	try {
		//判断是否需要重新绘制全部视图,如果是第一次绘制视图,那么显然应该绘制所以的视图
		//如果由于某些原因,导致了视图重绘,那么就没有必要绘制所有视图
	    draw(fullRedrawNeeded);
	} finally {
	    ...
	}
	...
}

private void draw(boolean fullRedrawNeeded) {
    ...
    //获取mDirty,该值表示需要重绘的区域
    final Rect dirty = mDirty;
    ...
    //如果fullRedrawNeeded为真,则把dirty区域置为整个屏幕,表示整个视图都需要绘制
    //第一次绘制流程,需要绘制所有视图
    if (fullRedrawNeeded) {
        mAttachInfo.mIgnoreDirtyState = true;
        dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
    }
    ...
    //把相关参数传递进去,包括dirty区域
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
        return;
    }
}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) {
    // 用软件渲染器绘制。
    final Canvas canvas;
    try {
        final int left = dirty.left;
        final int top = dirty.top;
        final int right = dirty.right;
        final int bottom = dirty.bottom;
        //锁定canvas区域,由dirty区域决定
        canvas = mSurface.lockCanvas(dirty);
        // The dirty rectangle can be modified by Surface.lockCanvas()
        //noinspection ConstantConditions
        if (left != dirty.left || top != dirty.top || right != dirty.right || bottom != dirty.bottom) {
            attachInfo.mIgnoreDirtyState = true;
        }
        canvas.setDensity(mDensity);
    } 
    try {
        if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
        }
        dirty.setEmpty();
        mIsAnimating = false;
        attachInfo.mDrawingTime = SystemClock.uptimeMillis();
        mView.mPrivateFlags |= View.PFLAG_DRAWN;
        try {
            canvas.translate(-xoff, -yoff);
            if (mTranslator != null) {
                mTranslator.translateCanvas(canvas);
            }
            canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
            attachInfo.mSetIgnoreDirtyState = false;
            //正式开始绘制
            mView.draw(canvas);
        }
    } 
    return true;
}

首先是实例化了Canvas对象,然后锁定该canvas的区域,由dirty区域决定,接着对canvas进行一系列的属性赋值,最后调用了mView.draw(canvas)方法,前面分析过,mView就是DecorView,也就是说从DecorView开始绘制,前面所做的一切工作都是准备工作,而现在则是正式开始绘制流程。

View的绘制

由于ViewGroup没有重写draw方法,因此所有的View都是调用View -> 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;
    /*
     * 绘制遍历执行必须按适当顺序执行的几个绘制步骤
     *      1. 绘制背景
     *      2. 如有必要,保存画布层以备褪色
     *      3. 绘图视图的内容
     *      4. 绘制子对象
     *      5. 如有必要,绘制淡入淡出的边并恢复层
     *      6. 绘制装饰(例如滚动条)
     */
    // 步骤1,如果需要,绘制背景
    int saveCount;
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }
    // 如果可能,跳过步骤2和5(常见情况)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // 第三步,画出内容
        if (!dirtyOpaque) onDraw(canvas);
        // 第四步,画子View
        dispatchDraw(canvas);
        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }
        // 步骤6,绘制装饰(前景,滚动条)
        onDrawForeground(canvas);
        // we're done...
        return;
    }
    ...
}

可以看到,draw过程比较复杂,但是逻辑十分清晰,而官方注释也清楚地说明了每一步的做法。我们首先来看一开始的标记位dirtyOpaque,该标记位的作用是判断当前View是否是透明的,如果View是透明的,那么根据下面的逻辑可以看出,将不会执行一些步骤,比如绘制背景、绘制内容等。这样很容易理解,因为一个View既然是透明的,那就没必要绘制它了。接着是绘制流程的六个步骤,这里先小结这六个步骤分别是什么,然后再展开来讲。

绘制流程的六个步骤
1、对View的背景进行绘制
2、保存当前的图层信息(可跳过)
3、绘制View的内容
4、对View的子View进行绘制(如果有子View)
5、绘制View的褪色的边缘,类似于阴影效果(可跳过)
6、绘制View的装饰(例如:滚动条)

其中第2步和第5步是可以跳过的,我们这里不做分析,我们重点来分析其它步骤。

Step 1 :绘制背景
private void drawBackground(Canvas canvas) {
    //mBackground是该View的背景参数,比如背景颜色
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }
    //根据View四个布局参数来确定背景的边界
    setBackgroundBounds();
    // 硬件加速绘制
    ...
    //获取当前View的mScrollX和mScrollY值
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        //如果scrollX和scrollY有值,则对canvas的坐标进行偏移,再绘制背景
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}

可以看出,这里考虑到了view的偏移参数,scrollX和scrollY,绘制背景在偏移后的view中绘制。

Step 3:绘制内容
/**
*  执行此操作来绘制图形。
* @param canvas 将在其上绘制背景的画布
*/
protected void onDraw(Canvas canvas) {
}

这里调用了View -> onDraw方法,View中该方法是一个空实现,因为不同的View有着不同的内容,这需要我们自己去实现,即在自定义View中重写该方法来实现。

Step 4: 绘制子View
//View
/**
 * 由draw调用以绘制子视图。这可能被派生类重写,
 * 以便在绘制其子级之前(但在绘制其自己的视图之后)获得控制权。
 * @param canvas 将在其上绘制背景的画布
 */
protected void dispatchDraw(Canvas canvas) {
}

View中该方法是空实现,因为他没有子视图,而在ViewGroup中重写了这个方法,下面我们来看看。

//ViewGroup
protected void dispatchDraw(Canvas canvas) {
    boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    int flags = mGroupFlags;
    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;
            }
        }
        int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
        final View child = (preorderedList == null)
                ? children[childIndex] : preorderedList.get(childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    ..
}

这里简单说明一下,里面主要遍历了所以子View,每个子View都调用了drawChild这个方法,我们找到这个方法。

//View -> drawChild
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
     return child.draw(canvas, this, drawingTime);
}
//View -> draw
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
   ...
    if (!drawingWithDrawingCache) {
        if (drawingWithRenderNode) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
        } else {
            // Fast path for layouts with no backgrounds
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                dispatchDraw(canvas);
            } else {
                draw(canvas);
            }
        }
    } else if (cache != null) {
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        if (layerType == LAYER_TYPE_NONE) {
            // no layer paint, use temporary paint to draw bitmap
            Paint cachePaint = parent.mCachePaint;
            if (cachePaint == null) {
                cachePaint = new Paint();
                cachePaint.setDither(false);
                parent.mCachePaint = cachePaint;
            }
            cachePaint.setAlpha((int) (alpha * 255));
            canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
        } else {
            // use layer paint to draw the bitmap, merging the two alphas, but also restore
            int layerPaintAlpha = mLayerPaint.getAlpha();
            mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
            canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
            mLayerPaint.setAlpha(layerPaintAlpha);
        }
    }
}

我们主要来看核心部分,首先判断是否已经有缓存,即之前是否已经绘制过一次了,如果没有,则会调用draw(canvas)方法,开始正常的绘制,即上面所说的六个步骤,否则利用缓存来显示。这一步也可以归纳为ViewGroup绘制过程,它对子View进行了绘制,而子View又会调用自身的draw方法来绘制自身,这样不断遍历子View及子View的不断对自身的绘制,从而使得View树完成绘制。

Step 6: 绘制装饰

所谓的绘制装饰,就是指View除了背景、内容、子View的其余部分,例如滚动条等,我们看View -> onDrawForeground

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

和一般的绘制流程非常相似,都是先设定绘制区域,然后利用canvas进行绘制。那么,到目前为止,View的绘制流程也讲述完毕了,希望这篇文章对你们起到帮助作用,谢谢你们的阅读。

结尾

有句话说的不错,好记忆不如烂笔头。Android学习过程中,最好的方式就是记录,记多了看多了不知不觉就成专家了。而写博客就是帮自己梳理知识点的最好的方式,你可以尝试去多读几篇类似的文章,然后自己梳理记下来,写出自己的博客,你会受益匪浅而且极易深刻,不信你试试!!共勉吧~

发布了17 篇原创文章 · 获赞 100 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/yinhaide/article/details/104040329