轉:Android佈局優化三劍客

在編寫Android佈局時總會遇到這樣或者那樣的痛點,好比:java

  1. 有些佈局的在不少頁面都用到了,並且樣式都同樣,每次用到都要複製粘貼一大段,有沒有辦法能夠複用呢?
  2. 解決了1中的問題以後,發現複用的佈局外面總要額外套上一層佈局,要知道佈局嵌套是會影響性能的吶;
  3. 有些佈局只有用到時纔會顯示,可是必須提早寫好,雖然設置了爲invisiblegone,仍是多多少少會佔用內存的。

要解決這些痛點,咱們能夠請Android佈局優化三劍客出碼,它們分別是includemergeViewStub三個標籤,如今咱們就來認識認識它們吧。在此以前,咱們先來看看咱們本次項目的界面效果:android

效果徹底版

界面不復雜,咱們來逐個實現吧。segmentfault

一、include

include的中文意思是「包含」、「包括」,當你在一個主頁面裏使用include標籤時,就表示當前的主佈局包含標籤中的佈局,這樣一來,就能很好地起到複用佈局的效果了。在那些經常使用的佈局好比標題欄和分割線等上面用上它能夠極大地減小代碼量的。它有兩個主要的屬性:bash

  1. layout:必填屬性,爲你須要插入當前主佈局的佈局名稱,經過R.layout.xx的方式引用;
  2. id:當你想給經過include添加進來的佈局設置一個id的時候就可使用這個屬性,它能夠重寫插入主佈局的佈局id。

下面咱們就來實戰一番。app

1.1 常規使用

咱們先建立一個ViewOptimizationActivity,而後再建立一個layout_include.xml佈局文件,它的內容很是簡單,就一個TextView:ide

<?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的佈局中:佈局

<?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,並給它設置文字:性能

TextView tvInclude1 = findViewById(R.id.tv_include1);
        tvInclude1.setText("1.1 常規下的include佈局");複製代碼

運行以後能夠能夠看到以下佈局:學習

include常規使用

說明咱們設置的layout和id都是成功的。不過你可能會對id這個屬性有疑問:id我能夠直接在TextView中設置啊,爲何重寫它呢?別忘了咱們的目的是複用,當你在一個主佈局中使用include標籤添加兩個以上的相同佈局時,id相同就會衝突了,因此重寫它可讓咱們更好地調用它和它裏面的控件。還有一種狀況,假如你的主佈局是RelateLayout,這時爲了設置相對位置,你也須要給它們設置不一樣的id。優化

1.2 重寫根佈局的佈局屬性

除了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"/>複製代碼

初始化後設置一段文字就能夠看到以下的效果了:

設置了右邊距的include佈局

能夠看到,1.2顯然比1.1多了一個右邊距。

1.3 控件ID相同時的處理

在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");複製代碼

運行以後能夠看到這樣的效果:
設置了右邊距的include佈局

可見雖然控件的id雖然相同,可是使用起來是沒有衝突的。

二、merge

include標籤雖然解決了佈局重用的問題,卻也帶來了另一個問題:佈局嵌套。由於把須要重用的佈局放到一個子佈局以後就必須加一個根佈局,若是你的主佈局的根佈局和你須要include的根佈局都是同樣的(好比都是LinearLayout),那麼就至關於在中間多加了一層多餘的佈局了。那麼有沒有辦法能夠在使用include時不增長佈局層級呢?答案固然是有的,那就是使用merge標籤。

使用merge標籤要注意一點:必須是一個佈局文件中的根節點,看起來跟其餘佈局沒什麼區別,但它的特別之處在於頁面加載時它的不會繪製的。打個比方,它就像是佈局或者控件的搬運工,把「貨物」搬到主佈局以後就會功成身退,不會佔用任何空間,所以也就不會增長佈局層級了。這正如它的名字同樣,只起「合併」做用。

2.1 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>複製代碼

運行出來的效果圖:

merge常規使用

2.2 merge標籤對佈局層級的影響

在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裏面的佈局和控件的位置。

2.3 merge的ID

在學習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了。

三、ViewStub

你必定遇到這樣的狀況:頁面中有些佈局在初始化時不必顯示,可是又不得不事先在佈局文件中寫好,雖然設置成了invisiblegone,可是在初始化時仍是會加載,這無疑會影響頁面加載速度。針對這一狀況,Android爲咱們提供了一個利器————ViewStub。這是一個不可見的,大小爲0的視圖,具備懶加載的功能,它存在於視圖層級中,但只會在setVisibility()inflate()方法調用只會纔會填充視圖,因此不會影響初始化加載速度。它有如下三個重要屬性:

  • android:layout:ViewStub須要填充的視圖名稱,爲「R.layout.xx」的形式;
  • android:inflateId:重寫被填充的視圖的父佈局id。

include標籤不一樣,ViewStubandroid:id屬性是設置ViewStub自己id的,而不是重寫佈局id,這一點可不要搞錯了。另外,ViewStub還提供了OnInflateListener接口,用於監聽佈局是否已經加載了。

3.1 填充佈局的正確方式

咱們先建立一個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()方法屢次調用,咱們能夠採用以下三種方式:

3.1.1 捕獲異常

咱們能夠捕獲異常,同時調用setVisibility()方法顯示佈局。

try {
                    viewStub.inflate();
                } catch (IllegalStateException e) {
                    Log.e("Tag",e.toString());
                    view.setVisibility(View.VISIBLE);
                }複製代碼

3.1.2 經過監聽ViewStub的填充事件

聲明一個布爾值變量isViewStubShow,默認值爲false,佈局填充成功以後,在監聽事件onInflate方法中將其置爲true。

if (isViewStubShow){
                    viewStub.setVisibility(View.VISIBLE);
                }else {
                    viewStub.inflate();
                }複製代碼

3.1.3 直接調用setVisibility()方法

我先來看看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以前,若是設置visibilityVISIBLE的話是會調用inflate()方法的,在mInflatedViewRef不爲null以後就不會再去調用inflate()了。

3.2 viewStub.getVisibility()爲什麼老是等於0?

在顯示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爲空時,則判斷visibilityVISIBLEINVISIBLE時調用inflate()方法填充佈局,若是爲GONE的話則不予處理。這樣一來,在mInflatedViewRef不爲空,也就是已經填充了佈局的狀況下,ViewStub中的setVisibility()方法其實是在設置內部視圖的可見性,而不是ViewStub自己。這樣的設計其實也符合ViewStub的特性,即填充佈局以後就自我銷燬了,給其設置可見性是沒有意義的。

3.3 操做佈局控件

仔細比較一下,其實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上的源碼吧。

思惟導圖

轉載:https://segmentfault.com/a/1190000016402378

相關文章
相關標籤/搜索