notifyDataSetChanged()?你真的会用ViewPager吗?

4,792 阅读5分钟

前言

最近发现自己有很多颇为基础的内容“不会写”了,就比如今天写的内容: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重绘就可以更新为最新的数据了。

尾声

代码写的很糙,大家勿喷。主要是通过这么一个小需求,记录一下自己那些年无视的源码细节~~

我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,以及我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~

个人公众号:咸鱼正翻身