編輯:關於Android編程
寫在前面:
幾個月之前在做項目的布局優化時,使用 Hierarchy Viewer 查看項目的層級結構,然後發現頂層的布局並不是在XML中我寫的根布局,而是嵌套了多層 Layout ,簡單查閱了一些資料之後明白這是系統為我們加上的。把這個知識點寫在了印象筆記中的 TODO list(裡面還有好多知識想研究,一直在拖延T.T),擱置了好久最近重新拿出來好好研究了一下,爭取做到溫故知新,融會貫通嘛。
也許有的同學沒看過 Hierarchy Viewer 下項目的界面布局,沒關系,我現在帶大家了解下。
新建一個 module ,打開 sdk tool 文件夾下的 Hierarchy Viewer ,布局結構展示如下:
先別著急找放大鏡,想想我們新建項目的默認布局,按理說根布局應該是 RelativeLayout ,並且子 View 是一個 TextView 寫著 “Hello World”才對啊~ 多出來的這些布局層級是什麼?
既然陌生又看不懂,那就先從我們熟悉的入手,找一下我們自己寫的布局:
原來 RelativeLayout 和它的子 View TextView 在這裡,看一下左下角的位置標識,紅框部分指明 RelativeLayout 是 Toolbar 以下的部分。
再想想,我們是通過什麼方法將這個布局填充到 Activity 上的呢?
沒錯是 setContentView
那就在 setContentView 中尋找蛛絲馬跡吧
因為在 Android Studio 中 MainActivity 默認繼承於v7包下的 AppCompatActivity ,目的是為了提供控件的向下兼容或者新控件,AppCompatActivity 也是層層繼承於 Activity ,所以我們直接去看 Activity 的 setContentView
/** * Set the activity content from a layout resource. The resource will be * inflated, adding all top-level views to the activity. * * @param layoutResID Resource ID to be inflated. * * @see #setContentView(android.view.View) * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) */ public void setContentView(int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }
getWindow 拿到了 Activity 的成員變量 mWindow ,進而調用了 setContentView 方法,mWindow 是 Window 類,繼續跟進,看看 Window類 是什麼
注釋中的描述翻譯過來就是,Window 是 視覺和行為表現的頂層抽象基類,它的實例會當作頂層視圖添加進 WindowManager , 它有一個唯一的實現類是 PhoneWindow。
本文我們不會去剖析 WindowManager 有哪些作用和行為,我默默地把它加入了我的 TODO list 中,拖延到什麼時候就不一定了哈T.T。
為了防止你忘了我們在做什麼和我們即將做什麼,先來一個中場回顧:
首先我們查看布局時發現有很多“超出我們預料和理解范疇”的布局出現,跟進 setContentView 方法,發現 Acitvity 中是 Window 調用了 setContentView ,而抽象基類 Window 有一個唯一的實現類 PhoneWindow。不多說,來看看實現類 PhoneWindow 中的 setContentView 方法。
@Override public void setContentView(int layoutResID) { // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window // decor, when theme attributes and the like are crystalized. Do not check the feature // before this happens. if (mContentParent == null) { //初始化 DectorView 和 mContentParent installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext()); transitionTo(newScene); } else { //首次 setContentView 走到這裡 mLayoutInflater.inflate(layoutResID, mContentParent); } final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } }
當我們沒有調用 setContentView 時,mContentParent (是ViewGroup) 是 null ,所以有兩行代碼值得我們關注 installDecor() 和 mLayoutInflater.inflate(layoutResID, mContentParent)
首先 mContentParent 作為第二個參數傳入了 inflate 方法中, 也就是說 我的布局中的 RelativeLayout 被層層解析之後的 View 視圖樹 作為了 mContentParent 的子 View 插入。
現在不知道 mContentParent 是什麼沒關系,繼續跟進 installDecor() 方法。
(隨著API level的升高,源碼發生了很多有關 Feature 、 style 和 Wiget 的細微變化,還是蠻有意思的)
(這裡我還想說一句,相信在 Android 設計之初 PhoneWindow 這個類就存在了,顯然現在的這個命名有些問題,畢竟目前的設備不僅僅是 phone 了,也許改成 DeviceWindow 會比較合適)
private void installDecor() { if (mDecor == null) { // new 一個 DecorView mDecor = generateDecor(); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); } if (mContentParent == null) { //初始化 mContentParent mContentParent = generateLayout(mDecor); // Set up decor part of UI to ignore fitsSystemWindows if appropriate. mDecor.makeOptionalFitsSystemWindows(); // 找到一個帶ActionBar屬性的布局容器 decorContentParent final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById( R.id.decor_content_parent); if (decorContentParent != null) { mDecorContentParent = decorContentParent; mDecorContentParent.setWindowCallback(getCallback()); //配置UI設置 mDecorContentParent.setUiOptions(mUiOptions); } } else { if (mContentParent instanceof FrameLayout) { ((FrameLayout)mContentParent).setForeground(null); } } }
省略了與分析無關的代碼,其中很多是對 Feature 和 style 屬性的一些判斷和設置,首先 installDecor() 方法從字面意思看,很有可能是初始化加載 DecorView 的,首先看看 PhoneWindow 中兩個成員變量 mDecor 和 mContentParent 分別是什麼:
描述的信息可以概括為 mDector 是 窗體的頂級視圖,mContentParent 是放置窗體內容的容器,也就是我們 setContentView 時,所加入的 View 視圖樹。
當二者為 null 時,有兩行代碼值得關注,分別為 mDecor = generateDecor() 和 mContentParent = generateLayout(mDecor)
不過在此之前,先來看看這行尋找decorContentParent布局的代碼
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById( R.id.decor_content_parent);
decor_content_parent 看起來很眼熟的樣子,點擊它進入布局來看看:
為什麼說 decor_content_parent 眼熟呢?打開布局查看器來看看
在 Hierarchy Viewer 中可以看到 ActionBarOverlayLayout 的布局文件的 id 正是 decor_content_parent 不光如此 布局文件中的每個 View 節點的名稱和 id 都與 Hierarchy Viewer<喎?/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPiDK08281tC1xNK70ru21NOmoaPU2b+0xuTW0LXEIEZyYW1lTGF5b3V0ILXEIGlkIM6qIGNvbnRlbnQgo6wgztLDx9fUyLu2+Mi7tcSywrLiy/y+zcrHztLDx7j5sry+1iBSZWxhdGl2ZUxheW91dCC1xDxzdHJvbmc+uLiyvL7WPC9zdHJvbmc+o6zQxMDv0rvPwtPQwcu116OsvMzQ+NHQvr9+PC9wPg0KPHA+uPq9+CBnZW5lcmF0ZURlY29yKCkgt723qKO6PC9wPg0KPHByZSBjbGFzcz0="brush:java;">
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
這個沒什麼可多說的,就是為我們的窗體 new 了 一個 DecorView 。 再來看 generateLayout(mDecor) 這個方法的代碼有300多行,剔除了很多無關代碼,我們分模塊來看: 首先 layoutResource 是系統的xml布局文件的id,裡面有我們設置窗體的 features 和 style 屬性,然後通過decor.addView 添加進 mDector 視圖。這裡也是我們要在 setContentView() 之前執行requestWindowFeature()才可以的原因 關鍵點來了, ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); 這個 id 是不是非常眼熟,與我們上文的猜測不謀而合,這就是我們一直在尋找的作為 activity_main 的父布局的 FrameLayout 我們在布局文件查看器中再找一下: return contentParent 這一步就返回了我們的成員變量 mContentParent 到現在為止其實整個知識點主干的邏輯已經走完了,為大家花了一張簡單的思維導圖 並不復雜,線性邏輯調用還是蠻清晰的。 不過相信你也許會問,上文你僅僅提到了兩個布局呀,一個頂層的 DecorView 和 我們布局文件的父布局 FrameLayout ,而查看布局層級時,為什麼有這麼多其他這麼多額外的布局呢? 因為隨著 Android API level 的不斷變化,組件也在隨之增多,比如ActionBar Toolbar等等,這些組件相關的布局是否加載與你的 feature 設置設備的特性相關聯,而且版本不同,布局文件的層級結構也在不斷變化著豐富著,我這個是 API22 的源碼,我做了一些對比,有許多代碼細節是不一樣的,比如在這裡的 Feature 就新增了 Toolbar ,但是大體上的邏輯框架肯定不會變 寫在後面:
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
// 獲得窗體的 style 樣式
TypedArray a = getWindowStyle();
// 省略大量無關代碼
// Inflate the window decor.
int layoutResource;
int features = getLocalFeatures();
//填充帶有 style 和 feature 屬性的 layoutResource (是一個layout id)
View in = mLayoutInflater.inflate(layoutResource, null);
// 插入的頂層布局 DecorView 中
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
// 找到我們XML文件的父布局 contentParent
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
// 省略無關代碼
mDecor.finishChanging();
// 返回 contentParent 並賦值給成員變量 mContentParent
return contentParent;
}
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);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
// Remaining setup -- of background and title -- that only applies
// to top-level windows.
mDecor.finishChanging();
return contentParent;
通過 findViewById 找到系統修飾布局文件中 id 為:
比如我們目前的 MainActivity 的視圖主要有兩大分支,一條設置 Toolbar 的相關配置,一條就是我們的 RelativeLayout 了。
寫這篇博客的原因一是我自己要研究梳理總結這個知識點,二是想讓大家明白,Android版本之間的迭代很快,一年前的博客闡述的觀點到今天可能就再不適用了,但是 PhoneWindow 管理布局視圖的這套邏輯框架,卻一直沒怎麼改變。通過閱讀源碼,可以學習 Google 工程師們良好的代碼風格,汲取他們搭建框架的思想,讓我們自己寫的代碼也能如此健壯。
背景:之前有過兩篇寫activity的博客 android之activity的生命周期詳解:詳細介紹了activity的整個生命周期、各狀態間的轉換和返回桌
先把來源貼上http://zrgiu.com/blog/2011/01/making-your-android-app-look-better/http://www.di
在上一篇文章裡,我總結了一下自定義控件需要了解的基礎知識:View的繪制流程——《自定義控件知識儲備-View的繪制流程》。其中,在View的測量
1.修改PagerTabStrip中的背景顏色我們在布局中直接設置background屬性即可: 2.修改指示條的顏色我們可以在java代碼中