原文地址:http://blog.csdn.net/qinjuning/article/details/8829877
通过对前面的一篇博文<从setContentView()谈起>的学习,我们掌握了Activity组件布局文件地创建过程以及
其顶层控件DecorView,今天我们继续庖丁解牛---深入到其中的generateLayout()方法,步步为营掌握一下内容:
1、Activity中Theme(主题)的系统定义以及使用之处;
2、如何根据设置的Feature(属性)选择合适的布局文件。
另外,对于下文中Theme和Style的概念进行一个简要说明:
都是由<style />节点进行定义的。但应用在<application />和<activity />则为theme,应用在<View />则为style.
一、关于Theme主题的使用方法以及原理分析
通常来说,可以直接使用系统定义好的Style/Theme,毕竟,系统为我们提供了丰富地选择。当然,你也可以
自定义Theme,前提是该Theme必须继承与某个已经存在地Theme,否则编译器会提示错误的。
1、 应用Theme属性两种方式
①、在AndroidManifest.xml文件中在<application/>或者<activity />节点设置android:theme属性.
②、直接在代码中调用方法setTheme()设置该Activity的主题,必须得在第一次调用setContentView()前设置,
否则,也是没有效果的(具体原因可见后面分析)。
2、原理分析
Android的所有系统资源定义位置存放在 frameworks\base\core\res\ 路径下,编译时会形成apk文件,即
framework-res.apk,所有应用程序共享。
实际上任何Style/Theme也是一组自定义属性集合,其内置在Android系统资源中,如下所示:
文件路径:frameworks\base\core\res\res\values\attrs.xml
- <!-- The set of attributes that describe a Windows's theme. -->
- <declare-styleable name="Window">
- <!-- 常见的Window属性 -->
-
- <attr name="windowBackground" />
- <attr name="windowFrame" />
- <attr name="windowNoTitle" />
- <attr name="windowFullscreen" />
- <attr name="windowIsFloating" />
- <attr name="windowIsTranslucent" />
- <attr name="windowSoftInputMode" />
-
- <!-- more 更多不常见地Window属性-->
- ...
-
- </declare-styleable>
特殊的是如果某个自定义属性如果没有指名 format属性,那么该属性必须在当前已经定义,即该属性只是一个
别名。
大部分Android属性定义在 name = "Theme"的属性集合下(仅列出Window attrs):
文件路径:frameworks\base\core\res\res\values\attrs.xml
- <!-- These are the standard attributes that make up a complete theme. -->
- <declare-styleable name="Theme">
- <!-- Drawable to use as the overall window background. There are a
- few special considerations you should use when settings this
- drawable:
- -->
- <attr name="windowBackground" format="reference" />
- <!-- Drawable to use as a frame around the window. -->
- <attr name="windowFrame" format="reference" />
- <!-- Flag indicating whether there should be no title on this window. -->
- <attr name="windowNoTitle" format="boolean" />
- <!-- Flag indicating whether this window should fill the entire screen. -->
- <attr name="windowFullscreen" format="boolean" />
- <!-- Flag indicating whether this is a floating window. -->
- <attr name="windowIsFloating" format="boolean" />
- <!-- Flag indicating whether this is a translucent window. -->
- <attr name="windowIsTranslucent" format="boolean" />
- <!-- Flag indicating that this window's background should be the
- user's current wallpaper. -->
- <attr name="windowShowWallpaper" format="boolean" />
- <!-- This Drawable is overlaid over the foreground of the Window's content area, usually
- to place a shadow below the title. -->
- <!-- This Drawable is overlaid over the foreground of the Window's content area, usually
- to place a shadow below the title. -->
- <attr name="windowContentOverlay" format="reference" />
- <!--more -->
- </declare-styleable>
属性定义如上,Android系统中这些属性定义了很多Style/Theme ,常见的有如下 :
- android:theme="Theme"
- android:theme="Theme.Light"
- android:theme="Theme.Light.NoTitleBar"
- android:theme="Theme.Light.NoTitleBar.Fullscreen"
- android:theme="Theme.Black"
名称为"Theme"属性(系统默认的Theme)的定义为(仅copy部分关于Window属性的定义) :
文件位于:frameworks\base\core\res\res\values\themes.xml
- <style name="Theme">
- <!-- Window attributes -->
- <item name="windowBackground">@android:drawable/screen_background_dark</item>
- <item name="windowFrame">@null</item>
- <item name="windowNoTitle">false</item>
- <item name="windowFullscreen">false</item>
- <item name="windowIsFloating">false</item>
- <item name="windowTitleSize">25dip</item>
- <item name="windowTitleBackgroundStyle">@android:style/WindowTitleBackground</item>
- <item name="android:windowAnimationStyle">@android:style/Animation.Activity</item>
- <item name="android:windowSoftInputMode">stateUnspecified|adjustUnspecified</item>
- lt;/style>
该Theme作为一个超元素集,所有其他的Style/Theme则继承了它。例如:我们关于自定义的Theme必须显示从
一个父Theme继承,如下:
- <!--自定义Theme 必须制定parent属性-->
- <style name="CustomTheme" parent="@android:style/Theme" >
- <item name="android:windowNoTitle">true</item>
- <item name="android:windowFrame">@drawable/icon</item>
- <item name="android:windowBackground">?android:windowFrame</item>
- </style>
我们看看Android另外一个Theme.NoTitleBar属性定义,默认继承了
"Theme"集合。
- <!-- Variant of the default (dark) theme with no title bar -->
- <style name="Theme.NoTitleBar">
- <item name="android:windowNoTitle">true</item>
- </style>
其实xml文件中声明的任何元素(包括属性),必须通过代码去获取他们的值,然后进行适当地逻辑运算。那么
系统是在什么地方去解析这些Window属性,并且选择合适地布局文件?
二、Theme主题的解析以及布局文件的选取
如果对setContentView()调用过程不太熟悉的朋友,可以先看看前面一篇博文<从setContentView()谈起>。
今天我们深入到其中generateLayout()方法,该方法地主要作用就是解析这些Window属性,然后选择合适地
布局文件作为我们地Activity或者Window界面地承载布局文件,即DecorView的直接子View。
在进行具体分析之前,Android还提供了另外两种简单API让我们制定界面的风格,如下两个方法:
1、requestFeature() 设定个该界面的风格Feature,例如,FEATURE_NO_TITLE(没有标题) 、
FEATURE_PROGRESS(标题栏带进度条) 。必须在setContentView()前调用,否则会报异常。
FEATURE属性定义在Window.java类
2、getWindow().setFlags(),为当前的WindowManager.LayoutParams添加一些Flag。
Flag标记定义在WindowManager.LayoutParams.java类。
通过这两种方法隐藏状态栏和标题栏的例子为:
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
-
- requestWindowFeature(Window.FEATURE_NO_TITLE);
-
-
- getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
- WindowManager.LayoutParams.FLAG_FULLSCREEN);
- setContentView(R.layout.main);
- }
完整例子,可见于博文<Android隐藏状态栏和标题栏,相当于全屏效果>
源码分析:
这两个方法是在Window.java类实现的,如下:
- public class Window {
-
- public static final int FEATURE_OPTIONS_PANEL = 0;
-
-
- public static final int FEATURE_NO_TITLE = 1;
-
- public static final int FEATURE_PROGRESS = 2;
-
- public static final int FEATURE_LEFT_ICON = 3;
-
- public static final int FEATURE_RIGHT_ICON = 4;
-
- public static final int FEATURE_INDETERMINATE_PROGRESS = 5;
-
- public static final int FEATURE_CONTEXT_MENU = 6;
-
- public static final int FEATURE_CUSTOM_TITLE = 7;
-
-
- protected static final int DEFAULT_FEATURES = (1 << FEATURE_OPTIONS_PANEL) |
- (1 << FEATURE_CONTEXT_MENU);
-
- private int mFeatures = DEFAULT_FEATURES;
-
-
- public boolean requestFeature(int featureId) {
- final int flag = 1<<featureId;
- mFeatures |= flag;
-
- mLocalFeatures |= mContainer != null ? (flag&~mContainer.mFeatures) : flag;
- return (mFeatures&flag) != 0;
- }
-
- public void addFlags(int flags) {
- setFlags(flags, flags);
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
- public void setFlags(int flags, int mask) {
- final WindowManager.LayoutParams attrs = getAttributes();
-
- attrs.flags = (attrs.flags&~mask) | (flags&mask);
- mForcedWindowFlags |= mask;
- if (mCallback != null) {
- mCallback.onWindowAttributesChanged(attrs);
- }
- }
- ...
- }
其实也挺简单的,主要是逻辑运算符的操作。
mFeatures 代表了当前Window的Feature值.
flags 保存在当前WindowManager.LayoutParams.flag属性中。
接下来具体分析generateLayout()方法.
如果当前界面的DecorView对象为空(一般由setContentView()或者addContentView()调用),则会创建一个
DecorView对象以及对应的装载xml布局的mContentParent对象。
Step 1、创建DecorView对象
- private void installDecor() {
- if (mDecor == null) {
- mDecor = generateDecor();
- mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
- }
-
- if (mContentParent == null) {
- mContentParent = generateLayout(mDecor);
- ...
- }
- }
Step 2
、创建
mContentParent
对象
- protected ViewGroup generateLayout(DecorView decor) {
-
- TypedArray a = getWindowStyle();
-
-
-
- mIsFloating = a.getBoolean(com.android.internal.R.styleable.Window_windowIsFloating, false);
- ...
- }
首先获取系统自定义的Style对应的TypeArray对象,然后获取对应的属性值。我们继续分析getWindowStyle()
方法。
2.1、
-
-
-
-
- public final TypedArray getWindowStyle() {
- synchronized (this) {
- if (mWindowStyle == null) {
-
- mWindowStyle = mContext.obtainStyledAttributes(com.android.internal.R.styleable.Window);
- }
- return mWindowStyle;
- }
- }
调用Context类对应地obtainStyledAttributes()方法,参数传递的是 Window对应的自定义属性集合。
2.2、
-
-
-
-
-
-
-
- public final TypedArray obtainStyledAttributes(
- int[] attrs) {
-
- return getTheme().obtainStyledAttributes(attrs);
- }
由于Activity继承至ContextThemeWapprer类,ContextThemeWapprer重写了getTheme()方法。
2.3
- @Override
- public Resources.Theme getTheme() {
- if (mTheme != null) {
- return mTheme;
- }
-
- if (mThemeResource == 0) {
- mThemeResource = com.android.internal.R.style.Theme;
- }
- initializeTheme();
- return mTheme;
- }
首先,判断是否是第一次调用该方法,即是否创建了mTheme对象;
其次,判断是否设置了该Theme所对应的资源ID,如果没有,则选取默认的theme style
即com.android.internal.R.style.Theme 。
最后,初始化对应资源。
-
-
-
-
-
-
-
-
-
-
-
- protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
- theme.applyStyle(resid, true);
- }
-
- private void initializeTheme() {
- final boolean first = mTheme == null;
- if (first) {
- mTheme = getResources().newTheme();
- Resources.Theme theme = mBase.getTheme();
- if (theme != null) {
- mTheme.setTo(theme);
- }
- }
- onApplyThemeResource(mTheme, mThemeResource, first);
- }
如果没有手动设置mThemeResource,则选取系统中为我们提供的默认Theme。当然我们也可以手动设置Theme
Resource ,如开篇所述。
方法一: Activity中调用setTheme()方法,该方法会实现在ContextThemeWrapper.java类中。
- @Override
- public void setTheme(int resid) {
- mThemeResource = resid;
- initializeTheme();
- }
方法二:在AndroidManifest文件中,为Activity节点配置android:theme属性. 当通过startActivity()启动一个
Activity时,会调用setTheme()方法。文件路径:frameworks\base\core\java\android\app\ActivityThread.java
- private final Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
- ...
- Activity activity = null;
- try {
-
- java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
- activity = mInstrumentation.newActivity(
- cl, component.getClassName(), r.intent);
- }
- ...
- try {
- ...
- if (activity != null) {
-
- ContextImpl appContext = new ContextImpl();
- appContext.init(r.packageInfo, r.token, this);
- appContext.setOuterContext(activity);
- CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
- ...
- activity.attach(appContext, this, getInstrumentation(), r.token,
- r.ident, app, r.intent, r.activityInfo, title, r.parent,
- r.embeddedID, r.lastNonConfigurationInstance,
- r.lastNonConfigurationChildInstances, config);
- ...
-
-
- int theme = r.activityInfo.getThemeResource();
- if (theme != 0) {
- activity.setTheme(theme);
- }
- ...
- }
- }
- ...
- return activity;
- }
总结: 如果没有为设置Theme Resource ,则会选取默认的Theme Style,否则选用我们设置的Theme。
因为mTheme对象是相对统一的,只不过每次都通过apply一个新的Style ID,感觉Android 框架会为每个
应用程序的资源形成一个统一的资源库,应用程序定义的所有Style都存在在该资源库中,可以通过通过Style
ID值显示获取对应值集合。 但由于对系统获取资源的过程不了解,目前还不清楚Android中是如何根据资源ID
获取对应的资源甚至一组资源的。但可喜的是,老罗目前正在研究这块,希望能在老罗的文章中找到答案。
具体可见 <
Android资源管理框架(Asset Manager)简要介绍和学习计划
>
另外,Dialog的构造函数也有一定启发性,创建了一个指定Theme 的ContextThemeWapper对象,然后通过它创
建对应的Window对象。 具体过程可以自行研究下。
- public Dialog(Context context, int theme) {
-
- mContext = new ContextThemeWrapper(
- context, theme == 0 ? com.android.internal.R.style.Theme_Dialog : theme);
- mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
-
- Window w = PolicyManager.makeNewWindow(mContext);
- ...
- }
PS : Android 4.0 之后默认的属性为Theme_Holo,呵呵,Holo倒挺有意思的。调用相关函数时,会判断SDK
版本,然后选取相应地Theme。相关函数如下: @ Resources.java
-
- public static int selectDefaultTheme(int curTheme, int targetSdkVersion) {
- return selectSystemTheme(curTheme, targetSdkVersion,
- com.android.internal.R.style.Theme,
- com.android.internal.R.style.Theme_Holo,
- com.android.internal.R.style.Theme_DeviceDefault);
- }
-
- public static int selectSystemTheme(int curTheme, int targetSdkVersion,
- int orig, int holo, int deviceDefault) {
-
- if (curTheme != 0) {
- return curTheme;
- }
-
- if (targetSdkVersion < Build.VERSION_CODES.HONEYCOMB) {
- return orig;
- }
- if (targetSdkVersion < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
- return holo;
- }
- return deviceDefault;
- }
Step 3、通过前面的分析,我们获取了Window属性对应的TypeArray对象,接下来就是获取对应的属性值。
如下代码所示:
-
- protected ViewGroup generateLayout(DecorView decor) {
-
- TypedArray a = getWindowStyle();
-
-
- mIsFloating = a.getBoolean(com.android.internal.R.styleable.Window_windowIsFloating, false);
- int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
- & (~getForcedWindowFlags());
-
-
- if (mIsFloating) {
- setLayout(WRAP_CONTENT, WRAP_CONTENT);
- setFlags(0, flagsToUpdate);
- } else {
- setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
- }
-
- if (a.getBoolean(com.android.internal.R.styleable.Window_windowNoTitle, false)) {
- requestFeature(FEATURE_NO_TITLE);
- }
-
- if (a.getBoolean(com.android.internal.R.styleable.Window_windowFullscreen, false)) {
- setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN&(~getForcedWindowFlags()));
- }
-
- if (a.getBoolean(com.android.internal.R.styleable.Window_windowShowWallpaper, false)) {
- setFlags(FLAG_SHOW_WALLPAPER, FLAG_SHOW_WALLPAPER&(~getForcedWindowFlags()));
- }
- WindowManager.LayoutParams params = getAttributes();
- if (!hasSoftInputMode()) {
- params.softInputMode = a.getInt(
- com.android.internal.R.styleable.Window_windowSoftInputMode,
- params.softInputMode);
- }
-
- if (getContainer() == null) {
- if (mBackgroundDrawable == null) {
- if (mBackgroundResource == 0) {
- mBackgroundResource = a.getResourceId(
- com.android.internal.R.styleable.Window_windowBackground, 0);
- }
- }
- ...
- }
-
- int layoutResource;
- int features = getLocalFeatures();
- if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
-
- if (mIsFloating) {
- layoutResource = com.android.internal.R.layout.dialog_title_icons;
- } else {
- layoutResource = com.android.internal.R.layout.screen_title_icons;
- }
- }
- else if {
- else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
-
-
-
- if (mIsFloating) {
- layoutResource = com.android.internal.R.layout.dialog_title;
- } else {
- layoutResource = com.android.internal.R.layout.screen_title;
- }
-
- } else {
- layoutResource = com.android.internal.R.layout.screen_simple;
- }
- View in = mLayoutInflater.inflate(layoutResource, null);
- decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
- ...
- if (getContainer() == null) {
- Drawable drawable = mBackgroundDrawable;
- if (mBackgroundResource != 0) {
- drawable = getContext().getResources().getDrawable(mBackgroundResource);
- }
- mDecor.setWindowBackground(drawable);
- drawable = null;
-
- if (mFrameResource != 0) {
- drawable = getContext().getResources().getDrawable(mFrameResource);
- }
- mDecor.setWindowFrame(drawable);
- }
- return contentParent;
- }
依次取出对应的属性值,然后根据这些值调用不同的函数,例如:requestFeature(),以及为
WindowMamager.LayoutParams设置Flag标记(这个掩码实现按位操作倒挺麻烦的,部分理解,有知道的朋友
可以给我指点下。)例如:如果是对话框窗口,则不设置FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR
位标记,因为该标记代表对应的是Activity窗口。
1、 FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR 代表的是典型的Activity窗口
2、 FLAG_LAYOUT_IN_SCREEN 代表的是典型的全屏窗口
3、 其他则代表其他对话框窗口。
最后,根据相应的Feature值,加载不同的布局文件。
PS : 前些日子在论坛中看到有网友说取消/隐藏ActionBar,Android 4.0中对一个支持ActionBar的资源文件
定义如下: 文件路径 frameworks\base\core\res\res\layout\screen_action_bar.xml
- <!--
- This is an optimized layout for a screen with the Action Bar enabled.
- -->
-
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:fitsSystemWindows="true">
- <!-- Action Bar 对应的 View文件 -->
- <com.android.internal.widget.ActionBarContainer android:id="@+id/action_bar_container"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- style="?android:attr/actionBarStyle">
- <com.android.internal.widget.ActionBarView
- android:id="@+id/action_bar"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- style="?android:attr/actionBarStyle" />
- <com.android.internal.widget.ActionBarContextView
- android:id="@+id/action_context_bar"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:visibility="gone"
- style="?android:attr/actionModeStyle" />
- </com.android.internal.widget.ActionBarContainer>
- <FrameLayout android:id="@android:id/content"
- android:layout_width="match_parent"
- android:layout_height="0dip"
- android:layout_weight="1"
- android:foregroundGravity="fill_horizontal|top"
- android:foreground="?android:attr/windowContentOverlay" />
- <com.android.internal.widget.ActionBarContainer android:id="@+id/split_action_bar"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- style="?android:attr/actionBarSplitStyle"
- android:visibility="gone"
- android:gravity="center"/>
- </LinearLayout>