1 背景
前段時間群里有伙伴問到了關於Android開發中Theme與Style的問題,當然,這類東西在網上隨便一搜一大把模板,所以關於怎么用的問題我想這里也就不做太多的說明了,我們這里把重點放在理解整個Android中Theme、Style的關系及結構,這樣我們就能游刃有余的面對實際開發中遇到的很多問題了,也就免得在自定義時遇到各種坑,譬如不清楚該繼承哪個parent、不清楚為何背景會有一個黑邊等。
本文主要分兩部分來進行簡單粗略的淺析,首先會圍繞Theme與Style的定義及在App開發中的使用來進行簡單回顧,接着第二部分會介紹Android系統關於Theme與Style的規則及源碼,然后簡單總結下我們開發中如何處理自定義Theme與Style的一些方法。
【工匠若水 http://blog.csdn.net/yanbober 未經允許嚴禁轉載,請尊重作者勞動成果。私信聯系我】
2 應用層使用基礎
對於Android開發者來說Theme主要是針對窗體及Activity級別的,通常改變的是窗體的樣式;而Style主要是針對窗體及Activity中具體成員元素級別的,通常改變的是控件的樣式。簡單粗暴理解就是Theme作用於所有支持屬性的子成員,Style作用於指定的成員。所以關於Theme與Style的基礎概述這里就不做過多贅述了,具體可以移步到Android官方文檔進行查閱,這里我們主要針對比較常用的一些基礎點進行粗略的回顧,具體如下。
2-1 Theme與Style的定義規則
Theme的實質也是Style,所以Theme的定義格式與Style的基本一致,具體格式如下(都定義在res目錄下,自己隨意起名的xml中):
<?xml version="1.0" encoding="utf-8"?> <resources> <style name="CodeFont" parent="@android:style/TextAppearance.Medium"> <item name="android:layout_width">fill_parent</item> <item name="android:layout_height">wrap_content</item> <item name="android:textColor">#00FF00</item> <item name="android:typeface">monospace</item> </style> </resources>
可以看見,一個style在定義時至少需要一個name的屬性用來被使用者識別,其中的item就是各種不同的屬性與指定的屬性值,而style除過有name屬性以外還可以有parent屬性,在這里你先理解為類似Java的繼承重寫關系即可,后面會詳細介紹。
不過這里要特別注意,一般在style中使用parent字段的繼承適用於繼承系統平台現有定義的style,而我們想要繼承自己實現的style一般不會通過parent字段來實現,而是通過指定格式的name字段來實現,如下:
<style name="CodeFont.Red"> <item name="android:textColor">#FF0000</item> </style>
看見name字段了嗎?證明這個style繼承自我們上面自定義的CodeFont style,可見我們自定義的繼承是通過“.”來實現的,在使用時只需要@style/CodeFont.Red即可使用該繼承重寫的style啦,如果你還想繼續在這基礎上繼承,那寫法還是一樣的,具體如下:
<style name="CodeFont.Red.Big"> <item name="android:textSize">30sp</item> </style>
整明白Theme與Style的這個約定了么!就是這么簡單而已,不過要注意這兩種的嚴格區別,別亂用,譬如將系統預定義的通過name來使用時錯誤的。
接着我們來看看style中item屬性是怎么搞來的,這玩意如果我們定義控件的style則可以直接在對應控件或者Window的API文檔中或者R.attr
文檔中找到支持哪些屬性,依次選擇合適的進行使用即可(特別提醒,這個技能很重要,譬如有時候你會說我自定義的Dialog為何背景周邊多一個黑框啥玩意的問題,然后上網一頓復制別人的style,也不明白別人為啥這么寫,其實一個很重要的技巧就是遇到這種問題自己去API查下相關的屬性就搞定了。)。使用系統已存在屬性時切記不要忘記<item name="android:inputType">
前面的android:前綴,還有就是item中存在的屬性不見得對所有View都有效,譬如Theme中需要的以windowXXX開頭的屬性就不適用於View,但是不會報錯,只是View會忽略這些不適合自己的屬性,應用適合自己的屬性。
2-2 Theme與Style的使用
有了上面的知識我們已經能夠定義出Theme與Style了,下來就是怎么將定義的這些樣式應用到UI中啦。將style設置到UI主要分兩類,如下:
- 對於單個控件通過style進行引入(注意:ViewGroup的style不會向下傳遞到子View上,除非用theme方式);
- 對於Activity、Application等窗口級向下應用的通過theme進行引入;
在Android中有許多預定義的style供我們使用,所以在使用主題時我們可以如下使用:
<!-- 使用系統預制style --> <activity android:theme="@android:style/Theme.Dialog"> <!-- 使用系統預制但局部修改的style --> <activity android:theme="@style/CustomTheme"> <color name="custom_theme_color">#b0b0ff</color> <style name="CustomTheme" parent="android:Theme.Light"> <item name="android:windowBackground">@color/custom_theme_color</item> <item name="android:colorBackground">@color/custom_theme_color</item> </style>
2-3 Theme的兼容性處理
在新版本的Android中添加了很多新的Theme,而老版本又不兼容這些Theme,所以很多時候我們可能需要處理一下這種兼容性問題,譬如我們在res/values/styles.xml文件中定義如下Theme:
<style name="LightThemeSelector" parent="android:Theme.Light"> ... </style>
當我們想在Android3.0(API 11)以上使用新的Theme則可以res/values-v11目錄下定義如下Theme:
<style name="LightThemeSelector" parent="android:Theme.Holo.Light"> ... </style>
這樣當我們編譯的APK在不同的設備上運行時就能自己切換選擇適合自己平台的Theme了。
2-4 Android系統預制的Theme與Style選擇
話說Android應用層開發之所以簡單的原因就在於系統已經幫我們實現了很多自由選擇的功能,關於Style與Theme也不例外(應用層開發難就難在知識面很廣),具體使用可以記住如下秘訣:
- 當我們想要知道Theme具體有哪些屬性可以有效使用時,可以查閱API的R.styleable進行選擇。
- 當我們想要知道Style具體有哪些屬性可以有效使用時,可以查閱API的R.attr進行選擇。
- 系統為我們提供了很多實用的Theme與Style,我們還可以通過查閱API的R.style進行選擇(要注意的是這里的文檔查到的不一定全,最好的辦法是去查FW下base的res或者appcompat的res),不過要注意,在API中譬如Theme_NoTitleBar主題樣式在我們xml引用時要替換為@android:style/Theme.NoTitleBar的格式。
2-5 Android應用資源拓展語法
上面提到的都是Theme與Style相關的東西,其實這兩個東西實質都屬於res資源的處理,關於Android的res資源使用規則和不同平台軟硬件系統匹配的策略不屬於本文范圍,不過也很簡單,感興趣的同學可以移步到API Guide的App Resources進行研讀。這里我們主要簡單說下資源的引用語法,因為Theme與Style中也會經常使用到,免得帶來不必要的疑惑。
Android中資源在Java文件中引用的語法定義如下:
[<package_name>.]R.<resource_type>.<resource_name> //注意:當資源在當前APP中則package_name可以省略,當為系統的資源則可換位譬如android.
Android中資源在XML文件中引用的語法定義如下:
@[<package_name>:]<resource_type>/<resource_name> //注意:package_name的規則同上java中,不過在XML中引入不是本包資源時要注意格式,譬如引用系統的資源格式為android:textColor="@android:color/secondary_text_dark"
Android系統預制資源在XML文件中引用的特殊語法定義如下:
//可以引用系統所有資源,public & private @*android:type/name //只能引用系統public的資源 @android:type/name //注意:沒在frameworks/base/core/res/res/values/public.xml(也就是<sdk_path>\platforms\android-X\data\res\values\public.xml)中申明的資源App時不推薦使用的。
Android在XML文件中引用當前主題屬性的語法定義如下:
?[<package_name>:][<resource_type>/]<resource_name> //資源值允許引用當前主題中的屬性的值,這個屬性值只能在style資源和XML中使用,隨着當前主題的切換該值也在變換,該resource_name不需要自己定義,系統會自己在當前主題下尋找,常見的譬如動畫中等。
Android在XML文件中創建或者引用資源語法定義如下:
//在R.java的type內部類中添加一條靜態常量id資源標識符,如果標示符(包括系統資源)已經存在則表示引用該標示符。 @+type/name //在R.java中尋找已經定義的標識符,如果找不到則提示失敗錯誤,一般在xml中定義有先后關系。 @type/name //所以一般推薦直接使用+號避免不必要的意外。
Android在XML文件中xmlns語法定義如下:
//xmlns(XML Namespaces)是XML的命名空間 //通用XML命名空間格式規則 xmlns:namespace-prefix="namespaceURI"
在Android的XML中命名空間規則如下:
xmlns:namespace-prefix=http://schemas.android.com/apk/res/應用程序包路徑
在使用時規則如下:
namespace-prefix:屬性
切記,xmlns的定義必須放在最外層開始的的標記中,譬如我們Activity的xml文件的根布局中的android前綴、tools前綴、自定義View的前綴等。常見的例子如下:
//android即為frameworks/base/core/res/res/values/attrs.xml中的屬性 xmlns:android="http://schemas.android.com/apk/res/android" //開發調試利器,不再過多說明 xmlns:tools="http://schemas.android.com/tools" //Email App中res/values/attrs.xml等自定義屬性 xmlns:settings="http://schemas.android.com/apk/res/com.android.email"
2-6 Android應用Theme、Style使用小結
到此關於Android應用中如何定義Theme、Style及使用和繼承重寫相信大家已經明白了,再出現詭異的現象就可以通過查詢相關API及google結合就能完全理會其中的原因了,而不是停留在能搜到復制;下面一節我們將針對上面的這些使用進行粗略的源碼分析說明。
【工匠若水 http://blog.csdn.net/yanbober 未經允許嚴禁轉載,請尊重作者勞動成果。私信聯系我】
3 源碼結構淺析
有了上面的應用使用基礎,下面的源碼簡單淺析可能存在跳躍性和經驗性,不會像之前博客那樣系統性的從頭到尾進行分析,而是分點點到為止,感興趣的同學可以自行深入研讀。
3-1 追根溯源Theme、Style等根源
在我們App開發中通常我們會在新建工程后的AndroidManifest.xml文件中看見工程默認引用了應用包下自定義的主題@style/AppTheme(用法完全符合上一大節的規則)。該主題在當前應用包的style.xml中定義如下:
<resources> <!-- Base application theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> </style> </resources>
看着木有,它活生生的繼承了Theme.AppCompat.Light.DarkActionBar這個style,這玩意又在framework的support v7包下res的themes.xml文件中,具體如下:
<!-- Platform-independent theme providing an action bar in a dark-themed activity. --> <style name="Theme.AppCompat.Light.DarkActionBar" parent="Base.Theme.AppCompat.Light.DarkActionBar" />
這個繼承關系一直追蹤下去到了該包下的themes_base.xml中的如下代碼:
<style name="Platform.AppCompat.Light" parent="android:Theme.Light"> <item name="android:windowNoTitle">true</item> ...... </style>.
哈哈,原來如此,這里的Theme.Light你應該十分熟悉了吧(這就是以前我們App用的不是Support包,而是默認的時候,theme默認就是這玩意哈),這玩意就在framework的base下的themes.xml中定義着呢(所以通過了android:進行引用,留意細節吧),具體如下:
<!-- Theme for a light background with dark text on top. Set your activity to this theme if you would like such an appearance. As with the default theme, you should try to assume little more than that the background will be a light color. <p>This is designed for API level 10 and lower.</p>--> <style name="Theme.Light"> <item name="isLightTheme">true</item> <item name="windowBackground">@drawable/screen_background_selector_light</item> ...... </style>.
到這里我們就很容易明白啦,Theme.Light的父類原來是Theme哇,也在這個文件中,如下:
<!-- The default theme for apps on API level 10 and lower. This is the theme used for activities that have not explicitly set their own theme. <p>You can count on this being a dark background with light text on top, but should try to make no other assumptions about its appearance. In particular, the text inside of widgets using this theme may be completely different, with the widget container being a light color and the text on top of it a dark color. <p>If you're developing for API level 11 and higher, you should instead use {@link #Theme_Holo} or {@link #Theme_DeviceDefault}.</p> --> <style name="Theme"> ...... <!-- Text styles --> ...... <!-- Button styles --> ...... <!-- List attributes --> ...... <!-- @hide --> ...... <!-- Gallery attributes --> ...... <!-- Window attributes --> ...... <!-- Define these here; ContextThemeWrappers around themes that define them should always clear these values. --> ...... <!-- Dialog attributes --> ...... <!-- AlertDialog attributes --> ...... <!-- Presentation attributes (introduced after API level 10 so does not have a special old-style theme. --> ...... <!-- Toast attributes --> ...... <!-- Panel attributes --> ...... <!-- These three attributes do not seems to be used by the framework. Declared public though --> ...... <!-- Scrollbar attributes --> ...... <!-- Text selection handle attributes --> ...... <!-- Widget styles --> ...... <!-- Preference styles --> ...... <!-- Search widget styles --> ...... <!-- Action bar styles --> ...... <!-- Floating toolbar styles --> ...... <!-- SearchView attributes --> ...... <!-- PreferenceFrameLayout attributes --> ...... <!-- NumberPicker style--> ...... <!-- CalendarView style--> ...... <!-- TimePicker style --> ...... <!-- TimePicker dialog theme --> ...... <!-- DatePicker style --> ...... <!-- DatePicker dialog theme --> ...... <!-- Pointer style --> ...... <!-- Accessibility focused drawable --> ...... <!-- Lighting and shadow properties --> ...... </style>.
看注釋吧,這貨有接近400多個item屬性,這也就是我們Android關於Theme的開山鼻祖了,在我們自定義時其實來這看比去API查還方便呢(其實需要兩個互相配合,一個查,一個看解釋,哈哈),因為它里面定義了關於我們整個應用中文字樣式、按鈕樣式、列表樣式、窗體樣式、對話框樣式等,這些樣式都是默認樣式,它還有很多我們常用的擴展樣式,譬如Theme.Light、Theme.NoTitleBar、Theme.NoTitleBar.Fullscreen等等,反正你要有需求來這里搞就行。當我們繼承使用時只用在前加上android:即可,有些屬性可能是找不到的。同理,我們所謂的style、attr等等也都是這么個框架,大致位置也類似主題Theme的,所以這里不再過多說明,自行腦補即可。
3-2 Theme、Style等res資源客戶化流程
對於純App開發來說這一個知識點可以忽略,因為本小節需要大致了解Android源碼的結構和編譯框架,對於固件等開發來說這個還是比較重要的,記得以前做TV盒子開發時很多系統資源需要替換及添加,也就是說會稍微涉及到修改System UI及FW的res,那時候好坑爹,雖然修改的地方不多,只是換幾個圖標和加幾個資源,但是那時候自己還是蒙圈了一段時間才搞明白,所以說有必要啰嗦幾句。
首先我們先要明白設備里系統目錄下的這些常見jar與apk的來源,如下:
名字 | 解釋 |
---|---|
am.jar | 執行am命令所需的java lib,對應FW的base/cmds/am目錄,具體可以參考下面的Android.mk定義。 |
framework-res.apk | Android系統資源庫集合,對應FW的core/res目錄,具體同理參見Android.mk定義。 |
framework.jar | Android SDK核心代碼,對應FW的base目錄,具體可以參考目錄下的Android.mk的MOUDLE定義。 |
SystemUI.apk | 從Android2.2開始狀態欄和下拉通知欄被分割出一個單獨的SystemUI.apk,一般在system的app或者priv-app下(還有很多其他模塊呢,譬如SettingProvider等,具體可以在設備下看看),對應的源碼在FW的packages下的SystemUI中。 |
Others | 其他的jar比較多,不做一一介紹,不同廠商可能還會不同定制,具體可在廠商設備的system下看看有哪些包,對應回去通過Android.mk文件尋找即可。 |
android.jar | 切記這個特例,這貨是make sdk生成的,多方整合,別以為也可以找到對應目錄,木有的!還有就是這個jar很實用的,很多時候我們想用AS直接調運系統的hide API等,自己編譯一個就能派上用場啦! |
有了上邊這幾個和我們本文相關的核心常識后我們簡單說下怎么修改編譯:
- 修改FW/base/XXX/下面需要修改的代碼;
- 單獨在XXX下mm編譯生成XXX.jar(apk);
- 把編譯的jar(apk)包(在out目錄對應路徑下)push到設備系統system的FW目錄下;
- reboot重啟設備驗證;
不過這里有些坑大家要明白,我們在mm前最好每次都去清除對應out/obj目錄下的中間文件,特別是資源文件更新時,否則容易被坑。還有就是切記添加系統API或者修改@hide的API或者添加資源(包含添加修改public.xml等)后,需要執行make update-api命令來同步base/api下的current.txt的修改,完事再make就行啦,這些編譯文檔都有介紹。
有了上面這些相信大家對於客戶化資源也就有了一些認識啦,想想如果我們需要用到framework.jar的hide資源或者framework-res.apk中新加的資源時又不想用反射和源碼下編譯怎么辦?當然是編譯一個no hide的jar引入我們工程即可哇,要注意我們引入以后一定是Providered的模式,也就是該jar只編譯不打包入該apk,還有就是依賴的先后優先級順序,否則又用的是sdk默認的。還有就是萬能的android.jar也是一種曲線救國的辦法。當然啦,如果是SDK開發則完全可以復制一份自己搞,完事編譯進系統即可,同時提供給App開發。
3-3 Theme、Style加載時機及加載源碼淺析
前面我們介紹了Android的Theme、Style的定義及使用及Theme、Style等res的由來,這里我們來看看這些被使用的Theme的最終是何時、怎樣被加載生效的。我們都知道對於Theme有兩種方式來使用,具體如下(Style等attr在View的使用也比較同類,這里只分析Theme、其他的請在View等地自行分析腦補):
- 在AndroidManifest.xml中
<application>
或者<activity>
節點設置android:theme屬性; - 在Java代碼中調用setTheme()方法設置Activity的Theme(須在setContentView()前設置;
可以看見,這兩種方式我們都比較常用,甚至有時候還會設置Window的一些屬性標記,這些標記方法都在Window類中。我們平時在設置這些Theme時總是有很多疑惑,譬如為毛只能在setContentView()前設置等等,那么下面我們就來庖丁解牛一把。故事在開始之前可能還需要你自行腦補下《Android應用setContentView與LayoutInflater加載解析機制源碼分析》與《Android應用Activity、Dialog、PopWindow、Toast窗口添加機制及源碼分析》兩篇文章,完事再來繼續下面的內容。
關於Activity通過setContentView方法設置View的來源這里就不多說了,參考前面兩篇即可,我們直接跳到PhoneWindow的setContentView方法來看下,如下:
public void setContentView(int layoutResID) { if (mContentParent == null) { installDecor();//每個Activity第一次進來必走 } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } ...... }.
我們接着來看下installDecor()方法,如下:
private void installDecor() { if (mDecor == null) { //僅僅new DecorView(getContext(), -1)而已,也就是FrameLayout mDecor = generateDecor(); ...... } if (mContentParent == null) { //生成我們布局的父布局 mContentParent = generateLayout(mDecor); // Set up decor part of UI to ignore fitsSystemWindows if appropriate. mDecor.makeOptionalFitsSystemWindows(); final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById( R.id.decor_content_parent); ...... } }.
接着我們繼續看看generateLayout(mDecor);這個方法,如下:
protected ViewGroup generateLayout(DecorView decor) { // Apply data from current theme. //獲取當前主題,重點!!!!!!! TypedArray a = getWindowStyle(); ...... //解析一堆主題屬性,譬如下面的是否浮動window(dialog)等 mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false); ...... // Inflate the window decor. //依據屬性獲取不同的布局添加到Decor int layoutResource; int features = getLocalFeatures(); // System.out.println("Features: 0x" + Integer.toHexString(features)); if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { layoutResource = R.layout.screen_swipe_dismiss; } ...... View in = mLayoutInflater.inflate(layoutResource, null); decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); mContentRoot = (ViewGroup) in; ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); ...... return contentParent; }.
一樣嘍,繼續先看下getWindowStyle()方法是神馬鬼,這個方法在其基類Window中,如下:
/** * Return the {@link android.R.styleable#Window} attributes from this * window's theme. */ public final TypedArray getWindowStyle() { synchronized (this) { if (mWindowStyle == null) { mWindowStyle = mContext.obtainStyledAttributes( com.android.internal.R.styleable.Window); } return mWindowStyle; } }.
哎,沒啥好看的,沒有邏輯,就是流程,繼續跟吧,去Context類看看obtainStyledAttributes(com.android.internal.R.styleable.Window)方法吧,如下:
/** * Return the Theme object associated with this Context. */ @ViewDebug.ExportedProperty(deepExport = true) public abstract Resources.Theme getTheme(); /** * Retrieve styled attribute information in this Context's theme. See * {@link android.content.res.Resources.Theme#obtainStyledAttributes(int[])} * for more information. * * @see android.content.res.Resources.Theme#obtainStyledAttributes(int[]) */ public final TypedArray obtainStyledAttributes(@StyleableRes int[] attrs) { //獲取當前Theme對應的TypedArray對象 return getTheme().obtainStyledAttributes(attrs); }.
哎呦我去,憋大招呢,急死人了!可以看見Context的getTheme()方法時一個抽象方法,那他的實現在哪呢,看過《Android應用Context詳解及源碼解析》一文的同學一定知道對於Activity來說他的實現類就是ContextThemeWapprer,那我們趕緊進去看看它到底搞了啥玩意,如下:
@Override public Resources.Theme getTheme() { //一旦設置有Theme則不再走后面邏輯,直接返回以前設置的Theme if (mTheme != null) { return mTheme; } //沒有設置Theme則獲取默認的selectDefaultTheme mThemeResource = Resources.selectDefaultTheme(mThemeResource, getApplicationInfo().targetSdkVersion); //初始化選擇的主題,mTheme就不為null了 initializeTheme(); return mTheme; } @Override public void setTheme(int resid) { //通過外部設置以后mTheme和mThemeResource就不為null了 if (mThemeResource != resid) { mThemeResource = resid; //初始化選擇的主題,mTheme就不為null了 initializeTheme(); } }.
我勒個去,憋大招總算憋出來翔了,ContextThemeWapprer才是重頭戲啊,總算看見了光明了。這里的getTheme方法有一個判斷,沒有設置過Theme(mTheme為空)則通過Resources.selectDefaultTheme獲取默認主題,否則用setTheme設置的主題。那么我們就來先看下假設沒有設置主題,使用默認主題的方法,Resources.selectDefaultTheme如下:
/** * Returns the most appropriate default theme for the specified target SDK version. * <ul> * <li>Below API 11: Gingerbread * <li>APIs 11 thru 14: Holo * <li>APIs 14 thru XX: Device default dark * <li>API XX and above: Device default light with dark action bar * </ul> * * @param curTheme The current theme, or 0 if not specified. * @param targetSdkVersion The target SDK version. * @return A theme resource identifier * @hide */ 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, com.android.internal.R.style.Theme_DeviceDefault_Light_DarkActionBar); } /** @hide */ public static int selectSystemTheme(int curTheme, int targetSdkVersion, int orig, int holo, int dark, 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; } if (targetSdkVersion < Build.VERSION_CODES.CUR_DEVELOPMENT) { return dark; } return deviceDefault; }.
哎呀媽呀,這不就解釋了我們創建不同版本的App時默認主題不一樣的原因么,哈哈,原來如果我們沒有設置主題Theme,系統會依據版本給我們選擇一個默認的主題,也就是上面這段代碼實現了該功能。
我們回過頭繼續回到ContextThemeWapprer的getTheme方法,當我們已經設置了Theme該方法就直接返回了,恰巧設置Theme的方法也在ContextThemeWapprer中。那這個方法啥時候被調運的呢?這一小節一開始我們就說了Activity的Theme設置有兩種方法,主動通過Java調運setTheme()和在AndroidManifest文件配置,AndroidManifest文件配置的Theme又是啥時候調運的呢?有了前面幾篇博客的鋪墊,我想你也一定能找到的,就在ActivityThread的performLaunchActivity()方法中,也就是我們通過startActivity()方法啟動Activity時就調運了Activity的setTheme方法,這個就不多說了,感興趣的自己進去看下就行了,也是流程憋大招,最終調用了activity.setTheme()完成了AndroidManifest文件的Theme獲取。
我們現在把目光回到ContextThemeWapprer的setTheme或者getTheme中調運的initializeTheme()方法中來看看,如下:
protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) { theme.applyStyle(resid, true); } //大招!!!!!!! private void initializeTheme() { //這就解釋了為何setTheme必須在setContentView前調運,不多解釋了,很明白了吧!!!!!!!! final boolean first = mTheme == null; if (first) { mTheme = getResources().newTheme(); Resources.Theme theme = getBaseContext().getTheme(); if (theme != null) { mTheme.setTo(theme); } } onApplyThemeResource(mTheme, mThemeResource, first); }.
這個方法就解釋了為何setTheme必須在setContentView前調運。最終通過onApplyThemeResource調運Resources.Theme的方法進行了設置,如下:
/**
* Place new attribute values into the theme. The style resource * specified by <var>resid</var> will be retrieved from this Theme's * resources, its values placed into the Theme object. * * <p>The semantics of this function depends on the <var>force</var> * argument: If false, only values that are not already defined in * the theme will be copied from the system resource; otherwise, if * any of the style's attributes are already defined in the theme, the * current values in the theme will be overwritten. * * @param resId The resource ID of a style resource from which to * obtain attribute values. * @param force If true, values in the style resource will always be * used in the theme; otherwise, they will only be used * if not already defined in the theme. */ public void applyStyle(int resId, boolean force) { AssetManager.applyThemeStyle(mTheme, resId, force); mThemeResId = resId; mKey.append(resId, force); }.
到此注釋也說明了一些概念,關於AssetManager的應用又是另一個大話題了,這里先不展開討論,我們只用知道到此一個Theme就選擇完成了,還有就是一個Theme的是怎么被選擇出來的,當然對於Dialog等Window的Theme也是一個樣子,這里不多說明,感興趣的自行腦補即可。
到現在為止我們已經找到了Theme是怎么來的了,下來我們需要回到我們這一小節開頭部分的源碼分析,也就是Resources的obtainStyledAttributes()方法,還記得我們最終傳遞了com.android.internal.R.styleable.Window進行獲取該style么。這貨不就是FW中res的attr.xml中自定義的屬性么,如下:
<!-- The set of attributes that describe a Windows's theme. --> <declare-styleable name="Window"> <attr name="windowBackground" /> <attr name="windowBackgroundFallback" /> ...... <attr name="windowLightStatusBar" format="boolean" /> </declare-styleable>.
可以看見,Style、Theme其實就是一組自定義的內置在Android系統資源中的屬性集合,而這里唯一比較特殊的就是這些定義的屬性沒有聲明format字段。其實在Android中如果某個自定義屬性沒有聲明format屬性則意味着該屬性已經定義過,這里只是別名而已。在哪定義的呢?當然還是attr中哇,屬性么,自然只能在這了,找找看發下如下:
<!-- These are the standard attributes that make up a complete theme. --> <declare-styleable name="Theme"> ...... <attr name="windowBackground" format="reference" /> <!-- Drawable to draw selectively within the inset areas when the windowBackground has been set to null. This protects against seeing visual garbage in the surface when the app has not drawn any content into this area. --> <attr name="windowBackgroundFallback" format="reference" /> ...... </declare-styleable>.
原來Theme屬性集才是這貨的正身哇,哈哈。有了這些屬性集在themes.xml中的Theme style就是對上面這些屬性的設置值了,如下樣例:
<style name="Theme"> <item name="isLightTheme">false</item> <item name="colorForeground">@color/bright_foreground_dark</item> <item name="colorForegroundInverse">@color/bright_foreground_dark_inverse</item> ...... </style>.
哈哈,有沒有覺得和App層開發自定義attr和style一樣呢,原來系統也是這么干的,哈哈!
到此我們已經知道Theme是怎么來的,其中的attr是怎么定義及設置的,下面我們就回到PhoneWindow的generateLayout(DecorView decor) 方法來看下萬事俱備以后的應用大雜燴即可,如下:
protected ViewGroup generateLayout(DecorView decor) { //上面已經分析過了,獲取Theme及Theme中Style定義的attr的屬性,這里獲取Window的屬性 TypedArray a = getWindowStyle(); //獲取是否floating的窗口,也就是是否Dialog mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false); ...... //各種屬性獲取及feature、flag設置 if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) { requestFeature(FEATURE_NO_TITLE); } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) { // Don't allow an action bar if there is no title. requestFeature(FEATURE_ACTION_BAR); } ...... if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) { setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags())); } // Inflate the window decor. //依據Theme及feature、flag獲取一個匹配的layoutResourceId資源布局 int layoutResource; int features = getLocalFeatures(); // System.out.println("Features: 0x" + Integer.toHexString(features)); if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { layoutResource = R.layout.screen_swipe_dismiss; } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) { if (mIsFloating) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogTitleIconsDecorLayout, res, true); layoutResource = res.resourceId; } else { layoutResource = R.layout.screen_title_icons; } // XXX Remove this once action bar supports these features. removeFeature(FEATURE_ACTION_BAR); // System.out.println("Title Icons!"); } ...... //將獲取到的資源id布局文件inflate成View添加到DectorView中,同時將mContentRoot賦值為當前resourceId View View in = mLayoutInflater.inflate(layoutResource, null); decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); mContentRoot = (ViewGroup) in; //找到contentParent返回使用,至此關於Window的屬性基本應用完畢 ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); ...... return contentParent; }.
哈哈,整明白了吧,到此就是整個Android關於Theme、Style的核心由來、流程、實現、使用的過程,相信有了上面這些知識后在使用Theme、Style時你該不會繼續蒙圈了吧,加油。