前言
最近发现自己有很多颇为基础的内容“不会写”了,就比如今天写的内容:ViewPager。
最近有小伙伴,在后台私信一些技术细节,大家真的好勤奋~~因为工作的原因,有些私信回复的不是很及时,多多包涵。996伤不起啊!
正文
平时我们很容易遇到这样的需求:页面底部很多Tab,可以点击或者活动切换不同的页面...估计话还没有说完,有朋友就会脱口而出:ViewPager
+ Fragment
实现。
说起ViewPager
,日常需求中必不可少的角色。无论是轮播,还是Tab页面效果,ViewPager都帮咱们输出了成吨的伤害。
没错,今天我们就聊一聊这个“传统”的用法。
一、最最基本的写法
ViewPager + Fragment实现这种效果很简单。直接上代码:
class TestViewPagerActivity : BaseActivity() {
private lateinit var adapter: ViewPagerAdapter
private val fragmentData = mutableListOf().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, 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(FRAGMENT_PARAM)?.let {
tv_title.text = it.title
}
}
}
普通的Tab滑动难不倒我们。但是,我们日常需求肯定不可能这么的简单。最直接来说,如果我们页面需要动态的更换内容,怎么办?
有朋友可能会说: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(),让object
是TestFragment2
类型的时候,返回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重绘就可以更新为最新的数据了。
尾声
代码写的很糙,大家勿喷。主要是通过这么一个小需求,记录一下自己那些年无视的源码细节~~