美文网首页Android开发Android技术知识
Android开发——系统ViewTree的创建与绘制

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

作者: 谁动了我的代码 | 来源:发表于2022-12-19 16:39 被阅读0次

    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中再做详细说明。

    以上是Android开发中的ViewTree的创建与绘制;更多Android核心技术学习前往:《Android核心技术手册》参考文档学习,里面记载了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的测量、布局、绘制

    相关文章

      网友评论

        本文标题:Android开发——系统ViewTree的创建与绘制

        本文链接:https://www.haomeiwen.com/subject/zoeeqdtx.html