你的ViewPager八成用錯了。

前言

創做過程:2020年5月22下午4點左右開始寫,晚上9點55寫下尾聲。晚上11點-12點補充第5、第六部分。java

有段時間沒寫文章了,此次不是由於懶...而是的確很忙...android

今天的文章內容是關於ViewPager的,不少同窗可能會吐槽:怎麼還寫這種「低級」的內容!爲何?由於絕大多數的同窗都用錯了,固然這主要的緣由是搜索引擎推出來的文章大多都是錯的!app

正文

1、錯誤用法

不知道有多少同窗是這樣用ViewPager的?ide

class TestViewPagerActivity : BaseActivity() {
    private lateinit var adapter: ViewPagerAdapter
    private val fragments = mutableListOf<Fragment>().apply {
        add(TestFragment1.newInstance("頁面-1"))
        add(TestFragment2.newInstance("頁面-2"))
        add(TestFragment3.newInstance("頁面-3"))
	}

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test_view_pager)
        adapter = ViewPagerAdapter(fragmentData, supportFragmentManager)
        vp.adapter = adapter
    }

    inner class ViewPagerAdapter(val fragments: List<Fragment>, fm: FragmentManager) : FragmentPagerAdapter(fm) {
        override fun getItem(position: Int): Fragment {
            return fragments[position]
        }

        override fun getCount(): Int {
            return fragments.size
        }
    }
}
複製代碼

若是看到這的同窗以爲這個用法沒什麼問題。那麼毫無疑問這篇文章你必需要讀一讀,由於上述的用法徹底曲解的Fragment在ViewPager中的應用。函數

2、正確用法

我猜有同窗可能有疑問了,那正確用法是什麼樣呢?源碼分析

固然有同窗反駁:憑什麼你說你的寫法是對的呢?這還用問嗎?還不是由於我大!!!....Google的文檔了:ViewPager學習

class TestViewPagerActivity : BaseActivity() {
    private lateinit var adapter: ViewPagerAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test_view_pager)
        adapter = ViewPagerAdapter(fragmentData, supportFragmentManager)
        vp.adapter = adapter
    }

    inner class ViewPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {
        override fun getItem(position: Int): Fragment {
            return when (position) {
                0 -> TestFragment1.newInstance("頁面-1")
                1 -> TestFragment2.newInstance("頁面-2")
                else -> TestFragment3.newInstance("頁面-3")
            }
        }

        override fun getCount(): Int {
            return 3
        }
    }
}
複製代碼

你們看出這倆種用法的不一樣了嗎?沒錯不一樣點只在於getItem()方法的實現。搞懂getItem()的調用,也就搞懂了Fragment在ViewPager裏的正確用法。因此接下來我們直接上源碼直觀感覺ViewPager的設計 。ui

3、FragmentPagerAdapter源碼

ViewPager對Fragment的支持很是的簡單,總體流程:this

    1. setAdapter時會基於當前position進行初始化當前Fragment
    1. 接下來會基於mOffscreenPageLimit的值對須要「預加載」的Fragment進行初始化
    1. 初始化該初始化的Fragment以後,調用commit()通知FragmentManager去attach Fragment

這3步走完,咱們當前的Fragment就已經出來了。搜索引擎

接下來我們經過源碼來具體理解一下上述的一、二、3這幾個步驟。

當咱們setAdapter時,會走到popuate方法:

void populate(int newCurrentItem) { // ViewPager中
    // ....
    // 基於當前position的位置判斷Item(Fragment)是否存在來決定,是否要初始化當前的Fragment
    if (curItem == null && N > 0) {
        // 而這裏會走到instantiateItem
        curItem = addNewItem(mCurItem, curIndex);
    }
    // 初始化當前以後,會基於limit,初始化該預加載的....
    // 此方法在FragmentPagerAdapter中會調用fm的commit
    mAdapter.finishUpdate(this);
}

/** * 這裏會調用instantiateItem(),這裏真正的實如今FragmentPagerAdapter裏 */
ItemInfo addNewItem(int position, int index) {
    ItemInfo ii = new ItemInfo();
    ii.position = position;
    ii.object = mAdapter.instantiateItem(this, position);
    ii.widthFactor = mAdapter.getPageWidth(position);
    if (index < 0 || index >= mItems.size()) {
        mItems.add(ii);
    } else {
        mItems.add(index, ii);
    }
    return ii;
}
複製代碼

一直走到這,咱們纔看到FragmentPagerAdapter對Fragment初始化的控制:

public Object instantiateItem(@NonNull ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    // 基於position找到itemId,這方法的默認實現就是position
    final long itemId = getItemId(position);

    // 生成一個tag
    String name = makeFragmentName(container.getId(), itemId);
    // 經過上邊生成的tag,在fragmentManager中試圖找到一個Fragment的實例
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    // 若是找到,直接調用attach
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
    	// 不然調用getItem(),基於咱們本身的實現拿到Fragment實例。
        fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
            mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
        } else {
            fragment.setUserVisibleHint(false);
        }
    }
    return fragment;
}
複製代碼

代碼的註釋詳細的說明了FragmentPagerAdapter若是基於當前position進行初始化Fragment的邏輯。簡單再梳理一遍:

  • 基於一套規則生成的tag,經過findFragmentByTag()來找是否已經生成過Fragment。
  • 若是沒有,調用getItem(),拿到咱們本身重寫後return的Fragment實例。

由於以上的流程,咱們能夠明確開篇第一種用法必定錯誤的!由於從源碼咱們能夠get到一個信息:對於Adapter來講,只有FragmentManage中找不到Fragment實例時纔會調用getItem()去初始化Fragment。所以這實際上是一種常見的懶加載機制。

而開篇第一種寫,在初始化的時候就把全部Fragment都new了一遍,很明顯是無心義的!由於若是咱們ViewPager有3個Fragment,用戶不滑到第3個Fragment,那麼new這個Fragment就是浪費的。

接下來我們再聊一聊第2步中的mOffscreenPageLimit,有經驗的老鐵們都知道這個是用於預加載的,並且這個值最低是1。populate()方法中基於mOffscreenPageLimit來決定預加載position左右倆邊多少個Fragment,1就意味着左右各預加載1個。

因爲mOffscreenPageLimit最小是1的緣由,因此咱們一次至少要加載2個Fragment。而有時咱們又恰恰須要在滑動到某個Fragment的時候再執行一些數據加載的操做。

在面對這種場景下,咱們通常都會用onHiddenChanged()/setUserVisibleHint()等方法來嘗試作可見性的邏輯回調。其實若是項目中的fragment庫版本較新的時候會發現系統提供了更方便且優雅的方式。

4、更優雅的滑動到當前Fragment時加載數據

新版本下的fragment,在使用FragmentStatePagerAdapter,咱們會發現默認的構造方法是過期的:

@Deprecated
public FragmentStatePagerAdapter(@NonNull FragmentManager fm) {
    this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
}
複製代碼

會發現系統在構造函數中增長了第二個參數,除了默認BEHAVIOR_SET_USER_VISIBLE_HINT的,系統還提供了BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT。而這個行爲和它的名字同樣,只有在滑動這個Fragment上時纔會調這個Fragment的onResume()方法。

可是注意是回調onResume()。而onResume以前的方法,已經在getItem()中實例化Fragment的時候調完了。

所以咱們僅僅想在當前Fragment可見的時候作初始化操做,能夠直接使用BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT。

5、getItemPosition()濫用

前段時間在公司項目中,看到有小夥伴重寫了getItemPosition()方法:

public int getItemPosition(@NonNull Object object) {
    return POSITION_NONE;
}
複製代碼

這麼寫有沒有問題?說有也有,說沒有也沒有!爲何這麼模棱兩可的回答呢?所以這個方法很特殊。

這個方法的註釋是這麼說的:return POSITION_UNCHANGED時,意味着當前視圖沒有發生改變,return POSITION_NONE意味着發生改變。註釋可能有些抽象,我們結合源碼來理解這個方法。

這個方法只會在ViewPager的dataSetChanged()中被調用,所以咱們能夠確認重寫這個方法只會在主動嘗試更新ViewPager時生效。

void dataSetChanged() {
    // for循環全部Fragment,而後基於getItemPosition()返回值判斷是否須要remove
    for (int i = 0; i < mItems.size(); i++) {
        final ItemInfo ii = mItems.get(i);
        final int newPos = mAdapter.getItemPosition(ii.object);

        if (newPos == PagerAdapter.POSITION_UNCHANGED) {
            continue;
        }

        if (newPos == PagerAdapter.POSITION_NONE) {
        	// 能夠看到,若是是POSITION_NONE,就會remove當前i下的Fragment
        	// 省略部分代碼
            mItems.remove(i);          
            mAdapter.destroyItem(this, ii.position, ii.object);
        }
        // 省略部分代碼
    }
    // 省略部分代碼
    // 此方法中會再次調用populate()去從新走初始化的操做
    setCurrentItemInternal(newCurrItem, false, true);
}

複製代碼

有了上述源碼的邏輯,其實咱們就可以明白getItemPosition()的意義:當咱們想使用notifyDataSetChanged()去刷新ViewPager時,getItemPosition()的返回時決定當前的Fragment是否須要被remove。所以當咱們不須要remove當前的Fragment時,則return POSITION_UNCHANGED(這樣此Fragment就不會發生任何狀態變化),否者則return POSITION_NONE(這樣此Fragment就會被remove,而後從新初始化新的Fragment)。咱們就能夠作出相似於RecyclerView的diff操做。

基於自身產品邏輯,合理的重寫getItemPosition(),避免沒必要要Fragment的銷燬重建。

6、如何主動get到ViewPager的Fragment實例

咱們都知道,FragmentManager爲咱們提供了findFragmentById()/findFragmentByTag()。一樣對於ViewPager也是如此,在第三部分源碼分析的時候,咱們知道FragmentPagerAdapter中獲取也是經過findFragmentByTag()嘗試獲取當前Fragment的實例,而tag的實現來自makeFragmentName(container.getId(), itemId)

private static String makeFragmentName(int viewId, long id) {
    return "android:switcher:" + viewId + ":" + id;
}
複製代碼

因此,咱們獲取ViewPager中的Fragment也能夠藉助這種方式。千萬不要像搜索引擎裏推出的那些答案:**主動調用什麼getItem()!**有了上邊源碼的分析,我猜你們已經get到這些用法錯的是多麼離譜!!!

尾聲

OK,本次想聊的就是這麼多~之後的文章,我會力求在絕對正確的狀況下再發出來,儘量的不要誤人子弟!

畢竟就今天的ViewPager而言,其實我一開始也是用那種錯誤的寫法,沒錯,就是受搜索引擎推出來的錯誤文章所誤導!

既然本身踩過坑,爭取能填上一個是一個!

我是一個應屆生,最近和朋友們維護了一個公衆號,內容是咱們在從應屆生過渡到開發這一路所踩過的坑,以及咱們一步步學習的記錄,若是感興趣的朋友能夠關注一下,一同加油~

我的公衆號:鹹魚正翻身
相關文章
相關標籤/搜索