首页 > 代码库 > Android View系统分析之从setContentView说开来(一)

Android View系统分析之从setContentView说开来(一)

 今天是中秋节,先祝各位中秋快乐吧。作为北漂的人,对于过节最大的感触就是没气 氛~ 中秋是一个特别重要的节日,小的时候过中秋都是特别快乐的,有月饼吃,和家人上月,过完中秋要去亲戚家拜访等等。现在对于我们来说也就是一个节日罢了,窝 在家里看点电视、看点书、吃顿好的,虽说生活好了,但日子过得没啥滋味。废话不多说,开始今天的学习吧。

Hello World

       对于学习编程的人而言,大多数人第一个项目都是著名的"Hello World",自从K&R开了这个先例,后面的人就很少有打破的。学习Android开发也是这样,我们第一次创建应用,估计也就是运行程序,然 后在模拟器上输出一个Hello World,我们看到最简单的Activity中的内容大致是这样的:

 

[java] view plaincopy
  1. public class MainActivity extends Activity {  
  2.   
  3.   
  4.     @Override  
  5.     public void onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         setContentView(R.layout.main_activity);  
  8.     }  
  9.   
  10. }  

main_activity.xml大致是这样的 :

 

[java] view plaincopy
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:gravity="center" >  
  6.   
  7.     <TextView  
  8.         android:layout_width="match_parent"  
  9.         android:layout_height="wrap_content"  
  10.         android:text="@string/hello_world" />  
  11.   
  12. </RelativeLayout>  

然后执行程序,我们就可以看到模拟器中的Hello World了。


  图1

       我们在整个过程中做的事情很少,在我们的main_activity.xml我们只有一个显示文本的TextView,但是在上图中却还多了一个title。我们好奇的是整个过程是怎么工作的?对于大型系统来说细节总是复杂的,在下水平有限,所以我们今天只来理一下它的基本脉络。

setContentView

一般来说我们设置页面的内容视图是都是通过setContentView方法,那么我们就以2.3源码为例就来看看Activity中的setContentView到底做了什么吧。

[java] view plaincopy
  1. /** 
  2.  * Set the activity content from a layout resource.  The resource will be 
  3.  * inflated, adding all top-level views to the activity. 
  4.  *  
  5.  * @param layoutResID Resource ID to be inflated. 
  6.  */  
  7. public void setContentView(int layoutResID) {  
  8.     getWindow().setContentView(layoutResID);  
  9. }  
  10.    
  11. public Window getWindow() {  
  12.     return mWindow;  
  13. }  
  14.   
  15.   
  16. private Window mWindow;  

我们可以看到,实际上调用的mWindow的setContentView方法,在Android Touch事件分发过程这篇文章中我们已经指出Window的实现类为PhoneWindow类,我们就移步到PhoneWindow的setConentView吧,核心源码如下 :

 

[java] view plaincopy
  1. @Override  
  2. public void setContentView(int layoutResID) {  
  3.     if (mContentParent == null) {  
  4.         installDecor();         // 1、生成DecorView  
  5.     } else {  
  6.         mContentParent.removeAllViews();  
  7.     }  
  8.     mLayoutInflater.inflate(layoutResID, mContentParent);// 2、将layoutResId的布局添加到mContentParent中  
  9.     final Callback cb = getCallback();  
  10.     if (cb != null) {  
  11.         cb.onContentChanged();  
  12.     }  
  13. }  
  14.     // 构建mDecor对象,并且初始化标题栏和Content Parent(我们要显示的内容区域)  
  15.     private void installDecor() {  
  16.     if (mDecor == null) {  
  17.         mDecor = generateDecor();          // 3、构建DecorView  
  18.         mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);  
  19.         mDecor.setIsRootNamespace(true);  
  20.     }  
  21.     if (mContentParent == null) {  
  22.         mContentParent = generateLayout(mDecor);              // 4、获取ContentView容器,即显示内容的区域  
  23.   
  24.         mTitleView = (TextView)findViewById(com.android.internal.R.id.title); 5、设置Title等  
  25.         if (mTitleView != null) {  
  26.             if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {  
  27.                 View titleContainer = findViewById(com.android.internal.R.id.title_container);  
  28.                 if (titleContainer != null) {  
  29.                     titleContainer.setVisibility(View.GONE);  
  30.                 } else {  
  31.                     mTitleView.setVisibility(View.GONE);  
  32.                 }  
  33.                 if (mContentParent instanceof FrameLayout) {  
  34.                     ((FrameLayout)mContentParent).setForeground(null);  
  35.                 }  
  36.             } else {  
  37.                 mTitleView.setText(mTitle);  
  38.             }  
  39.         }  
  40.     }  
  41. }  
  42.   
  43.     protected DecorView generateDecor() {  
  44.     return new DecorView(getContext(), -1);    // 构建mDecor对象  
  45. }  

我们可以看到,setContentView的基本流程简单概括就是如下几步:

1、构建mDecor对象。mDecor就是整个窗口的顶层视图,它主要包含了Title和Content View两个区域 (参考图1中的两个区域 ),Title区域就是我们的标题栏,Content View区域就是显示我们xml布局内容中的区域。关于mDecor对象更多说明也请参考Android Touch事件分发过程这篇文章;

2、设置一些关于窗口的属性,初始化标题栏区域和内容显示区域;

这里比较复杂的就是generateLayout(mDecor)这个函数,我们一起来分析一下吧。

 

[java] view plaincopy
  1. // 返回用于显示我们设置的页面内容的ViewGroup容器  
  2. protected ViewGroup generateLayout(DecorView decor) {  
  3.    // Apply data from current theme.  
  4.    // 1、获取窗口的Style属性  
  5.    TypedArray a = getWindowStyle();  
  6.   
  7.    if (false) {  
  8.        System.out.println("From style:");  
  9.        String s = "Attrs:";  
  10.        for (int i = 0; i < com.android.internal.R.styleable.Window.length; i++) {  
  11.            s = s + " " + Integer.toHexString(com.android.internal.R.styleable.Window[i]) + "="  
  12.                    + a.getString(i);  
  13.        }  
  14.        System.out.println(s);  
  15.    }  
  16.    // 窗口是否是浮动的  
  17.    mIsFloating = a.getBoolean(com.android.internal.R.styleable.Window_windowIsFloating, false);  
  18.    int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)  
  19.            & (~getForcedWindowFlags());  
  20.    if (mIsFloating) {  
  21.        setLayout(WRAP_CONTENT, WRAP_CONTENT);  
  22.        setFlags(0, flagsToUpdate);  
  23.    } else {  
  24.        setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);  
  25.    }  
  26.    // 设置是否不显示title区域  
  27.    if (a.getBoolean(com.android.internal.R.styleable.Window_windowNoTitle, false)) {  
  28.        requestFeature(FEATURE_NO_TITLE);  
  29.    }  
  30.    // 设置全屏的flag  
  31.    if (a.getBoolean(com.android.internal.R.styleable.Window_windowFullscreen, false)) {  
  32.        setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN&(~getForcedWindowFlags()));  
  33.    }  
  34.   
  35.    if (a.getBoolean(com.android.internal.R.styleable.Window_windowShowWallpaper, false)) {  
  36.        setFlags(FLAG_SHOW_WALLPAPER, FLAG_SHOW_WALLPAPER&(~getForcedWindowFlags()));  
  37.    }  
  38.   
  39.    WindowManager.LayoutParams params = getAttributes();  
  40.    // 设置输入法模式  
  41.    if (!hasSoftInputMode()) {  
  42.        params.softInputMode = a.getInt(  
  43.                com.android.internal.R.styleable.Window_windowSoftInputMode,  
  44.                params.softInputMode);  
  45.    }  
  46.   
  47.    if (a.getBoolean(com.android.internal.R.styleable.Window_backgroundDimEnabled,  
  48.            mIsFloating)) {  
  49.        /* All dialogs should have the window dimmed */  
  50.        if ((getForcedWindowFlags()&WindowManager.LayoutParams.FLAG_DIM_BEHIND) == 0) {  
  51.            params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;  
  52.        }  
  53.        params.dimAmount = a.getFloat(  
  54.                android.R.styleable.Window_backgroundDimAmount, 0.5f);  
  55.    }  
  56.    // 窗口动画  
  57.    if (params.windowAnimations == 0) {  
  58.        params.windowAnimations = a.getResourceId(  
  59.                com.android.internal.R.styleable.Window_windowAnimationStyle, 0);  
  60.    }  
  61.   
  62.    // The rest are only done if this window is not embedded; otherwise,  
  63.    // the values are inherited from our container.  
  64.    if (getContainer() == null) {  
  65.        if (mBackgroundDrawable == null) {  
  66.            if (mBackgroundResource == 0) {  
  67.                mBackgroundResource = a.getResourceId(  
  68.                        com.android.internal.R.styleable.Window_windowBackground, 0);  
  69.            }  
  70.            if (mFrameResource == 0) {  
  71.                mFrameResource = a.getResourceId(com.android.internal.R.styleable.Window_windowFrame, 0);  
  72.            }  
  73.            if (false) {  
  74.                System.out.println("Background: "  
  75.                        + Integer.toHexString(mBackgroundResource) + " Frame: "  
  76.                        + Integer.toHexString(mFrameResource));  
  77.            }  
  78.        }  
  79.        mTextColor = a.getColor(com.android.internal.R.styleable.Window_textColor, 0xFF000000);  
  80.    }  
  81.   
  82.    // Inflate the window decor.   
  83.    // 2、根据一些属性来选择不同的顶层视图布局,例如设置了FEATURE_NO_TITLE的属性,那么就选择没有Title区域的那么布局;  
  84.    // layoutResource布局就是整个Activity的布局,其中含有title区域和content区域,content区域就是用来显示我通过  
  85.    // setContentView设置进来的内容区域,也就是我们要显示的视图。  
  86.   
  87.    int layoutResource;  
  88.    int features = getLocalFeatures();  
  89.    // System.out.println("Features: 0x" + Integer.toHexString(features));  
  90.    if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {  
  91.        if (mIsFloating) {  
  92.            layoutResource = com.android.internal.R.layout.dialog_title_icons;  
  93.        } else {  
  94.            layoutResource = com.android.internal.R.layout.screen_title_icons;  
  95.        }  
  96.        // System.out.println("Title Icons!");  
  97.    } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0) {  
  98.        // Special case for a window with only a progress bar (and title).  
  99.        // XXX Need to have a no-title version of embedded windows.  
  100.        layoutResource = com.android.internal.R.layout.screen_progress;  
  101.        // System.out.println("Progress!");  
  102.    } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {  
  103.        // Special case for a window with a custom title.  
  104.        // If the window is floating, we need a dialog layout  
  105.        if (mIsFloating) {  
  106.            layoutResource = com.android.internal.R.layout.dialog_custom_title;  
  107.        } else {  
  108.            layoutResource = com.android.internal.R.layout.screen_custom_title;  
  109.        }  
  110.    } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {  
  111.        // If no other features and not embedded, only need a title.  
  112.        // If the window is floating, we need a dialog layout  
  113.        if (mIsFloating) {  
  114.            layoutResource = com.android.internal.R.layout.dialog_title;  
  115.        } else {  
  116.            layoutResource = com.android.internal.R.layout.screen_title;  
  117.        }  
  118.        // System.out.println("Title!");  
  119.    } else {  
  120.        // Embedded, so no decoration is needed.  
  121.        layoutResource = com.android.internal.R.layout.screen_simple;  
  122.        // System.out.println("Simple!");  
  123.    }  
  124.   
  125.    mDecor.startChanging();  
  126.    // 3、加载视图  
  127.    View in = mLayoutInflater.inflate(layoutResource, null);  
  128.    // 4、将layoutResource的内容添加到mDecor中  
  129.    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));  
  130.    // 5、获取到我们的内容显示区域,这是一个ViewGroup类型的,其实是FrameLayout  
  131.    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);  
  132.    if (contentParent == null) {  
  133.        throw new RuntimeException("Window couldn‘t find content container view");  
  134.    }  
  135.   
  136.    if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {  
  137.        ProgressBar progress = getCircularProgressBar(false);  
  138.        if (progress != null) {  
  139.            progress.setIndeterminate(true);  
  140.        }  
  141.    }  
  142.   
  143.    // 6、设置一些背景、title等属性  
  144.    // Remaining setup -- of background and title -- that only applies  
  145.    // to top-level windows.  
  146.    if (getContainer() == null) {  
  147.        Drawable drawable = mBackgroundDrawable;  
  148.        if (mBackgroundResource != 0) {  
  149.            drawable = getContext().getResources().getDrawable(mBackgroundResource);  
  150.        }  
  151.        mDecor.setWindowBackground(drawable);  
  152.        drawable = null;  
  153.        if (mFrameResource != 0) {  
  154.            drawable = getContext().getResources().getDrawable(mFrameResource);  
  155.        }  
  156.        mDecor.setWindowFrame(drawable);  
  157.   
  158.        // System.out.println("Text=" + Integer.toHexString(mTextColor) +  
  159.        // " Sel=" + Integer.toHexString(mTextSelectedColor) +  
  160.        // " Title=" + Integer.toHexString(mTitleColor));  
  161.   
  162.        if (mTitleColor == 0) {  
  163.            mTitleColor = mTextColor;  
  164.        }  
  165.   
  166.        if (mTitle != null) {  
  167.            setTitle(mTitle);  
  168.        }  
  169.        setTitleColor(mTitleColor);  
  170.    }  
  171.   
  172.    mDecor.finishChanging();  
  173.   
  174.    return contentParent;  

其实也就是这么几个步骤:

1、获取用户设置的一些属性与Flag;

2、根据一些属性选择不同的顶层视图布局,例如FEATURE_NO_TITLE则选择没有title的布局文件等;这里我们看一个与图1中符合的顶层布局吧,即layoutResource = com.android.internal.R.layout.screen_title的情形:

[html] view plaincopy
  1. <?xml version="1.0" encoding="utf-8"?>  
  2.   
  3. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  4.     android:orientation="vertical"  
  5.     android:fitsSystemWindows="true">  
  6.     <!-- Popout bar for action modes -->  
  7.     <ViewStub android:id="@+id/action_mode_bar_stub"  
  8.               android:inflatedId="@+id/action_mode_bar"  
  9.               android:layout="@layout/action_mode_bar"  
  10.               android:layout_width="match_parent"  
  11.               android:layout_height="wrap_content" />  
  12.     <!--  title区域-->  
  13.     <FrameLayout  
  14.         android:layout_width="match_parent"   
  15.         android:layout_height="?android:attr/windowTitleSize"  
  16.         style="?android:attr/windowTitleBackgroundStyle">  
  17.         <TextView android:id="@android:id/title"   
  18.             style="?android:attr/windowTitleStyle"  
  19.             android:background="@null"  
  20.             android:fadingEdge="horizontal"  
  21.             android:gravity="center_vertical"  
  22.             android:layout_width="match_parent"  
  23.             android:layout_height="match_parent" />  
  24.     </FrameLayout>  
  25.     <!--内容显示区域, 例如main_activity.xml布局就会被放到这个ViewGroup下面 -->  
  26.     <FrameLayout android:id="@android:id/content"  
  27.         android:layout_width="match_parent"   
  28.         android:layout_height="0dip"  
  29.         android:layout_weight="1"  
  30.         android:foregroundGravity="fill_horizontal|top"  
  31.         android:foreground="?android:attr/windowContentOverlay" />  
  32. </LinearLayout>  

我们可以看到有两个区域,即title区域和content区域,generateLayout函数中的

[java] view plaincopy
  1. // 5、获取到我们的内容显示区域,这是一个ViewGroup类型的,其实是FrameLayout  
  2.        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);  

获取的就是xml中id为content的FrameLayout,这个content就是我们的内容显示区域。整个布局对应的效果如下 :


这两个区域就组成了mDecor视图,我们的main_activity.xml就是放在内容视图这个区域的。

3、加载顶层布局文件,转换为View,将其添加到mDecor中;

4、获取内容容器Content Parent,即用于显示我们的内容的区域;

5、设置一些背景图和title等。

在经过这几步,我们就得到了mContentParent,这就是用来装载我们的视图的ViewGroup。再回过头来看setContentView函数:

 

[java] view plaincopy
  1. public void setContentView(int layoutResID) {  
  2.     if (mContentParent == null) {  
  3.         installDecor();         // 1、生成DecorView,并且根据窗口属性加载顶级视图布局、获取mContentParent、设置一些基本属性等  
  4.     } else {  
  5.         mContentParent.removeAllViews();  
  6.     }  
  7.     mLayoutInflater.inflate(layoutResID, mContentParent);// 2、将layoutResId加载到mContentParent中,这里的layoutResId就是我们的main_activity.xml  
  8.     final Callback cb = getCallback();  
  9.     if (cb != null) {  
  10.         cb.onContentChanged();  
  11.     }  
  12. }  

我们看看LayoutInflater的inflate函数吧 : 

 

[java] view plaincopy
  1. /** 
  2.  * Inflate a new view hierarchy from the specified xml resource. Throws 
  3.  * {@link InflateException} if there is an error. 
  4.  *  
  5.  * @param resource ID for an XML layout resource to load (e.g., 
  6.  *        <code>R.layout.main_page</code>) 
  7.  * @param root Optional view to be the parent of the generated hierarchy. 
  8.  * @return The root View of the inflated hierarchy. If root was supplied, 
  9.  *         this is the root View; otherwise it is the root of the inflated 
  10.  *         XML file. 
  11.  */  
  12. public View inflate(int resource, ViewGroup root) {  
  13.     return inflate(resource, root, root != null);  
  14. }  
  15.   
  16. /** 
  17.  * Inflate a new view hierarchy from the specified xml resource. Throws 
  18.  * {@link InflateException} if there is an error. 
  19.  *  
  20.  * @param resource ID for an XML layout resource to load (e.g., 
  21.  *        <code>R.layout.main_page</code>) 
  22.  * @param root Optional view to be the parent of the generated hierarchy (if 
  23.  *        <em>attachToRoot</em> is true), or else simply an object that 
  24.  *        provides a set of LayoutParams values for root of the returned 
  25.  *        hierarchy (if <em>attachToRoot</em> is false.) 
  26.  * @param attachToRoot Whether the inflated hierarchy should be attached to 
  27.  *        the root parameter? If false, root is only used to create the 
  28.  *        correct subclass of LayoutParams for the root view in the XML. 
  29.  * @return The root View of the inflated hierarchy. If root was supplied and 
  30.  *         attachToRoot is true, this is root; otherwise it is the root of 
  31.  *         the inflated XML file. 
  32.  */  
  33. public View inflate(int resource, ViewGroup root, boolean attachToRoot) {  
  34.     if (DEBUG) System.out.println("INFLATING from resource: " + resource);  
  35.     XmlResourceParser parser = getContext().getResources().getLayout(resource);  
  36.     try {  
  37.         return inflate(parser, root, attachToRoot);  
  38.     } finally {  
  39.         parser.close();  
  40.     }  
  41. }  

实际上就是将layoutResId这个布局的视图附加到mContentParent中。

DecorView

移步 : DecorView 。

ViewGroup

ViewGroup从语义上来说就是视图组,它也继承自View类,它其实就是视图的容器。我们看官方的定义 : 

[java] view plaincopy
  1. * A ViewGroup is a special view that can contain other views  
  2. * (called children.) The view group is the base class for layouts and views  
  3. * containers. This class also defines the  
  4. * {@link android.view.ViewGroup.LayoutParams} class which serves as the base  
  5. class for layouts parameters.  

我们通过ViewGroup来组织、管理子视图,例如我们常见的FrameLayout、LinearLayout、RelativeLayout、ListView等都是ViewGroup类型,总之只要能包含其他View或者ViewGroup的都是ViewGroup类型。使用ViewGroup来构建视图树。



View

View就是UI界面上的一个可见的组件,任何在UI上可见的都为View的子类。我们看官方定义 :

[java] view plaincopy
  1. * This class represents the basic building block for user interface components. A View  
  2. * occupies a rectangular area on the screen and is responsible for drawing and  
  3. * event handling. View is the base class for <em>widgets</em>, which are  
  4. * used to create interactive UI components (buttons, text fields, etc.). The  
  5. * {@link android.view.ViewGroup} subclass is the base class for <em>layouts</em>, which  
  6. * are invisible containers that hold other Views (or other ViewGroups) and define  
  7. * their layout properties.  

TextView、Button、ImageView、FrameLayout、LinearLayout、ListView等都是View的子类。

总结 

整个窗口由Title区域和Content区域组成,Content区域就是我们要显示内容的区域,在这个区域中mContentParent是根ViewGroup,由mContentParent组织、管理其子视图,从而构建整个视图树。当Activity启动时,就将这些内容就会显示在手机上。

 

Android View系统分析之从setContentView说开来(一)