ViewPager的notifyDataSetChanged()沒有效果?來從源碼上解決這個問題

前言

最近發現自己有很多頗爲基礎的內容“不會寫”了,就比如今天寫的內容:ViewPager。

最近有小夥伴,在後臺私信一些技術細節,大家真的好勤奮~~因爲工作的原因,有些私信回覆的不是很及時,多多包涵。996傷不起啊!

正文

平時我們很容易遇到這樣的需求:頁面底部很多Tab,可以點擊或者活動切換不同的頁面…估計話還沒有說完,有朋友就會脫口而出:ViewPager+ Fragment實現。

說起ViewPager,日常需求中必不可少的角色。無論是輪播,還是Tab頁面效果,ViewPager都幫咱們輸出了成噸的傷害。

沒錯,今天我們就聊一聊這個“傳統”的用法。

一、最最基本的寫法

ViewPager + Fragment實現這種效果很簡單。直接上代碼:

class TestViewPagerActivity : BaseActivity() {
    private lateinit var adapter: ViewPagerAdapter
    private val fragmentData = mutableListOf<FragmentParams>().apply {
        add(FragmentParams("頁面-1"))
        add(FragmentParams("頁面-2"))
        add(FragmentParams("頁面-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 data: List<FragmentParams>, fm: FragmentManager) : FragmentPagerAdapter(fm) {
        override fun getItem(position: Int): Fragment {
            return when (position) {
                0 -> TestFragment1.newInstance(data[position])
                1 -> TestFragment2.newInstance(data[position])
                else -> TestFragment3.newInstance(data[position])
            }
        }

        override fun getCount(): Int {
            return 3
        }
    }
}

@Parcelize
data class FragmentParams(var title: String) : Parcelable

當然,Fragment也很簡單:

class TestFragment1 : BaseFragment() {
    companion object {
        const val FRAGMENT_PARAM = "fragment_params"
        fun newInstance(params: FragmentParams): Fragment =
                TestFragment1().apply {
                    arguments = Bundle().apply {
                        putParcelable(FRAGMENT_PARAM, params)
                    }
                }
    }

    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_test1, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        arguments?.getParcelable<FragmentParams>(FRAGMENT_PARAM)?.let {
            tv_title.text = it.title
        }
    }
}

這個效果難不倒我們。但是,我們日常需求肯定不可能這麼的簡單。最直接來說,如果我們頁面需要動態的更換內容,怎麼辦?

有朋友可能會說:notifyDataSetChanged()

二、notifyDataSetChanged()

最開始,我是這麼想的:當Fragment數據需要變化時,改變fragmentData的內容,然後調Adapter中的notifyDataSetChanged()。比如這樣:

fun refreshUI(){
    fragmentData[1].title="新的頁面-2"
    adapter.notifyDataSetChanged()
}

然而run起來,我並沒有發現頁面有任何的變化。並且沒有發現任何方法被重新調用!這也就是說明,notifyDataSetChanged()一定需要特定的條件。

我猜踩過坑的小夥伴應該知道,此時應該重寫getItemPosition(@NonNull Object object)方法

那麼接下來就讓我們從源碼中一探究竟,如何才能使notifyDataSetChanged()生效…

三、源碼分析

這個方法只是對外暴露出現的接口,notifyDataSetChanged()最終會調用到ViewPager的Observer中,也就是下邊的方法:

private class PagerObserver extends DataSetObserver {
    PagerObserver() {
    }

    @Override
    public void onChanged() {
        dataSetChanged();
    }
    @Override
    public void onInvalidated() {
        dataSetChanged();
    }
}

而邏輯的關鍵就在dataSetChage()中:

void dataSetChanged() {
    // 遍歷所有的mItems
    for (int i = 0; i < mItems.size(); i++) {
        final ItemInfo ii = mItems.get(i);
        // 這個方法是關鍵,也就是上文提到的重寫getItemPosition()
        final int newPos = mAdapter.getItemPosition(ii.object);
        // 如果不重寫,默認就是POSITION_UNCHANGED,也就是說遍歷的時候直接continue掉。
        if (newPos == PagerAdapter.POSITION_UNCHANGED) {
            continue;
        }

        if (newPos == PagerAdapter.POSITION_NONE) {
            // 省略部分代碼
            // 等於POSITION_NONE時,我們可以看到,此時destory掉當前的Item,也就是當前的Fragment
            mAdapter.destroyItem(this, ii.position, ii.object);
            // 省略部分代碼
        }
        // 省略部分代碼
        if (needPopulate) {
            // 省略部分代碼
            // Fragment被移除,那麼勢必要有重新添加的過程,而具體的實現就在下邊...
            setCurrentItemInternal(newCurrItem, false, true);
            requestLayout();
        }
    }
}

點進setCurrentItemInternal()方法,我們會發現細節比較多,這裏我們就不深究這麼多的邊界條件,直接進入它內部的populate(),而這個方法內部又會調用addNewItem(),這個方法我們需要看一下:

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;
}

這裏的實現,有一個我們比較熟悉的方法instantiateItem()。而這個方法在FragmentPagerAdapter裏被重寫了:

注意看這個方法,有很多細節藏在裏邊!

@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    // 這個方法默認實現是return的position
    final long itemId = getItemId(position);
    // 這裏是Adapter通過tag去嘗試從FragmentManager中找已經被管理的Fragment
    String name = makeFragmentName(container.getId(), itemId);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        // 如果找到,直接attach
        mCurTransaction.attach(fragment);
    } else {
        // 如果找不到,調用getItem()交由業務放去處理new Fragment的實現
        fragment = getItem(position);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }
    return fragment;
}

看完這個方法,我們能得到倆個信息:

  • 1、如果FragmentManager能通過Tag找到Fragment的實例,那麼就直接attch()上這個Fragment
  • 2、如果找不到,纔會調用getItem()去初始化這個Fragment

基於這個實現,我們就明白了。前文中因爲POSITION_NONE被detach掉的Fragment在這裏被attach上的。

四、解決問題

既然如此,那麼對於我們開篇的那個動態改Fragment內容信息的需求,也就迎刃而解了:

這裏我們只需寫getItemPosition(),讓objectTestFragment2類型的時候,返回PagerAdapter.POSITION_NONE。就可以解決這個問題了。

override fun getItemPosition(`object`: Any): Int {
    if(`object` is TestFragment2){
        return PagerAdapter.POSITION_NONE
    }
    return super.getItemPosition(`object`)
}

detach/attach過程,會使Fragment重繪,也就是重走onCreateView()onViewCreated()。因此此時我們的數據源已經發生了變化,所以Fragment重繪就可以更新爲最新的數據了。

尾聲

代碼寫的很糙,大家勿噴。主要是通過這麼一個小需求,記錄一下自己那些年無視的源碼細節~~

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

個人公衆號:鹹魚正翻身

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章