在編寫Android佈局時總會遇到這樣或者那樣的痛點,好比:html
invisible
或gone
,仍是多多少少會佔用內存的。要解決這些痛點,咱們能夠請Android佈局優化三劍客出碼,它們分別是include
、merge
和ViewStub
三個標籤,如今咱們就來認識認識它們吧。在此以前,咱們先來看看咱們本次項目的界面效果:java
界面不復雜,咱們來逐個實現吧。android
include
的中文意思是「包含」、「包括」,當你在一個主頁面裏使用include
標籤時,就表示當前的主佈局包含標籤中的佈局,這樣一來,就能很好地起到複用佈局的效果了。在那些經常使用的佈局好比標題欄和分割線等上面用上它能夠極大地減小代碼量的。它有兩個主要的屬性:git
layout
:必填屬性,爲你須要插入當前主佈局的佈局名稱,經過R.layout.xx的方式引用;id
:當你想給經過include添加進來的佈局設置一個id的時候就可使用這個屬性,它能夠重寫插入主佈局的佈局id。下面咱們就來實戰一番。github
咱們先建立一個ViewOptimizationActivity
,而後再建立一個layout_include.xml佈局文件,它的內容很是簡單,就一個TextView:segmentfault
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:gravity="center_vertical" android:textSize="14sp" android:background="@android:color/holo_red_light" android:layout_height="40dp"> </TextView>
如今咱們就用include
標籤,將其添加到ViewOptimizationActivity
的佈局中:app
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".ViewOptimizationActivity"> <!--include標籤的使用--> <TextView android:textSize="18sp" android:text="一、include標籤的使用" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <include android:id="@+id/tv_include1" layout="@layout/layout_include"/> </LinearLayout>
沒錯,include
的使用就是這麼簡單,只需指明要包含的佈局id就行。除此以外,咱們還給這個include
標籤設置了一個id,爲了驗證它就是layout_include.xml的根佈局TextView的id,咱們在ViewOptimizationActivity
中初始化TextView,並給它設置文字:ide
TextView tvInclude1 = findViewById(R.id.tv_include1); tvInclude1.setText("1.1 常規下的include佈局");
運行以後能夠能夠看到以下佈局:佈局
說明咱們設置的layout和id都是成功的。不過你可能會對id這個屬性有疑問:id我能夠直接在TextView中設置啊,爲何重寫它呢?別忘了咱們的目的是複用,當你在一個主佈局中使用include
標籤添加兩個以上的相同佈局時,id相同就會衝突了,因此重寫它可讓咱們更好地調用它和它裏面的控件。還有一種狀況,假如你的主佈局是RelateLayout
,這時爲了設置相對位置,你也須要給它們設置不一樣的id。性能
除了id以外,咱們還能夠重寫寬高、邊距和可見性(visibility
)這些佈局屬性。可是必定要注意,單單重寫android:layout_height
或者android:layout_width
是不行,必須兩個同時重寫才起做用。包括邊距也是這樣,若是咱們想給一個include進來的佈局添加右邊距的話的完整寫法是這樣的:
<include android:layout_width="match_parent" android:layout_height="40dp" android:layout_marginEnd="40dp" android:id="@+id/tv_include2" layout="@layout/layout_include"/>
初始化後設置一段文字就能夠看到以下的效果了:
能夠看到,1.2顯然比1.1多了一個右邊距。
在1.1中咱們知道了id屬性能夠重寫include
佈局的根佈局id,但對於根佈局裏面的佈局和控件是無能爲力的,若是這時一個佈局在主佈局中include了屢次,那怎麼區別裏面的控件呢?
咱們先建立一個layout_include2.xml的佈局,它的根佈局是FrameLayout
,裏面有一個TextView
,它的id是tv_same:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:background="@android:color/holo_orange_light" android:layout_height="wrap_content"> <TextView android:gravity="center_vertical" android:id="@+id/tv_same" android:layout_width="match_parent" android:layout_height="50dp" /> </FrameLayout>
在主佈局中添加進去:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".ViewOptimizationActivity"> <!--include標籤的使用--> …… <include layout="@layout/layout_include2"/> <include android:id="@+id/view_same" layout="@layout/layout_include2"/> </LinearLayout>
爲了區分,這裏給第二個layout_include2設置了id。也許你已經反應過來了,沒錯,咱們就是要建立根佈局的對象,而後再去初始化裏面的控件:
TextView tvSame = findViewById(R.id.tv_same); tvSame.setText("1.3 這裏的TextView的ID是tv_same"); FrameLayout viewSame = findViewById(R.id.view_same); TextView tvSame2 = viewSame.findViewById(R.id.tv_same); tvSame2.setText("1.3 這裏的TextView的ID也是tv_same");
可見雖然控件的id雖然相同,可是使用起來是沒有衝突的。
include
標籤雖然解決了佈局重用的問題,卻也帶來了另一個問題:佈局嵌套。由於把須要重用的佈局放到一個子佈局以後就必須加一個根佈局,若是你的主佈局的根佈局和你須要include的根佈局都是同樣的(好比都是LinearLayout
),那麼就至關於在中間多加了一層多餘的佈局了。那麼有沒有辦法能夠在使用include
時不增長佈局層級呢?答案固然是有的,那就是使用merge
標籤。
使用merge
標籤要注意一點:必須是一個佈局文件中的根節點,看起來跟其餘佈局沒什麼區別,但它的特別之處在於頁面加載時它的不會繪製的。打個比方,它就像是佈局或者控件的搬運工,把「貨物」搬到主佈局以後就會功成身退,不會佔用任何空間,所以也就不會增長佈局層級了。這正如它的名字同樣,只起「合併」做用。
咱們來驗證一下,首先建立一個layout_merge.xml,在根節點使用merge
標籤:
<?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/tv_merge1" android:text="我是merge中的TextView1" android:background="@android:color/holo_green_light" android:gravity="center" android:layout_width="wrap_content" android:layout_height="40dp" /> <TextView android:layout_toEndOf="@+id/tv_merge1" android:id="@+id/tv_merge2" android:text="我是merge中的TextView2" android:background="@android:color/holo_blue_light" android:gravity="center" android:layout_width="match_parent" android:layout_height="40dp" /> </merge>
這裏我使用了一些相對佈局的屬性,緣由後面你就知道了。咱們接着在ViewOptimizationActivity
的佈局添加RelativeLayout
,而後使用include
標籤將layout_merge.xml添加進去:
<RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <include android:id="@+id/view_merge" layout="@layout/layout_merge"/> </RelativeLayout>
運行出來的效果圖:
在layout_merge.xml中,咱們使用相對佈局的屬性android:layout_toEndOf
將藍色TextView設置到了綠色TextView的右邊,而layout_merge.xml的父佈局是RelativeLayout
,因此這個屬性是起了做用了,merge
標籤不會影響裏面的控件,也不會增長佈局層級。
若是你還不放心,能夠用Android Studio來檢查。我用的Android Studio是3.1版本的,能夠經過Layout Inspector查看佈局層級,不過記得要先在真機或者模擬器上把項目跑起來。依次點擊Tools-Layout Inspector,而後選擇你要查看的Activity,就能夠看到以下的層級圖:
能夠看到RelativeLayout
下面直接就是兩個TextView了, merge
標籤並無增長佈局層級。從這裏也能夠看出merge
的侷限性,即你須要明確將merge
裏面的佈局和控件include
到什麼類型的佈局中,才能提早設置好merge
裏面的佈局和控件的位置。
在學習include
標籤時咱們知道,它的android:id
屬性能夠重寫被include的根佈局id,但若是根節點是merge
呢?前面說了merge
並不會做爲一個佈局繪製出來,因此這裏給它設置id是不起做用的。咱們能夠在它的父佈局RelativeLayout
中再加一個TextView,使用android:layout_below
屬性把設置到layout_merge下面:
<RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <include android:id="@+id/view_merge" layout="@layout/layout_merge"/> <TextView android:text="我不是merge中的佈局" android:layout_below="@+id/view_merge" android:background="@android:color/holo_purple" android:gravity="center" android:layout_width="match_parent" android:layout_height="40dp"/> </RelativeLayout>
運行以後你會發現新加的TextView會把merge佈局蓋住,沒有像預期那樣在其下方。若是把android:layout_below
中的id改成layout_merge.xml中任一TextView的id(好比tv_merge1),運行以後就能夠看到以下效果:
這也符合2.2中的狀況,即父佈局RelativeLayout
下級佈局就是include進去的TextView了。
你必定遇到這樣的狀況:頁面中有些佈局在初始化時不必顯示,可是又不得不事先在佈局文件中寫好,雖然設置成了invisible
或gone
,可是在初始化時仍是會加載,這無疑會影響頁面加載速度。針對這一狀況,Android爲咱們提供了一個利器————ViewStub
。這是一個不可見的,大小爲0的視圖,具備懶加載的功能,它存在於視圖層級中,但只會在setVisibility()
和inflate()
方法調用只會纔會填充視圖,因此不會影響初始化加載速度。它有如下三個重要屬性:
android:layout
:ViewStub須要填充的視圖名稱,爲「R.layout.xx」的形式;android:inflateId
:重寫被填充的視圖的父佈局id。與include
標籤不一樣,ViewStub
的android:id
屬性是設置ViewStub
自己id的,而不是重寫佈局id,這一點可不要搞錯了。另外,ViewStub
還提供了OnInflateListener
接口,用於監聽佈局是否已經加載了。
咱們先建立一個layout_view_stub.xml,裏面放置一個Switch
開關:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:background="@android:color/holo_blue_dark" android:layout_height="100dp"> <Switch android:id="@+id/sw" android:layout_gravity="center" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </FrameLayout>
而後在Activity的佈局中修改以下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".ViewOptimizationActivity"> <!--ViewStub標籤的使用--> <TextView android:textSize="18sp" android:text="三、ViewStub標籤的使用" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ViewStub android:id="@+id/view_stub" android:inflatedId="@+id/view_inflate" android:layout="@layout/layout_view_stub" android:layout_width="match_parent" android:layout_height="100dp" /> <LinearLayout android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content"> <Button android:text="顯示" android:id="@+id/btn_show" android:layout_weight="1" android:layout_width="0dp" android:layout_height="wrap_content" /> <Button android:text="隱藏" android:id="@+id/btn_hide" android:layout_weight="1" android:layout_width="0dp" android:layout_height="wrap_content" /> <Button android:text="操做父佈局控件" android:id="@+id/btn_control" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> </LinearLayout>
在ViewOptimizationActivity
中監聽ViewStub
的填充事件:
viewStub.setOnInflateListener(new ViewStub.OnInflateListener() { @Override public void onInflate(ViewStub viewStub, View view) { Toast.makeText(ViewOptimizationActivity.this, "ViewStub加載了", Toast.LENGTH_SHORT).show(); } });
而後經過按鈕事件來填充和顯示layout_view_stub:
@Override public void onClick(View view) { switch (view.getId()) { case R.id.btn_show: viewStub.inflate(); break; case R.id.btn_hide: viewStub.setVisibility(View.GONE); break; default: break; } }
運行以後,點擊「顯示」按鈕,layout_view_stub顯示了,並彈出"ViewStub加載了"的Toast;點擊「隱藏」按鈕,佈局又隱藏掉了,可是再點擊一下「顯示」按鈕,頁面竟然卻閃退了,查看日誌,發現拋出了一個異常:
java.lang.IllegalStateException: ViewStub must have a non-null ViewGroup viewParent
咱們打開ViewStub
的源碼,看看是哪裏拋出這個異常的。很快咱們就能夠定位到是在inflate()
方法中
public View inflate() { final ViewParent viewParent = getParent(); if (viewParent != null && viewParent instanceof ViewGroup) { if (mLayoutResource != 0) { final ViewGroup parent = (ViewGroup) viewParent; final View view = inflateViewNoAdd(parent); replaceSelfWithView(view, parent); mInflatedViewRef = new WeakReference<>(view); if (mInflateListener != null) { mInflateListener.onInflate(this, view); } return view; } else { throw new IllegalArgumentException("ViewStub must have a valid layoutResource"); } } else { throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent"); } }
注意到if
語句中有一個replaceSelfWithView()
方法,聽這名字就讓人有一種不祥的預感了,點進去一看:
private void replaceSelfWithView(View view, ViewGroup parent) { final int index = parent.indexOfChild(this); parent.removeViewInLayout(this); final ViewGroup.LayoutParams layoutParams = getLayoutParams(); if (layoutParams != null) { parent.addView(view, index, layoutParams); } else { parent.addView(view, index); } }
果真,ViewStub
在這裏調用了removeViewInLayout()
方法把本身從佈局移除了。到這裏咱們就明白了,ViewStub
在填充佈局成功以後就會自我銷燬,再次調用inflate()
方法就會拋出IllegalStateException異常了。此時若是想要再次顯示佈局,能夠調用setVisibility()
方法。
爲了不inflate()
方法屢次調用,咱們能夠採用以下三種方式:
咱們能夠捕獲異常,同時調用setVisibility()
方法顯示佈局。
try { viewStub.inflate(); } catch (IllegalStateException e) { Log.e("Tag",e.toString()); view.setVisibility(View.VISIBLE); }
聲明一個布爾值變量isViewStubShow
,默認值爲false,佈局填充成功以後,在監聽事件onInflate
方法中將其置爲true。
if (isViewStubShow){ viewStub.setVisibility(View.VISIBLE); }else { viewStub.inflate(); }
我先來看看ViewStub
中的setVisibility()
源碼:
public void setVisibility(int visibility) { if (mInflatedViewRef != null) { View view = mInflatedViewRef.get(); if (view != null) { view.setVisibility(visibility); } else { throw new IllegalStateException("setVisibility called on un-referenced view"); } } else { super.setVisibility(visibility); if (visibility == VISIBLE || visibility == INVISIBLE) { inflate(); } } }
能夠看到,在inflate()
初始化mInflatedViewRef
以前,若是設置visibility
爲VISIBLE
的話是會調用inflate()
方法的,在mInflatedViewRef
不爲null以後就不會再去調用inflate()
了。
在顯示ViewStub中的佈局時,你可能會採起以下的寫法:
if (viewStub.getVisibility() == View.GONE){ viewStub.setVisibility(View.VISIBLE); }else { viewStub.setVisibility(View.GONE); }
恭喜你,踩到一個大坑了。這樣寫你會發現點擊「顯示」按鈕後ViewStub
裏面的佈局不會再顯示出來,也就是說if語句裏面的代碼沒有執行。若是你將viewStub.getVisibility()
的值打印出來,就會看到它始終爲0,這偏偏是View.VISIBLE
的值。奇怪,咱們明明寫了viewStub.setVisibility(View.GONE)
,layout_view_stub也隱藏了,爲何ViewStub
的狀態仍是可見呢?
從新回到3.1.3,看看ViewStub
中的setVisibility()
源碼,首先判斷弱引用對象mInflatedViewRef
是否爲空,不爲空則取出存放進去的對象,也就是咱們ViewStub
中的View,而後調用了view的setVisibility()
方法,mInflatedViewRef
爲空時,則判斷visibility
爲VISIBLE
或INVISIBLE
時調用inflate()
方法填充佈局,若是爲GONE
的話則不予處理。這樣一來,在mInflatedViewRef
不爲空,也就是已經填充了佈局的狀況下,ViewStub
中的setVisibility()
方法其實是在設置內部視圖的可見性,而不是ViewStub
自己。這樣的設計其實也符合ViewStub
的特性,即填充佈局以後就自我銷燬了,給其設置可見性是沒有意義的。
仔細比較一下,其實ViewStub
就像是一個懶惰的include
,咱們須要它加載時才加載。要操做佈局裏面的控件也跟include
同樣,你能夠先初始化ViewStub
中的佈局中再初始化控件:
//一、初始化被inflate的佈局後再初始化其中的控件, FrameLayout frameLayout = findViewById(R.id.view_inflate);//android:inflatedId設置的id Switch sw = frameLayout.findViewById(R.id.sw); sw.toggle();
若是主佈局中控件的id沒有衝突,能夠直接初始化控件使用:
//二、直接初始化控件 Switch sw = findViewById(R.id.sw); sw.toggle();
好了,關於ViewStub
的知識就講這麼多了。
本來覺得知識點不難,應該能夠寫得快一點的,沒想到仍是斷斷續續寫了四五天,寫得本身都以爲有點累了。但願仍是能對你們有點幫助,不足之處還望指正。下面使用思惟導圖總計一下,並給出GitHub上的源碼吧。