前言
最近發現自己有很多頗爲基礎的內容“不會寫”了,就比如今天寫的內容: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(),讓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重繪就可以更新爲最新的數據了。
尾聲
代碼寫的很糙,大家勿噴。主要是通過這麼一個小需求,記錄一下自己那些年無視的源碼細節~~