以下圖巢狀的 ViewPager 為例,它是一個兩層巢狀的 ViewPager,也就是說 ViewPager 裡面是 Fragment ,每個 Fragment 裡面又是一個 ViewPager。在下面的例子中,每個 Fragment 都有一個相同名字的選單項,可以看到不在當前頁顯示的 Fragment 它的選單項也顯示出來了。使用者滑動到 B ,ViewPager 能正確處理第一層的選單,顯示 B 的時候同時預載入 A、C 兩個 Fragment,而選單裡只顯示 B 的選單項。到第二層就有問題了, BA 是第二層當前的 Fragment,它的選單項也能顯示出來,這沒問題。但卻多出來 AA 和 CA,這是因為 ViewPager 預載入了 A,A 裡面的 ViewPager 把 AA 當成是當前 Fragment,把它的選單項也顯示出來了。CA 也是同樣道理。
決定是否顯示選單的程式碼是由 PagerAdapter#setPrimaryItem
實現的,屬於主項(primary item)的 fragment 才會顯示選單項。以 FragmentPagerState 為例,具體程式碼如下:
Fragment fragment = (Fragment)object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
mCurrentPrimaryItem.setUserVisibleHint(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
fragment.setUserVisibleHint(true);
}
mCurrentPrimaryItem = fragment;
}複製程式碼
AA、CA 的顯示就很好理解了,因為它們各自是 A 和 C 的主項(primary item),所以都呼叫了 setMenuVisibility(true)
。
要修復這個問題,一開始想到的是覆蓋父 fragment 的 setMenuVisibility
方法,把值傳遞到當前子 fragment
@Override public void setMenuVisibility(boolean menuVisible) {
if (isAdded() && getChildFragmentManager().getFragments() != null) {
Fragment f = getChildFragmentManager().findFragmentByTag(
"android:switcher:" + mPager.getId() + ":" + mPager.getCurrentItem());// 不支援 FragmentStatePagerAdapter
if (f != null) {
f.setMenuVisibility(menuVisible);
}
}
super.setMenuVisibility(menuVisible);
}複製程式碼
這樣從 A 滑到 B 時,AA 能隱藏了。但仍然不能解決問題,從 A 滑到 B 時離屏載入 C,並設定 C 的 MenuVisibility 為 false。FragmentPagerAdapter 幾次 setMenuVisibility 都在 finishUpdate 之前,所以此時 C 還未新增到 Activity,CA 更不存在。等到 CA 載入時,已經不會再觸發 C 的 MenuVisibility 了。
考慮自定義 FragmentPagerAdapter,主項(primary item)的 fragment 的 menuVisibility 同步父 Fragment 的狀態,mParent
是介面卡建構函式傳入的 ViewPager 宿主 Fragment。
@Override public void setPrimaryItem(ViewGroup container, int position, Object object) {
super.setPrimaryItem(container, position, object);
if (mParent != null) ((Fragment) object).setMenuVisibility(mParent.isMenuVisible());
}複製程式碼
這樣的問題是,從 A 滑到 B 時,只是根 ViewPager 的當前主項(primary item)發生變化,A 介面卡和 B 介面卡的主項不會發生變化,所以 setPrimaryItem 不會被觸發,AA 的選單仍然可見,而 BA 的選單則仍然不可見。
幸運的是把修改兩個地方合併起來,這樣就覆蓋了各種可能了。但未免過於繁瑣,把問題重新整理一遍,建立模型,才是優雅的解決方法:
- 給介面卡(Adapter)引入是否可視(visible)屬性,不再是主項(primary item)的 Fragment 就顯示選單,而是隻有當前介面卡是可視的情況下才可以顯示選單。
- 是否可視的遞迴定義:父介面卡(管理宿主 Fragment 的 Adapter)是可視的 ,且宿主 Fragment 是主項,介面卡才是可視的。
- 父介面卡的可視狀態和宿主 Fragment 主項狀態發生改變,要傳遞到其子介面卡。
- 子介面卡初始化要正確初始化他的可視狀態。
這樣就能設計一個新的 PagerAdapter,把棘手的問題都放在 PagerAdapter 來做。
要實現第三點,介面卡需要獲得指向其子介面卡的引用,介面卡是 Fragments 的管理者,這些 Fragments 又是子介面卡的宿主,只要讓 Fragment 實現介面來獲取其內部的介面卡便行。
public interface AdapterHolder {
HierarchyFragmentPagerAdapter getAdapter();
}
/**
* 通知子 Adapter(宿主是 holder) ,父 Adapter(當前的 Adapter) visible 發生了變化。
* 或者通知子 Adapter,父 Adapter 希望它的 visible 發生變化
*/
private void notifyChildVisibleChanged(boolean visible, Fragment holder) {
if (holder instanceof AdapterHolder) {
HierarchyFragmentPagerAdapter adapter = ((AdapterHolder) holder).getAdapter();
if (adapter != null) {
adapter.setVisible(visible);
}
}
}複製程式碼
沒有繼承 AdapterHolder 都會被介面卡當成沒有子 Adapter。
相比第三點,第四點反而更麻煩。在 finishUpdate 之前,Fragment 是不知道它在樹中的位置的。這時如果嘗試用 getParentFragment()
是返回空,
Fragment parent = fragment.getParentFragment();
if (parent == null || !(parent instanceof AdapterHolder)) {
// 拿不到 parent 有兩種情況
// Adapter 在根 Pager 裡
// 也有可能是第一次初始化,當前 Fragment 還未和其父 Fragment 建立連結
setVisible(isVisible());
} else {
// 否則,只有父 Adapter 是 visible primary,當前 primary item 才可能是 visible primary.
setVisible(((AdapterHolder) parent).getAdapter().isVisible());
}複製程式碼
為了能夠正確初始化,需要在建構函式做個 hack。
public HierarchyFragmentPagerAdapter(PagerAdapter adapter, AdapterHolder holder) {
mAdapter = adapter;
mVisible = true;
if (holder != null) {
if (holder instanceof Fragment) {
// 一個 hack,初始化 的 visible 狀態
// holder 不是 Fragment 表示 Adapter 為根 Adapter
// menu visible 為 true,便斷言宿主 Fragment 是 primary item.
mVisible = ((Fragment) holder).isMenuVisible();
}
}
}複製程式碼
剩下的便沒什麼,鑑於 PagerAdapter 有兩個, FragmentPagerAdapter 和 FragmentStatePagerAdapter。所以介面卡的設計便用代理模式比較合適,實現起來比想象中的簡潔,用起來也簡單,只需將實際的 PagerAdapter 外面包一層 HierarchyFragmentPagerAdapter
就行,具體的程式碼見:HierarchyFragmentPagerAdapter