从源码解析ViewPager动态更改Fragment的实现

从源码解析ViewPager动态更改Fragment的实现

需求背景(What)

项目中有个需求的实现,详情页中有两个Tab(概览、数据详情),概览页根据业务类型不同,显示不同的UI。并且详情可修该业务类型,并且动态更换掉概览页面。抽象出来就是ViewPager中包含AFragment、BFragment,当业务类型在ViewPager显示时被更改,需要把AFragment替换成CFragment。

问题(Why)

原本可供ViewPager使用的Adapter有FragmentPagerAdapter与FragmentStatePagerAdapter,前者和后者的区别是前者类内的每一个生成的 Fragment 都将保存在内存之中,后者只保留当前页面,当页面离开视线后,就会被消除,释放其资源。而我们系统在销毁前,会把Fragment的Bundle在我们的onSaveInstanceState(Bundle)保存下来;而在页面需要显示时,Fragment就会根据我们的savedInstanceState生成新的页面。

所以跟据以上分析,大部分场景FragmentPagerAdapter已经可以满足我们的需求了,满心欢喜的使用该Adapter,在需要动态改变的地方调用notificationDataChange()方法,期待结果就像ListView、RecyclerView一样,刷新出来新的UI。

但是......依然显示AFragment,纹丝不动。

解决方法(how)

没办法找资料,看FragmentPagerAdapter源码

@NonNull
public Object instantiateItem(@NonNull ViewGroup container, int position) {
    if (this.mCurTransaction == null) {
        this.mCurTransaction = this.mFragmentManager.beginTransaction();
    }
    //获取itemId 默认是fragment的position
    long itemId = this.getItemId(position);
    String name = makeFragmentName(container.getId(), itemId);
    //通过tag获取Fragment
    Fragment fragment = this.mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        this.mCurTransaction.attach(fragment);
    } else {
        fragment = this.getItem(position);
        //添加Fragment,并标记tag = "android:switcher:" + viewId + ":" + id
        this.mCurTransaction.add(container.getId(), fragment, 
                                 makeFragmentName(container.getId(), itemId));
    }

    if (fragment != this.mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}
//省略部分代码......
public long getItemId(int position) {
    return (long)position;
}

private static String makeFragmentName(int viewId, long id) {
    return "android:switcher:" + viewId + ":" + id;
}

通过阅读这部分源码可以知道,FragmentPagerAdapter默认提供的getItemId是position,因此就算调用notificationDataChange(),第一个Fragment改成CFragment但itemId依旧是0,然后通过tag获取到的Fragment依旧还是AFragment,所以我们在自己实现FragmentPagerAdapter时需要实现getItemId方法,保证每个Fragment的itemid不重复,我用的是:

@override
public long getItemId(int position) {
    return mFragmentList.get(position).hashCode();
}

改变此处应该可以了吧,跑了一遍,依然纹丝不动......慌了,这怎么回事

定定神,接着看源码。instantiateItem是什么时候调用的呢?它是抽象类PagerAdapter 中定义的,PagerAdapter文件头注视中有段话:

PagerAdapter supports data set changes. Data set changes must occur on the
main thread and must end with a call to {@link #notifyDataSetChanged()} similar
to AdapterView adapters derived from {@link android.widget.BaseAdapter}. A data
set change may involve pages being added, removed, or changing position. The
ViewPager will keep the current page active provided the adapter implements the method {@link #getItemPosition(Object)}.

再看看getItemPosition方法

   /**
    * Called when the host view is attempting to determine if an item's position
    * has changed. Returns {@link #POSITION_UNCHANGED} if the position of the given
    * item has not changed or {@link #POSITION_NONE} if the item is no longer present
    * in the adapter.
    *
    * <p>The default implementation assumes that items will never
    * change position and always returns {@link #POSITION_UNCHANGED}.
    *
    * @param object Object representing an item, previously returned by a call to
    *               {@link #instantiateItem(android.view.View, int)}.
    * @return object's new position index from [0, {@link #getCount()}),
    *         {@link #POSITION_UNCHANGED} if the object's position has not changed,
    *         or {@link #POSITION_NONE} if the item is no longer present.
    */
    public int getItemPosition(Object object) {
        return POSITION_UNCHANGED;
    }

从注释看,原来系统默认使用POSITION_UNCHANGED表示item的位置不变化;返回object新的index值用来更新位置,这个可以用来动态增减item用;返回POSITION_NONE则表示这个item不再出现,所以对于我们目前固定两个Fragment,只替换第一个位置的场景需要使用POSITION_NONE。如此信心大增,只要在自己实现FragmentPagerAdapter时实现getItemPosition方法。

最终我的Adapter实现是:

mViewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {
    @Override
    public Fragment getItem(int position) {
        return mFragments.get(position);
    }

    @Override
    public int getCount() {
        return mFragments.size();
    }

    @Override
    public int getItemPosition(@NonNull Object object) {
        return POSITION_NONE;
    }

    @Override
    public long getItemId(int position) {
        return mFragments.get(position).hashCode();
    }
});

如此终于,需求实现了。但是,依然没有说明上面提到instantiateItem是什么时候调用的。

PagerAdapter类中,第一行代码就是

 private DataSetObservable mObservable = new DataSetObservable();

一看就知道使用了观察者模式,这是被观察对象,也就是Adapter对应的数据集,下面找到了观察者的注册方法

 //省略其他代码......
 /**
  * This method should be called by the application if the data backing this adapter has changed and associated views should update.
  */
   public void notifyDataSetChanged() {
       mObservable.notifyChanged();
   }

 /**
   * Register an observer to receive callbacks related to the adapter's data      changing.
   *
   * @param observer The {@link android.database.DataSetObserver} which will receive callbacks.
   */
   public void registerDataSetObserver(DataSetObserver observer) {
       mObservable.registerObserver(observer);
   }

   /**
     * Unregister an observer from callbacks related to the adapter's data changing.
     *
     * @param observer The {@link android.database.DataSetObserver} which will be unregistered.
     */
    public void unregisterDataSetObserver(DataSetObserver observer) {
        mObservable.unregisterObserver(observer);
    }
//省略其他代码......

PagerAdapter的实现类是供ViewPager调用的,所以再看ViewPager代码,果然在其中是有PagerAdapter,以下是ViewPager部分代码,觉得太过冗长可以跳过看结论。

//省略其他代码......
private PagerAdapter mAdapter;

//省略其他代码......
/**
 * Set a PagerAdapter that will supply views for this pager as needed.
 *
 * @param adapter Adapter to use
 */
public void setAdapter(PagerAdapter adapter) {
    if (mAdapter != null) {
        //解除之前的监听
        mAdapter.unregisterDataSetObserver(mObserver);
        mAdapter.startUpdate(this);
        for (int i = 0; i < mItems.size(); i++) {
            final ItemInfo ii = mItems.get(i);
            mAdapter.destroyItem(this, ii.position, ii.object);
        }
        mAdapter.finishUpdate(this);
        mItems.clear();
        removeNonDecorViews();
        mCurItem = 0;
        scrollTo(0, 0);
    }

    final PagerAdapter oldAdapter = mAdapter;
    mAdapter = adapter;
    mExpectedAdapterCount = 0;

    if (mAdapter != null) {
        if (mObserver == null) {
            mObserver = new PagerObserver();
        }
        //重新注册监听
        mAdapter.registerDataSetObserver(mObserver);
        mPopulatePending = false;
        final boolean wasFirstLayout = mFirstLayout;
        mFirstLayout = true;
        mExpectedAdapterCount = mAdapter.getCount();
        if (mRestoredCurItem >= 0) {
            mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
            setCurrentItemInternal(mRestoredCurItem, false, true);
            mRestoredCurItem = -1;
            mRestoredAdapterState = null;
            mRestoredClassLoader = null;
        } else if (!wasFirstLayout) {
            populate();
        } else {
            requestLayout();
        }
    }

    if (mAdapterChangeListener != null && oldAdapter != adapter) {
        mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
    }
}
//省略其他代码......
//内部类
private class PagerObserver extends DataSetObserver {
    @Override
    public void onChanged() {
        dataSetChanged();
    }
    @Override
    public void onInvalidated() {
        dataSetChanged();
    }
}
//省略其他代码......
void dataSetChanged() {
    // This method only gets called if our observer is attached, so mAdapter is non-null.
    final int adapterCount = mAdapter.getCount();
    mExpectedAdapterCount = adapterCount;
    boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 &&
            mItems.size() < adapterCount;
    int newCurrItem = mCurItem;
    boolean isUpdating = false;
    for (int i = 0; i < mItems.size(); i++) {
        final ItemInfo ii = mItems.get(i);
        //获取当前新数据item的position
        final int newPos = mAdapter.getItemPosition(ii.object);
        //这是关键!!!
        if (newPos == PagerAdapter.POSITION_UNCHANGED) {
            continue;
        }
         //这是关键!!!
        if (newPos == PagerAdapter.POSITION_NONE) {
            mItems.remove(i);
            i--;
            if (!isUpdating) {
                mAdapter.startUpdate(this);
                isUpdating = true;
            }
            mAdapter.destroyItem(this, ii.position, ii.object);
            needPopulate = true;
            if (mCurItem == ii.position) {
                // Keep the current item in the valid range
                newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
                needPopulate = true;
            }
            continue;
        }
        if (ii.position != newPos) {
            if (ii.position == mCurItem) {
                // Our current item changed position. Follow it.
                newCurrItem = newPos;
            }
            ii.position = newPos;
            needPopulate = true;
        }
    }
   //省略其他代码......
}
 

综上,当我们调用FragmentPagerAdapter的notifyDataSetChanged方法时,PagerAdapter的notifyDataSetChanged也会执行,由于观察者模式,它也会通知观察者ViewPager中的PagerObserver,继续调用ViewPager的dataSetChanged方法。这个方法中,当mAdapter.getItemPosition(object)拿到的position是POSITION_UNCHANGED时,什么都没变化;是POSITION_NONE才会移除当前位置旧的item对象,换上新的;其它不等于position的(增加、删除item)也会处理。

总结

我们通过源码FragmentPagerAdapter、PagerAdapter、ViewPager终于弄清楚了更新ViewPager的方法,以及为什么之前我们不能成功更新页面的原因。当我们需要实现这样ViewPager中Fragment数量不变,改变实体对象的时候,我们需要自己重新覆盖FragmentPagerAdapter的getItemPosition和getItemId两个方法。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容