最近發現本身有不少頗爲基礎的內容「不會寫」了,就好比今天寫的內容:ViewPager。java
最近有小夥伴,在後臺私信一些技術細節,你們真的好勤奮~~由於工做的緣由,有些私信回覆的不是很及時,多多包涵。996傷不起啊!app
平時咱們很容易遇到這樣的需求:頁面底部不少Tab,能夠點擊或者活動切換不一樣的頁面...估計話尚未說完,有朋友就會脫口而出:ViewPager
+ Fragment
實現。ide
提及ViewPager
,平常需求中必不可少的角色。不管是輪播,仍是Tab頁面效果,ViewPager都幫我們輸出了成噸的傷害。源碼分析
沒錯,今天咱們就聊一聊這個「傳統」的用法。學習
ViewPager + Fragment實現這種效果很簡單。直接上代碼:ui
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
也很簡單:this
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
}
}
}
複製代碼
這個效果難不倒咱們。可是,咱們平常需求確定不可能這麼的簡單。最直接來講,若是咱們頁面須要動態的更換內容,怎麼辦?spa
有朋友可能會說:notifyDataSetChanged()
。code
最開始,我是這麼想的:當Fragment數據須要變化時,改變fragmentData
的內容,而後調Adapter中的notifyDataSetChanged()
。好比這樣:cdn
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;
}
複製代碼
看完這個方法,咱們能獲得倆個信息:
用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重繪就能夠更新爲最新的數據了。
代碼寫的很糙,你們勿噴。主要是經過這麼一個小需求,記錄一下本身那些年無視的源碼細節~~