Android开发——系统ViewTree的创建与绘制

192 阅读8分钟

ViewTree的创建

在分析Activity启动过程中:

Activity启动流程会执行startSpecificActivityLocked
	ActivityThread.performLaunchActivity		//onCreate -> onStart
		SetContentView创建DecorView(DecorView = ContentView(用户需要显示的组件) + 系统其它组件)
	ActivityThread.handleResumeActivity			//onResume
		WindowManager.addView
			WindowManagerImpl.addView	 (将DecorView保存到WindowManagerImpl.mViews,将ViewRootImpl保存到mRoots)
				ViewRootImpl.setView		 (将DecorView保存到ViewRootImpl.mView)

ViewTree的创建过程首先从onCreate开始分析:

public class MainActivity extends AppCompatActivity {
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
	}
}

public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
        TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }
}

class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
        implements MenuBuilder.Callback, LayoutInflater.Factory2 {
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }
}

1.1 分析ensureSubDecor

class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
        implements MenuBuilder.Callback, LayoutInflater.Factory2 {
    private void ensureSubDecor() {
        if (!mSubDecorInstalled) {
            mSubDecor = createSubDecor();
    }

    private ViewGroup createSubDecor() {
        TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
        mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
        final LayoutInflater inflater = LayoutInflater.from(mContext);
        ViewGroup subDecor = null;
    
        if (!mWindowNoTitle) {
            if (mIsFloating) {
                // If we're floating, inflate the dialog title decor
                subDecor = (ViewGroup) inflater.inflate(		//根据R.styleable属性加载不同的布局文件
                        R.layout.abc_dialog_title_material, null);
    
                // Floating windows can never have an action bar, reset the flags
                mHasActionBar = mOverlayActionBar = false;
            } else if (mHasActionBar) {
    		...
            }
        } else {
            ...
        }
    
        final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                R.id.action_bar_activity_content);
    
        final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
        if (windowContentView != null) {
            // There might be Views already added to the Window's content view so we need to
            // migrate them to our content view
            while (windowContentView.getChildCount() > 0) {
                final View child = windowContentView.getChildAt(0);
                windowContentView.removeViewAt(0);
                contentView.addView(child);
            }
    
            // Change our content FrameLayout to use the android.R.id.content id.
            // Useful for fragments.
            windowContentView.setId(View.NO_ID);
            contentView.setId(android.R.id.content);	//设置subDecor的contentView的id为android.R.id.content
        }
    
        // Now set the Window's content view with the decor
        mWindow.setContentView(subDecor);	//根据加载的布局文件创建的view - subDecor设置为PhoneWindow的contentView
        return subDecor;
    }
}

根据R.styleable属性加载不同的布局文件,根据布局文件创建ViewGroup并保存到mSubDecor 将加载的布局文件创建的view - subDecor设置为PhoneWindow的contentView,即mWindow.setContentView(subDecor); 其中subDecor的contentView的id为android.R.id.content,因此创建的subDecor为ViewGroup,subDecor中真正的内容是id为android.R.id.content的用户自定义View

1.2 分析inflate

public abstract class LayoutInflater {
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        final XmlResourceParser parser = res.getLayout(resource);	//resource = R.layout.activity_main
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
	public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);		//attrs为布局文件的attr参数
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;
    
            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }
    
                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }
    
                final String name = parser.getName();	//获取根节点的标签名
                if (TAG_MERGE.equals(name)) {	//如果是merge标签,一般走else
                    ...
                } else {
                    // Temp is the root view that was found in the xml
    				//根据根节点的标签名生成root view --- temp
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);	//root是id为android.R.id.content的contentView,name为布局文件R.layout.activity_main的根节点的标签名,attrs为布局文件的attr参数
    
                    ViewGroup.LayoutParams params = null;
    
                    if (root != null) {
                        // Create layout params that match root, if supplied
    					//创建root view的布局属性params,例如宽高、字体大小
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {		//如果root不为null且attachToRoot设为false
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);		//将布局文件最外层的所有layout属性设置到布局文件的根节点temp
                        }
                    }
    
                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);	//parser为根节点,temp为根节点对应的view,attrs为根节点对应的属性
    
                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
    
                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {		//如果id为android.R.id.content的root为空,或者attachToRoot为false
                        result = temp;		//直接返回布局文件的根节点对应的view --- temp
                    }
                }
            }
            return result;
        }
    }
}

public abstract class LayoutInflater {
    final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }

    void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        final int depth = parser.getDepth();		//获取子节点的深度
        int type;
        boolean pendingRequestFocus = false;
        while (((type = parser.next()) != XmlPullParser.END_TAG ||		//使用while循环
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
            final String name = parser.getName();	//获取子节点的name
    		final View view = createViewFromTag(parent, name, context, attrs);		//创建子节点对应的view
    		final ViewGroup viewGroup = (ViewGroup) parent;		//viewGroup即为root view
    		final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);	
    		rInflateChildren(parser, view, attrs, true);	//递归调用
    		viewGroup.addView(view, params);	//将创建的子节点对应的view添加到root view中
        }
    }
}

总结:

1> ViewTree创建总结 — 即创建DecorView的过程

a. 在Activity的onCreate中调用setContentView,传入自定义布局文件对应的id b. 调用createSubDecor根据R.styleable属性加载不同的布局文件创建ViewGroup — subDecor,并将subDecor设置为PhoneWindow的contentView,subDecor的contentView对应的id设置为android.R.id.content c. 调用inflate首先找到自定义布局文件的根节点对应的View — temp,然后在rInflateChildren中的while循环递归创建子节点对应的View,将创建的子View添加到temp中,最终将temp添加到android.R.id.content对应的contentView中

2> inflate参数总结

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) //parser对应R.layout.activity_main //root对应android.R.id.content //attachToRoot:true表示将R.layout.activity_main中的根节点对应的view添加到android.R.id.content对应的contentView中,false表示直接将parser中的根节点对应的view直接作为android.R.id.content对应的contentView,而不是添加进去

a. 如果root为null,attachToRoot将失去作用,设置任何值都没有意义 b. 如果root不为null,attachToRoot参数默认为true b.1 attachToRoot设为true,则会给加载的布局文件的指定一个父布局,即id为android.R.id.content的root b.2 attachToRoot设为false,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效

View绘制的三大流程

1.measure

1.1.View的measure过程

View的measure过程由其measure方法来完成,measure方法是一个final类型方法(意味着子类不能复写)。在View的measure方法中会调用View的onMeasure方法,onMeasure方法也是我们在自定义View时重点需要复写的方法。

onMeasure方法有两个参数:widthMeasureSpec和heightMeasureSpec,分别表示宽度测量规格和高度测量规格,它们都对应着同一个类——MeasureSpec。

MeasureSpec表示View的尺寸规格,其代表着一个32位的int值。 其中高2位代表SpecMode,即测量模式;低30位代表SpecSize,即该模式下的大小。 SpecMode有三类:UNSPECIFIED、EXACTLY和AT_MOST。其中UNSPECIFIED主要用于系统内部,此处可以不做考虑。EXACTLY表示View的MeasureSpec所提供的大小是精确值,也是View需要展现的大小;AT_MOST则表示View的MeasureSpec所提供的的大小是一个最大上限值,View的实际大小不能大于这个值。

一个View的MeasureSpec是由其自身的LayoutParms和父容器的MeasureSpec来共同决定的。

秘诀: ①当View的布局参数是明确的大小(指定dp值),该View的SpecMode一律为EXACTLY,并且SpecSize值为View自身布局参数中设置的大小。 ②当View的布局参数是match_parent,则其SpecMode和父容器的SpecMode相同,SpecSize为父容器的大小。 ③当View的布局参数是wrap_content,该View的SpecMode一律为AT_MOST,且其SpecSize为其父容器的大小。

在View的onMeasure方法中,通过调用setMeasuredDimension方法,即完成了measure过程。

1.2.ViewGroup的measure过程

ViewGroup除了要完成自己的measure过程,还会遍历调用所有子元素的measure方法。

由于不同的ViewGroup有不同的布局特性,这导致它们的测量细节各不相同。ViewGroup是个抽象类,它自身并未重写View的onMeasure方法,它的测量过程需要其各个子类去具体实现,如LinearLayout、RelativeLayout等。

1.3.在Activity中获取View的的测量宽高

由于View的measure过程和Activity的生命周期方法不是同步执行的,所以无法保证Activity执行onCreate、onStart、onResume时View已经测量完毕了。如果View没有测量完毕,那么获得的宽高大小就是0。 这里主要介绍两种常用的方法。 ①View.post(Runnable) 通过post方法可以将runnable投递到主线程消息队列的尾部,等Looper调用此runnable时,View已经完成初始化了。因此可以在Runnable中获取View的宽高。 ②ViewTreeObserver 为ViewTreeObserver添加监听器OnGlobalLayoutListener,当View树的状态发生改变或者View树内部的View可见性发生变化时,onGlobalLayout方法将会被回调,因此这是获取此View的宽高的一个好时机。

    @Override
    protected void onStart() {
        super.onStart();
        ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });
    }

2.layout

Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout方法中会遍历所有的子元素并调用其layout方法,在子元素的layout方法中子元素的onLayout方法又会接着被调用。

在layout方法中,首先通过setFrame方法来设定View的四个顶点的位置,View的四个顶点一旦确定,其在父容器中的位置也就确定了。接着会调用onLayout方法,用来确定子元素的位置,这个方法在View和ViewGroup中都没有实现,需要根据子类的需求去具体实现。

总之,layout方法用来确定自身位置,onLayout方法用来确定所有子元素的位置

View的测量宽高和最终宽高的区别?

View的测量宽高对应View的getMeasuredWidth/Height方法,最终宽高则对应View的getWidth/Height方法。

在View的默认实现中,View的测量宽高和最终宽高是相等的,只不过测量宽高形成于View的measure过程,最终宽高形成于View的layout过程,两者赋值时机不同而已。

例外:可以通过重写View的layout方法来强行改变View的最终宽高。

3.draw

Draw的作用是将View绘制到屏幕上。 通过浏览View的draw方法,可以看出View的绘制过程遵循如下几步:

  1. 绘制背景
  2. 绘制自己(onDraw)
  3. 绘制子元素
  4. 绘制装饰

其中第二步,通过调用onDraw方法绘制自己的内容,这也是在自定义View时非常重要的需要复写的方法。 onDraw方法中的内容需要根据视图的特征去具体实现,这方面内容在自定义View中再做详细说明。

042a88fbfa797e5d170281ecca5f70c0_v2-a3883f0642ab19c2c165a8923e701066_720w.png

以上是Android开发中的ViewTree的创建与绘制;更多Android核心技术可以前往传送直达↓↓↓ :link.juejin.cn/?target=htt…,里面记载了30多个Android开发技术点,供大家浏览学习。

文末

Activity启动流程会执行startSpecificActivityLocked

  • 首先调用ActivityThread.performLaunchActivity,此函数最终调用onCreate -> onStart,在onCreate中调用SetContentView创建DecorView
  • 然后调用ActivityThread.handleResumeActivity,此函数首先调用onResume,然后调用ViewRootImpl.setView将创建的DecorView保存到ViewRootImpl.mView,在ViewRootImpl.performTraversals中分别调用performMeasure、performLayout、performDraw进行View的测量、布局、绘制