FragmentStatePagerAdapter支持動態(tài)更新數(shù)據(jù)

在Android開發(fā)中,我們應(yīng)該使用到很高頻率的一個控件就是ViewPager乌逐。但是在使用ViewPager的過程中擎椰,我們會發(fā)現(xiàn)有兩個問題,一是不能關(guān)閉預(yù)加載钟哥;二是更新ViewPager的Adapter不生效迎献。所以我在這里以FragmentStatePagerAdapter為例,探討一下為什么更新adapter無法生效腻贰,并且提出解決方案吁恍。

為什么Adapter更新不生效

更新不生效其實很簡單,我們看一下源碼的調(diào)用過程
PagerAdapter.java

 //  PagerAdapter.java
  /**
     * This method should be called by the application if the data backing this adapter has changed
     * and associated views should update.
     */
    public void notifyDataSetChanged() {
        synchronized (this) {
            if (mViewPagerObserver != null) {
                mViewPagerObserver.onChanged();
            }
        }
        mObservable.notifyChanged();
    }

ViewPager.java

// ViewPager.java
    private class PagerObserver extends DataSetObserver {
        PagerObserver() {
        }

        @Override
        public void onChanged() {
            dataSetChanged();
        }
        @Override
        public void onInvalidated() {
            dataSetChanged();
        }
    }

// 接著調(diào)用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);
            // 調(diào)用adapter的getItemPosition方法播演,獲取新位置
            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;
            }
        }

        if (isUpdating) {
            mAdapter.finishUpdate(this);
        }

        Collections.sort(mItems, COMPARATOR);

        if (needPopulate) {
            // Reset our known page widths; populate will recompute them.
            final int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                if (!lp.isDecor) {
                    lp.widthFactor = 0.f;
                }
            }

            setCurrentItemInternal(newCurrItem, false, true);
            requestLayout();
        }
    }

PageAdapter.getItemPosition(ii.object)

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

默認(rèn)是返回POSITION_UNCHANGED并徘,所以結(jié)合上面的邏輯

    if (newPos == PagerAdapter.POSITION_UNCHANGED) {
         continue;
    }

這里跳出循環(huán),所以其實什么事情都沒有做盛杰,也沒辦法因為數(shù)據(jù)集的變更而變更UI禽捆。

如何解決

其實ViewPager.dataSetChanged()方法已經(jīng)為我們預(yù)留了更新的邏輯。

......

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) {
               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;
           }
           // 這里的newPos就是上面從PagerAdapter.getItemPosition()獲取到的位置信息
           if (ii.position != newPos) {
               if (ii.position == mCurItem) {
                   // Our current item changed position. Follow it.
                   newCurrItem = newPos;
               }

               ii.position = newPos;
               needPopulate = true;
           }
       }

......

所以我們要跳過newPos == PagerAdapter.POSITION_UNCHANGEDnewPos == PagerAdapter. POSITION_NONE 的邏輯顶霞,需要對PagerAdapter.getItemPosition()進行改造肄程。

復(fù)制FragmentStatePagerAdapter源碼

由于改造getItemPosition()方法需要對源碼進行操作锣吼,所以我們首先需要復(fù)制一份源碼,暫時叫DynamicFragmentStatePagerAdapter

改造getItemPosition

@Override
    public int getItemPosition(@NonNull Object object) {
        int index = indexOfFragments(object);
        return index != -1 ? index : super.getItemPosition(object);
    }

    private int indexOfFragments(Object object) {
        if (object instanceof Fragment) {
            return mFragments.indexOf(object);
        }
        return -1;
    }

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(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.
*/
簡單來說就是就是根據(jù)這個方法玄叠,來判斷當(dāng)前item的位置是否發(fā)生了改變,返回值包括POSITION_UNCHANGED拓提、POSITION_NONE读恃、以及[0, {@link #getCount()}。所以當(dāng)我們數(shù)據(jù)集發(fā)生變化的時候代态,其實fragment的位置寺惫,也要相應(yīng)的發(fā)生變化否則就會在錯誤的位置獲取到錯誤的頁面,會發(fā)生頁面顯示錯亂蹦疑。
mFragments對象是用來緩存當(dāng)前adapter維護的在內(nèi)存中的fragment緩存對象西雀。是一個數(shù)組結(jié)構(gòu),數(shù)組會隨著數(shù)據(jù)集的增大而增大歉摧,數(shù)組的內(nèi)容是fragment對象艇肴,會根據(jù)當(dāng)前viewpager位置,保存的ViewPager.setOffscreenPageLimit(size)中size的大小的實例叁温,ViewPager進行滑動時再悼,超出page limit的頁面會被執(zhí)行destroyItem方法,并且預(yù)加載的fragment會執(zhí)行instantiateItem方法膝但,確保adapter中只保留相應(yīng)數(shù)量的fragment實例冲九。這里不展開說,可以詳細(xì)看下FragmentStatePagerAdapter.instantiateItem(@NonNull ViewGroup container, int position)的方法源碼跟束。
mSavedState對象用來緩存adapter中所有fragment執(zhí)行onSaveInstanceState()之后的數(shù)據(jù)莺奸,在destroyItem方法中調(diào)用被銷毀的fragment的onSaveInstanceState方法,并把數(shù)據(jù)放在mSavedState對應(yīng)的位置泳炉。隨后在執(zhí)行instantiateItem的時候憾筏,首先從mFragments找相應(yīng)位置的fragment,并且找到mSavedState相應(yīng)位置的state數(shù)據(jù)花鹅,進行頁面的恢復(fù)氧腰。

有了上面的這兩個知識點,我們就知道接下來要怎么做刨肃,首先我們在給fragment確定位置的時候古拴,也就是在getItemPosition方法中,我們根據(jù)當(dāng)前的fragment對象在mFragments確定新的位置真友。如果位置發(fā)生變化黄痪,則會重新刷新。代碼如下:

    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);
            final int newPos = mAdapter.getItemPosition(ii.object);

            ......
            ......
            
            // 這里發(fā)現(xiàn)位置和原來的位置不一樣盔然,說明發(fā)生了變化桅打,
            if (ii.position != newPos) {
                if (ii.position == mCurItem) {
                    // Our current item changed position. Follow it.
                    newCurrItem = newPos;
                }

                ii.position = newPos;
                needPopulate = true;
            }
        }

        if (isUpdating) {
            mAdapter.finishUpdate(this);
        }

        Collections.sort(mItems, COMPARATOR);
        // 這里會做刷新操作是嗜,設(shè)置新的current item位置
        if (needPopulate) {
            // Reset our known page widths; populate will recompute them.
            final int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                if (!lp.isDecor) {
                    lp.widthFactor = 0.f;
                }
            }

            setCurrentItemInternal(newCurrItem, false, true);
            requestLayout();
        }
    }

所以一切的根源,就是在數(shù)據(jù)集發(fā)生變化時挺尾,需要通知對應(yīng)位置的** mFragmentsmSavedState**對象鹅搪,插入null對象。

舉例

如果我們要在數(shù)據(jù)集的頭部插入數(shù)據(jù)遭铺,我們可以這么做丽柿,在copy源碼的DynamicFragmentStatePagerAdapter類中加入我們的自定義方法,例如:

    public void insertEmptyHeaderFragment() {
        mFragments.add(0, null);
        mSavedState.add(0, null);
    }

在數(shù)據(jù)集插入單個數(shù)據(jù)的同時魂挂,也在該方法中插入對應(yīng)的null甫题,這樣位置和值才能對的上,以至于恢復(fù)頁面的時候涂召,不會找錯save state而在對的位置產(chǎn)生錯誤的頁面坠非。舉例

    fun insertData(data: XXX) {
         dataSet?.add(0, data)
         insertEmptyHeaderFragment()
    }

插入其他位置,我相信對同學(xué)們應(yīng)該也不難了芹扭。記得數(shù)據(jù)集更新后要調(diào)用adapter.notifyDataSetChanged()方法麻顶。這樣就會去刷新數(shù)據(jù)集了。

總結(jié)

重點是改變getItemPosition()的位置計算舱卡,并且在更新數(shù)據(jù)集的時候,更新mFragments和mSavedState的位置队萤。本文是根據(jù)使用ViewPager+FragmentStatePagerAdapter來舉例轮锥,如果是其他adapter類,相信你經(jīng)過上面的介紹之后要尔,應(yīng)該不是什么難事舍杜。最后就是,遇到困難不用怕赵辕,分析既绩,跟蹤源碼,不能通過繼承實現(xiàn)的还惠,就copy源碼來改饲握,只要思想不滑坡,方法總比困難多蚕键。歡迎有不清楚的同學(xué)救欧,可以線下持續(xù)交流。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锣光,一起剝皮案震驚了整個濱河市笆怠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌誊爹,老刑警劉巖蹬刷,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瓢捉,死亡現(xiàn)場離奇詭異,居然都是意外死亡办成,警方通過查閱死者的電腦和手機泡态,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诈火,“玉大人兽赁,你說我怎么就攤上這事±涫兀” “怎么了刀崖?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拍摇。 經(jīng)常有香客問我亮钦,道長,這世上最難降的妖魔是什么充活? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任蜂莉,我火速辦了婚禮,結(jié)果婚禮上混卵,老公的妹妹穿的比我還像新娘映穗。我一直安慰自己,他們只是感情好幕随,可當(dāng)我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布蚁滋。 她就那樣靜靜地躺著,像睡著了一般赘淮。 火紅的嫁衣襯著肌膚如雪辕录。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天梢卸,我揣著相機與錄音走诞,去河邊找鬼。 笑死蛤高,一個胖子當(dāng)著我的面吹牛蚣旱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播襟齿,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼姻锁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了猜欺?” 一聲冷哼從身側(cè)響起位隶,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎开皿,沒想到半個月后涧黄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體篮昧,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年笋妥,在試婚紗的時候發(fā)現(xiàn)自己被綠了懊昨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡春宣,死狀恐怖酵颁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情月帝,我是刑警寧澤躏惋,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站嚷辅,受9級特大地震影響簿姨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜簸搞,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一扁位、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧趁俊,春花似錦域仇、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至沽讹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間武鲁,已是汗流浹背爽雄。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留沐鼠,地道東北人挚瘟。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像乘盖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子憔涉,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,452評論 2 348