前言
今天的文章內容是關於ViewPager的,很多同學可能會吐槽:怎麼還寫這種“低級”的內容!爲什麼?因爲絕大多數的同學都用錯了,當然這主要的原因是搜索引擎推出來的文章大多都是錯的!
正文
一、錯誤用法
不知道有多少同學是這樣用ViewPager的?
class TestViewPagerActivity : BaseActivity() {
private lateinit var adapter: ViewPagerAdapter
private val fragments = mutableListOf<Fragment>().apply {
add(TestFragment1.newInstance("頁面-1"))
add(TestFragment2.newInstance("頁面-2"))
add(TestFragment3.newInstance("頁面-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 fragments: List<Fragment>, fm: FragmentManager) : FragmentPagerAdapter(fm) {
override fun getItem(position: Int): Fragment {
return fragments[position]
}
override fun getCount(): Int {
return fragments.size
}
}
}
如果看到這的同學覺得這個用法沒什麼問題。那麼毫無疑問這篇文章你必須要讀一讀,因爲上述的用法完全曲解的Fragment在ViewPager中的應用。
二、正確用法
我猜有同學可能有疑問了,那正確用法是什麼樣呢?
當然有同學反駁:憑什麼你說你的寫法是對的呢?這還用問嗎?還不是因爲我大!!!....Google的文檔了:ViewPager
class TestViewPagerActivity : BaseActivity() {
private lateinit var adapter: ViewPagerAdapter
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(fm: FragmentManager) : FragmentPagerAdapter(fm) {
override fun getItem(position: Int): Fragment {
return when (position) {
0 -> TestFragment1.newInstance("頁面-1")
1 -> TestFragment2.newInstance("頁面-2")
else -> TestFragment3.newInstance("頁面-3")
}
}
override fun getCount(): Int {
return 3
}
}
}
大家看出這倆種用法的不同了嗎?沒錯不同點只在於getItem()方法的實現。搞懂getItem()的調用,也就搞懂了Fragment在ViewPager裏的正確用法。所以接下來咱們直接上源碼直觀感受ViewPager的設計 。
三、FragmentPagerAdapter源碼
ViewPager對Fragment的支持非常的簡單,整體流程:
- setAdapter時會基於當前position進行初始化當前Fragment
- 接下來會基於mOffscreenPageLimit的值對需要“預加載”的Fragment進行初始化
- 初始化該初始化的Fragment之後,調用commit()通知FragmentManager去attach Fragment
這3步走完,我們當前的Fragment就已經出來了。
接下來咱們通過源碼來具體理解一下上述的1、2、3這幾個步驟。
當我們setAdapter時,會走到popuate方法:
void populate(int newCurrentItem) { // ViewPager中
// ....
// 基於當前position的位置判斷Item(Fragment)是否存在來決定,是否要初始化當前的Fragment
if (curItem == null && N > 0) {
// 而這裏會走到instantiateItem
curItem = addNewItem(mCurItem, curIndex);
}
// 初始化當前之後,會基於limit,初始化該預加載的....
// 此方法在FragmentPagerAdapter中會調用fm的commit
mAdapter.finishUpdate(this);
}
/**
* 這裏會調用instantiateItem(),這裏真正的實現在FragmentPagerAdapter裏
*/
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;
}
一直走到這,我們纔看到FragmentPagerAdapter對Fragment初始化的控制:
public Object instantiateItem(@NonNull ViewGroup container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
// 基於position找到itemId,這方法的默認實現就是position
final long itemId = getItemId(position);
// 生成一個tag
String name = makeFragmentName(container.getId(), itemId);
// 通過上邊生成的tag,在fragmentManager中試圖找到一個Fragment的實例
Fragment fragment = mFragmentManager.findFragmentByTag(name);
// 如果找到,直接調用attach
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
// 否則調用getItem(),基於我們自己的實現拿到Fragment實例。
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
} else {
fragment.setUserVisibleHint(false);
}
}
return fragment;
}
代碼的註釋詳細的說明了FragmentPagerAdapter如果基於當前position進行初始化Fragment的邏輯。簡單再梳理一遍:
- 基於一套規則生成的tag,通過findFragmentByTag()來找是否已經生成過Fragment。
- 如果沒有,調用getItem(),拿到我們自己重寫後return的Fragment實例。
因爲以上的流程,我們可以明確開篇第一種用法一定錯誤的!因爲從源碼我們可以get到一個信息:對於Adapter來說,只有FragmentManage中找不到Fragment實例時纔會調用getItem()去初始化Fragment。因此這其實是一種常見的懶加載機制。
而開篇第一種寫,在初始化的時候就把所有Fragment都new了一遍,很明顯是無意義的!因爲如果我們ViewPager有3個Fragment,用戶不滑到第3個Fragment,那麼new這個Fragment就是浪費的。
接下來咱們再聊一聊第2步中的mOffscreenPageLimit,有經驗的老鐵們都知道這個是用於預加載的,而且這個值最低是1。populate()方法中基於mOffscreenPageLimit來決定預加載position左右倆邊多少個Fragment,1就意味着左右各預加載1個。
由於mOffscreenPageLimit最小是1的原因,所以我們一次至少要加載2個Fragment。而有時我們又偏偏需要在滑動到某個Fragment的時候再執行一些數據加載的操作。
在面對這種場景下,我們一般都會用onHiddenChanged()/setUserVisibleHint()等方法來嘗試做可見性的邏輯回調。其實如果項目中的fragment庫版本較新的時候會發現系統提供了更方便且優雅的方式。
四、更優雅的滑動到當前Fragment時加載數據
新版本下的fragment,在使用FragmentStatePagerAdapter,我們會發現默認的構造方法是過時的:
@Deprecated
public FragmentStatePagerAdapter(@NonNull FragmentManager fm) {
this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
}
會發現系統在構造函數中增加了第二個參數,除了默認BEHAVIOR_SET_USER_VISIBLE_HINT的,系統還提供了BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT。而這個行爲和它的名字一樣,只有在滑動這個Fragment上時纔會調這個Fragment的onResume()方法。
但是注意是回調onResume()。而onResume之前的方法,已經在getItem()中實例化Fragment的時候調完了。
因此我們僅僅想在當前Fragment可見的時候做初始化操作,可以直接使用BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT。
五、getItemPosition()濫用
前段時間在公司項目中,看到有小夥伴重寫了getItemPosition()方法:
public int getItemPosition(@NonNull Object object) {
return POSITION_NONE;
}
這麼寫有沒有問題?說有也有,說沒有也沒有!爲什麼這麼模棱兩可的回答呢?因此這個方法很特殊。
這個方法的註釋是這麼說的:return POSITION_UNCHANGED時,意味着當前視圖沒有發生改變,return POSITION_NONE意味着發生改變。註釋可能有些抽象,咱們結合源碼來理解這個方法。
這個方法只會在ViewPager的dataSetChanged()中被調用,因此我們可以確認重寫這個方法只會在主動嘗試更新ViewPager時生效。
void dataSetChanged() {
// for循環所有Fragment,然後基於getItemPosition()返回值判斷是否需要remove
for (int i = 0; i < mItems.size(); i++) {
final ItemInfo ii = mItems.get(i);
final int newPos = mAdapter.getItemPosition(ii.object);
if (newPos == PagerAdapter.POSITION_UNCHANGED) {
continue;
}
if (newPos == PagerAdapter.POSITION_NONE) {
// 可以看到,如果是POSITION_NONE,就會remove當前i下的Fragment
// 省略部分代碼
mItems.remove(i);
mAdapter.destroyItem(this, ii.position, ii.object);
}
// 省略部分代碼
}
// 省略部分代碼
// 此方法中會再次調用populate()去重新走初始化的操作
setCurrentItemInternal(newCurrItem, false, true);
}
有了上述源碼的邏輯,其實我們就能夠明白getItemPosition()的意義:當我們想使用notifyDataSetChanged()去刷新ViewPager時,getItemPosition()的返回時決定當前的Fragment是否需要被remove。因此當我們不需要remove當前的Fragment時,則return POSITION_UNCHANGED(這樣此Fragment就不會發生任何狀態變化),否者則return POSITION_NONE(這樣此Fragment就會被remove,然後重新初始化新的Fragment)。我們就可以做出類似於RecyclerView的diff操作。
基於自身產品邏輯,合理的重寫getItemPosition(),避免不必要Fragment的銷燬重建。
六、如何主動get到ViewPager的Fragment實例
我們都知道,FragmentManager爲我們提供了findFragmentById()/findFragmentByTag()。同樣對於ViewPager也是如此,在第三部分源碼分析的時候,我們知道FragmentPagerAdapter中獲取也是通過findFragmentByTag()嘗試獲取當前Fragment的實例,而tag的實現來自makeFragmentName(container.getId(), itemId)
private static String makeFragmentName(int viewId, long id) {
return "android:switcher:" + viewId + ":" + id;
}
所以,我們獲取ViewPager中的Fragment也可以藉助這種方式。千萬不要像搜索引擎裏推出的那些答案:**主動調用什麼getItem()!**有了上邊源碼的分析,我猜大家已經get到這些用法錯的是多麼離譜!!!
尾聲
OK,本次想聊的就是這麼多~以後的文章,我會力求在絕對正確的情況下再發出來,儘可能的不要誤人子弟!
畢竟就今天的ViewPager而言,其實我一開始也是用那種錯誤的寫法,沒錯,就是受搜索引擎推出來的錯誤文章所誤導!
既然自己踩過坑,爭取能填上一個是一個