TabLayout+ViewPager+Fragment實現切頁展示

yourzeromax發表於2018-08-27

寫在前面

網易雲介面
目前大多數的APP都採用的是幾個Tab標籤以及多個介面滑動的形式來提供多層次的互動體驗,最為常用的做法就是採用TabLayout+ViewPager+Fragment的方式,最近在公司專案中遇到類似的介面,也看了各個論壇很多份部落格,但是發現都沒有完全把這種方法的坑填完,因此寫下這篇部落格,一方面是對知識的總結,另一方面也能讓其他開發者們少走一些彎路,部落格內容主要分為四個章節:

  1. TabLayout+ViewPager+Fragment的簡單用法總結。
  2. 所使用的兩種PagerAdapter的差別分析及選擇。
  3. 懶載入策略。
  4. 卡頓及效能優化建議。

一般情況下上面四個章節的內容足以應付過來,但是往往在一些特殊的情況下,仍然會遇到一些不能解決的問題,這時就需要深入到原始碼之中來具體問題具體分析。話不多說,接下來將進行使用總結。

TabLayout+ViewPager+Fragment的用法

首先,需要引入工具包:

    implementation 'com.android.support:design:27.1.1'
    implementation 'com.android.support:support-v4:27.1.1'

用法其實非常簡單,有點類似於RecyclerView,其中主要關心四個物件:Tablayout、ViewPager、PagerAdapter、Fragment。前兩個就跟普通的View控制元件一樣,可以直接通過XML來進行佈局以及在onCreate獲取相應的例項:

<?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=".activities.TabLayoutActivity">

    <android.support.design.widget.TabLayout
        android:id="@+id/tl_tabs"
        android:layout_width="match_parent"
        android:layout_height="40dp" />

    <android.support.v4.view.ViewPager
        android:id="@+id/vp_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

Fragment建議採用v4相容包下的,我們所需要使用的Fragment是需要自己來實現,但是和普通的Fragment沒什麼區別,因此也就省略了Fragment的建立步驟,而PagerAdapter有兩種實現可以使用,具體會在下一小節介紹,TabLayout+ViewPager+Fragment方法的使用流程:

  1. 建立儲存多個Fragment例項的列表
  2. 建立PagerAdapter例項並關聯到Viewpager中
  3. 將ViewPager關聯到Tablayout中
  4. 根據需求改寫Tablayout屬性*

最後一步不是必須的,為了更加清楚地描述這個呼叫流程,貼上一個示意圖:
這裡寫圖片描述

貼上程式碼:

public class TabLayoutActivity extends AppCompatActivity implements MyFragment.OnFragmentInteractionListener {
    TabLayout tabLayout;
    ViewPager viewPager;
    
    List<Fragment> fragments = new ArrayList<>();
    List<String> titles = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tab_layout);

        tabLayout = findViewById(R.id.tl_tabs);
        viewPager = findViewById(R.id.vp_content);

        fragments.add(MyFragment.newInstance("11111", "11111"));
        fragments.add(MyFragment.newInstance("22222", "22222"));
        fragments.add(MyFragment.newInstance("33333", "33333"));
        fragments.add(MyFragment.newInstance("44444", "44444"));
        fragments.add(MyFragment.newInstance("55555", "55555"));
        titles.add("fragment1");
        titles.add("fragment2");
        titles.add("fragment3");
        titles.add("fragment4");
        titles.add("fragment5");
        viewPager.setAdapter(new FragmentStatePagerAdapter(getSupportFragmentManager()) {
            @Override
            public Fragment getItem(int position) {
                return fragments.get(position);
            }

            @Override
            public int getCount() {
                return fragments.size();
            }

            @Override
            public void destroyItem(ViewGroup container, int position, Object object) {
                super.destroyItem(container, position, object);
            }

            @Nullable
            @Override
            public CharSequence getPageTitle(int position) {

                return titles.get(position);
            }
        });

        tabLayout.setupWithViewPager(viewPager);
    }

    @Override
    public void onFragmentInteraction(Uri uri) {

    }
}

getPageTitle(int position)函式是返回當前TabLayout的標籤標題的,當然,也可以不通過PagerAdapter中的這個函式返回,採用下面的這種方式也可行(有多少個就addTab多少次):

 tabLayout.addTab(tabLayout.newTab().setText("tab 1"));

PagerAdapter

PagerAdapter是一個抽象類,它有兩個實現子類供我們使用,分別是FragmentStatePagerAdapter和FragmentPagerAdapter。建立這兩個類的例項需要傳入一個FragmentManager物件,像程式碼那樣處理就行了,從類名就可以看出來它倆的最大差別就在“State-狀態”上,什麼意思呢?指的是所包含儲存的Fragment物件的狀態是否儲存。看原始碼可以發現,FragmentStatePagerAdapter中比FragmentPagerAdapter多維護著兩個列表:

    private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
    private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();

而這兩個列表帶來的最大差別則體現在void destroyItem(ViewGroup container, int position, Object object)這個函式之中,看下FragmentStatePagerAdapter的函式原始碼:

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment) object;

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
                + " v=" + ((Fragment)object).getView());
        while (mSavedState.size() <= position) {
            mSavedState.add(null);
        }
        mSavedState.set(position, fragment.isAdded()
                ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
        mFragments.set(position, null);

        mCurTransaction.remove(fragment);
    }

再看下FragmentPagerAdapter的這個函式:

   @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
                + " v=" + ((Fragment)object).getView());
        mCurTransaction.detach((Fragment)object);
    }

具體情況就不再往下分析啦,還有一個坑等下再說。
ViewPager還有一個比較重要的函式是:

viewPager.setOffscreenPageLimit(int limit);

這個方法預設值為1,Google在開發ViewPager時,考慮到如果滑動的時候才建立Fragment例項時會帶來一定程度的卡頓,因此為ViewPager設定了快取機制,而上述函式則是設定快取Fragment的數量,示意圖如下:
這裡寫圖片描述
也就是說,limit的值代表著還要快取當前Fragment左右各limit個Fragment,一共會建立2*limit+1個Fragment。超出這個limit範圍的Fragment就會被銷燬,而上述兩種PagerAdapter的差別就是銷燬的流程不同!
這裡就不放Log圖給大家看,直接告訴大家,FragmentPagerAdapter在銷燬Fragment時不會呼叫onDestroy()方法,而帶了State的Adapter則會呼叫Fragment的onDestroy()方法,換言之,前者僅僅是銷燬了Fragment的View檢視而沒有銷燬Fragment這個物件,但是後者則徹徹底底地消滅了Fragment物件,這是很重要的知識要點哦~!也是下面談效能優化和懶載入的前提條件。

本小節最後,告訴大家一個關於如何選擇PagerAdapter的結論:

FragmentPagerAdapter適用於Fragment比較少的情況,它會把每一個Fragment儲存在記憶體中,不用每次切換的時候,去儲存現場,切換回來在重新建立,所以使用者體驗比較好。而對於Fragment比較多的情況,需要切換的時候銷燬以前的Fragment以釋放記憶體,就可以使用FragmentStatePagerAdapter。

暫時不懂這句話的含義沒關係,請接著往下面看。

懶載入策略

Android的View繪製流程是最消耗CPU時間片的操作,尤其是在ViewPager快取Fragment的情況下,如果在View繪建的同時還進行多個Fragment的資料載入,那使用者體驗簡直是爆炸(不僅浪費流量,而且還造成不必要的卡頓)。。。因此,需要對Fragment們進行懶載入策略。什麼是懶載入?就是被動載入,當Fragment頁面可見時,才從網路載入資料並顯示出來。那什麼時候Fragment可見呢?Fragment之中有這樣一個函式:

  @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        doYourJobs();
    }

當Fragment的可見狀態發生變化時就會呼叫這個函式,boolean引數isVisibleToUser代表當前的Fragment是否可見。
如果這麼簡單地呼叫函式就能實現懶載入的話,那也沒什麼好說的,但是這裡又有一個巨坑,則是因為這個setUserVisibleHint函式是遊離在Fragment生命週期之外的,它的執行有可能早於onCreate和onCreateView,然而既然要時間資料的載入,就必須要在onCreateView建立完檢視過後才能使用,不然就會返回空指標崩潰,懶載入的重點也是在這兒,那麼我們來分析,實行懶載入必須滿足哪些條件呢?

1.View檢視載入完畢,即onCreateView()執行完成
2.當前Fragment可見,即setUserVisibleHint()的引數為true
3.初次載入,即防止多次滑動重複載入

有了這兩個條件過後,便能夠正常執行懶載入過程,我們在Fragment全域性變數之中增加對應的三個標誌引數並賦上初始值:

boolean mIsPrepare = false;		//檢視還沒準備好
boolean mIsVisible= false;		//不可見
boolean mIsFirstLoad = true;	//第一次載入

當然在onCreateView中確保了View已經準備好時,將mPrepare置為true,在setUserVisibleHint中確保了當前可見時,mIsVisible置為true,第一次載入完畢後則將mIsFirstLoad置為false,避免重複載入。

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    mIsPrepare = true;
    lazyLoad();
}

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    //isVisibleToUser這個boolean值表示:該Fragment的UI 使用者是否可見
    if (isVisibleToUser) {
        mIsVisible = true;
        lazyLoad();
    } else {
        mIsVisible = false;
    }
}

最後,貼上懶載入的lazyLoad()程式碼:

一定要記住,只要標誌位改變,就要進行lazyLoad()函式的操作

private void lazyLoad() {
    //這裡進行三個條件的判斷,如果有一個不滿足,都將不進行載入
    if (!mIsPrepare || !mIsVisible||!mIsFirstLoad) {
    return;
    }
        loadData();
        //資料載入完畢,恢復標記,防止重複載入
        mIsFirstLoad = false;
    }
    
  private void loadData() {
    //這裡進行網路請求和資料裝載
    }

當然,在最後,如果Fragment銷燬的話,還應該將三個標誌位進行預設值初始化:

   @Override
    public void onDestroyView() {
        super.onDestroyView();
        mIsFirstLoad=true;
        mIsPrepare=false;
        mIsVisible = false;
    }

為什麼在onDestroyView中進行而不是在onDestroy中進行呢?這又要提到之前Adapter的差異,onDestroy並不一定會呼叫,讀者可以思考思考為什麼。

卡頓及效能優化建議

Fragment的載入最為耗時的步驟主要有兩個,一個是Fragment建立(尤其是建立View的過程),另一個就是讀取資料填充到View上的過程。懶載入能夠解決後者所造成的卡頓,但是針對前者來說,並沒有效果。
Google為了避免使用者因翻頁而造成卡頓,採用了快取的形式,但是其實緩不快取,只要該Fragment會顯示,都會進行Fragment建立,都會耗費相應的時間,換言之,快取只不過將本應該在翻頁時的卡頓集中在啟動該Activity的時候一起卡頓。

優化方案一:設定快取頁面數

viewPager.setOffscreenPageLimit(int limit) 能夠有效地一次性快取多個Fragment,這樣就能夠解決在之後每次切換時不會建立例項物件,看起來也會流暢。但是這樣的做法,最大的缺點就是容易造成第一次啟動時非常緩慢!如果第一次啟動時間滿足要求的話,就使用這種簡單地辦法吧。

優化方案二:避免Fragment的銷燬

不管是FragmentStatePagerAdapter還是FragmentPagerAdapter,其中都有一個方法可以被覆寫:

            @Override
            public void destroyItem(ViewGroup container, int position, Object object) {
               // super.destroyItem(container, position, object);
            }

把中間的程式碼註釋掉就行了,這樣就可以避免Fragment的銷燬過程,一般情況下能夠這樣使用,但是容易出現一個問題,我們再來看看FragmentStatePagerAdapter的原始碼:

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment) object;

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
                + " v=" + ((Fragment)object).getView());
        while (mSavedState.size() <= position) {
            mSavedState.add(null);
        }
        mSavedState.set(position, fragment.isAdded()
                ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
        mFragments.set(position, null);

        mCurTransaction.remove(fragment);
    }

看到沒?這個過程之中包含了對FragmentInstanceState的儲存!這也是FragmentStatePagerAdapter的精髓之處,如果註釋掉,一旦Activity被回收進入異常銷燬狀態,Fragment就無法恢復之前的狀態,因此這種方法也是有紕漏和侷限性的。FragmentPagerAdapter的原始碼就留給大家自己去研究分析,也會發現一些問題的哦。

優化方案三:避免重複建立View

優化Viewpager和Fragment的方法就是儘可能地避免Fragment頻繁建立,當然,最為耗時的都是View的建立。所以更加優秀的優化方案,就是在Fragment中快取自身有關的View,防止onCreateView函式的頻繁執行,我就直接上原始碼了:

public class MyFragment extends Fragment {
	View rootView;
	
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        if (rootView == null) {
            rootView = inflater.inflate(R.layout.fragment_my, container, false);
        }
        return rootView;

   @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.d(TAG, "onDestroyView: " + mParam1);
        mIsFirstLoad=true;
        mIsPrepare=false;
        mIsVisible = false;
        if (rootView != null) {
            ((ViewGroup) rootView.getParent()).removeView(rootView);
        }
}

onCreateView中將會對rootView進行null判斷,如果為null,說明還沒有快取當前的View,因此會進行過快取,反之則直接利用。當然,最為重要的是需要在onDestroyView() 方法中及時地移除rootView,因為每一個View只能擁有一個Parent,如果不移除,將會重複載入而導致程式崩潰。

其實ViewPager+Fragment的方式,ViewPager中顯示的就是Fragment中所建立的View,Fragment只是一個控制器,並不會直接顯示於ViewPager之中,這一點容易被忽略。

暫時想到的優化方案就只有這麼多了。

總結

本文主要講述兩個部分的知識:三駕馬車實現切頁展示的基礎方法以及如何優化效能表現和避免卡頓。其中,對於ViewPager+Fragment體系的卡頓原因進行了分析,也主要有兩個方面:建立Framgent例項(建立View)資料載入導致卡頓。後者卡頓通過懶載入的形式能夠完美解決,而前者因例項建立引起的卡頓則提出了三種不同的優化選擇,應該說,每一種方案都有利有弊,並沒有絕對的好與不好,在專案運用中,還是得根據需求和實際情況來進行選擇,當然,要從記憶體洩漏、卡頓時間、容錯率等多個方面來綜合考量。不過話說回來,最優的優化方案還是儘可能的精簡自己的View佈局。

總之,Fragment是Android中最為重要的知識點之一,我在總結本部落格的過程之中也有很大的收穫,多看原始碼瞭解問題的根源過後再對症下藥,不失為一種程式設計師的基本素養。

歡迎關注我的部落格
頭條內推簡歷請投遞:yourzeromax@163.com

相關文章