叨叨ViewPager那些事兒(一)

前言

問ViewPager為何物魏烫?
谷歌文檔有云'Layout manager that allows the user to flip left and right through pages of data.'
提供左右切換功能的布局控制器是也
既然是控制器涂屁,必定要Adapter相以輔助,繼續(xù)翻閱文檔,只見
有PagerAdapter审胚,正是'Base class providing the adapter to populate pages inside of a ViewPager'
填充ViewPager內(nèi)部頁面數(shù)據(jù)的基類適配器是也

路子都引出來了委造,自然沿著文檔和源碼步步深入,一探究竟


從使用說起 先談適配器

谷歌很直接
When you implement a PagerAdapter, you must override the following methods at minimum:
要想實現(xiàn)PagerAdapter,必須覆寫以下四個方法:
1.instantiateItem(ViewGroup container, int position):為容器指定位置創(chuàng)建頁面
2.destroyItem(ViewGroup container, int position, Object object):銷毀容器指定位置頁面
3.getCount():返回容器內(nèi)有效頁面數(shù)量
4.isViewFromObject(View view, Object object):判斷頁面視圖與instantiateItem返回元素是否為同一視圖

  • 一小段示例

private class MyPagerAdapter extends PagerAdapter {
        @Override
        public int getCount() {
            return  dataList== null ? 0 : dataList.size();
        }

        @Override
        public Object instantiateItem(ViewGroup container, int pos) {
            MyPageItem item = new MyPageItem ();           
            container.addView(item.mRootView);
            return item;
        }

        @Override
        public void destroyItem(ViewGroup container, int pos, Object o) {
            MyPageItem item = (MyPageItem) o;
            container.removeView(item.mRootView);
        }

        @Override
        public boolean isViewFromObject(View view, Object o) {
            MyPageItem item = (MyPageItem) o;
            return view == item.mRootView;
        }
    }
  • 關(guān)于刷新的“坑”

ViewPager一個眾所周知的問題--數(shù)據(jù)源發(fā)生變化后調(diào)用 notifyDataSetChanged()团滥,視圖并不會立即刷新
雖然是谷歌為節(jié)省資源開銷竿屹,為ViewPager承載大圖的特點專門設(shè)計,初次碰到也著實犯難灸姊。

從源碼看起拱燃,一步一問

首先,直奔ViewPager的適配器--PagerAdapter力惯,查看notifyDataSetChanged方法

//PagerAdapter
public void notifyDataSetChanged() {
        mObservable.notifyChanged();
    }
//DataSetObservable
public void notifyChanged() {
        synchronized(mObservers) {           
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }

好嘛碗誉,還是熟悉的觀察者模式,回調(diào)onChanged方法
那ViewPager中是如何定義Observer類的呢父晶?

private class PagerObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            dataSetChanged();
        }
        @Override
        public void onInvalidated() {
            dataSetChanged();
        }
    }

矛頭都指向dataSetChanged()哮缺,看來刷新玄機暗藏其中
這里暫時只留下解決刷新疑問的相關(guān)代碼

void dataSetChanged() {
        ··· ···
        //遍歷容器中的元素
        for (int i = 0; i < mItems.size(); i++) {
            final ItemInfo ii = mItems.get(i);

            // 返回元素相應(yīng)位置是否發(fā)生變化的標(biāo)志
            // POSITION_UNCHANGED = -1;
            // POSITION_NONE = -2; 
            final int newPos = mAdapter.getItemPosition(ii.object);
            // 若返回POSITION_UNCHANGED,跳過
            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
                continue;
            }

            if (newPos == PagerAdapter.POSITION_NONE) {
                // 返回POSITION_NONE時移除元素并記錄標(biāo)志
                // 這里對元素先移除甲喝,后重新加載
                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;
            }
            ··· ···
            // 重新加載來了
            setCurrentItemInternal(newCurrItem, false, true);
            requestLayout();
    }

看來尝苇,是否更新,由getItemPosition的返回值決定啊埠胖,那getItemPosition方法內(nèi)部又是怎樣實現(xiàn)的呢糠溜?

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

沒錯,默認返回POSITION_UNCHANGED直撤,原來如此非竿!
那我們重寫getItemPosition方法,讓其返回POSITION_NONE刷新不就能生效了嘛谋竖?
答案是肯定的红柱。
但同時要注意,全部返回POSITION_NONE意味著要刷新所有元素蓖乘,是灰常浪費資源的锤悄,畢竟谷歌這么設(shè)計也總有道理。
一個稍加優(yōu)化的思路是初始化時為頁面設(shè)置tag驱敲,在getItemPosition方法中根據(jù)tag判斷僅更新當(dāng)前頁面視圖

@Override
public int getItemPosition(Object object) {
            MyPageItem v = (MyPageItem) object;
            if (v == null || v.mRoot == null){
                return POSITION_UNCHANGED;
            }
            int position = (int) v.mRootView.getTag();
            return mCurPageIndex == position ? POSITION_NONE : POSITION_UNCHANGED;
        }

說回ViewPager

  • 我們繼續(xù)一步一問铁蹈,提出幾個重要方法一探。
    先看看instantiateItem在ViewPager中的調(diào)用
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;
    }

嗯众眨,添加新元素握牧,顧名可以思義。但是該方法在何處調(diào)用娩梨,返回值又怎么使用呢沿腰?


都是在populate中調(diào)用,看來是個很厲害的方法了狈定!

    void populate(int newCurrentItem) {
        ItemInfo oldCurInfo = null;
        int focusDirection = View.FOCUS_FORWARD;
        if (mCurItem != newCurrentItem) {
            focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
            // 獲取舊元素信息
            oldCurInfo = infoForPosition(mCurItem);
            // 更新當(dāng)前視圖index
            mCurItem = newCurrentItem;
        }

        if (mAdapter == null) {
            // 視圖位置重排
            sortChildDrawingOrder();
            return;
        }

        // 若滑動未停止則暫停populate操作防止出現(xiàn)問題
        if (mPopulatePending) {
            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
            sortChildDrawingOrder();
            return;
        }

        // 若視圖未依附于窗口則暫停populate操作
        if (getWindowToken() == null) {
            return;
        }

        mAdapter.startUpdate(this);
        // mOffscreenPageLimit為設(shè)定的預(yù)加載數(shù)颂龙,具體下邊說
        // 根據(jù)當(dāng)前視圖位置和預(yù)加載數(shù)計算填充位置的起始點和終結(jié)點
        final int pageLimit = mOffscreenPageLimit;
        final int startPos = Math.max(0, mCurItem - pageLimit);
        final int N = mAdapter.getCount();
        final int endPos = Math.min(N-1, mCurItem + pageLimit);

        if (N != mExpectedAdapterCount) {
            String resName;
            try {
                resName = getResources().getResourceName(getId());
            } catch (Resources.NotFoundException e) {
                resName = Integer.toHexString(getId());
            }
            throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
                    " contents without calling PagerAdapter#notifyDataSetChanged!" +
                    " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
                    " Pager id: " + resName +
                    " Pager class: " + getClass() +
                    " Problematic adapter: " + mAdapter.getClass());
        }

        // 在內(nèi)存中定位所需視圖元素习蓬,若不存在則重新添加
        int curIndex = -1;
        ItemInfo curItem = null;
        for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
            final ItemInfo ii = mItems.get(curIndex);
            if (ii.position >= mCurItem) {
                if (ii.position == mCurItem) curItem = ii;
                break;
            }
        }

        if (curItem == null && N > 0) {
            // 終于看到了addNewItem,若當(dāng)前需填充的元素不在內(nèi)存中則通過addNewItem調(diào)用instantiateItem加載
            curItem = addNewItem(mCurItem, curIndex);
        }

        // Fill 3x the available width or up to the number of offscreen
        // pages requested to either side, whichever is larger.
        // If we have no current item we have no work to do.
        if (curItem != null) {
            float extraWidthLeft = 0.f;
            // 當(dāng)前視圖左邊的元素
            int itemIndex = curIndex - 1;
            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            final int clientWidth = getClientWidth();
            // 計算左側(cè)預(yù)加載視圖寬度
            final float leftWidthNeeded = clientWidth <= 0 ? 0 :
                    2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
            // 遍歷當(dāng)前視圖左邊的所有元素
            for (int pos = mCurItem - 1; pos >= 0; pos--) {
                // 若該元素在預(yù)加載范圍外
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                    if (ii == null) {
                        break;
                    }
                    // 移除該頁面元素
                    if (pos == ii.position && !ii.scrolling) {
                        mItems.remove(itemIndex);
                        mAdapter.destroyItem(this, pos, ii.object);
                        if (DEBUG) {
                            Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
                                    " view: " + ((View) ii.object));
                        }
                        itemIndex--;
                        curIndex--;
                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                    }
                } else if (ii != null && pos == ii.position) {
                    // 若該左側(cè)元素在內(nèi)存中措嵌,則更新記錄
                    extraWidthLeft += ii.widthFactor;
                    itemIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                } else {
                    // 若該左側(cè)元素不在內(nèi)存中躲叼,則重新添加,再一次來到了addNewItem
                    ii = addNewItem(pos, itemIndex + 1);
                    extraWidthLeft += ii.widthFactor;
                    curIndex++;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            }
            // 來到當(dāng)前視圖右側(cè)企巢,思路大致和左側(cè)相同
            float extraWidthRight = curItem.widthFactor;
            itemIndex = curIndex + 1;
            if (extraWidthRight < 2.f) {
                ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                final float rightWidthNeeded = clientWidth <= 0 ? 0 :
                        (float) getPaddingRight() / (float) clientWidth + 2.f;
                for (int pos = mCurItem + 1; pos < N; pos++) {
                    if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
                        if (ii == null) {
                            break;
                        }
                        if (pos == ii.position && !ii.scrolling) {
                            mItems.remove(itemIndex);
                            mAdapter.destroyItem(this, pos, ii.object);
                            if (DEBUG) {
                                Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
                                        " view: " + ((View) ii.object));
                            }
                            ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                        }
                    } else if (ii != null && pos == ii.position) {
                        extraWidthRight += ii.widthFactor;
                        itemIndex++;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    } else {
                        ii = addNewItem(pos, itemIndex);
                        itemIndex++;
                        extraWidthRight += ii.widthFactor;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    }
                }
            }
            // 計算頁面偏移量
            calculatePageOffsets(curItem, curIndex, oldCurInfo);
        }

        if (DEBUG) {
            Log.i(TAG, "Current page list:");
            for (int i=0; i<mItems.size(); i++) {
                Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
            }
        }

        mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);

        mAdapter.finishUpdate(this);

        // 遍歷子視圖枫慷,若寬度不合法則重繪
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            lp.childIndex = i;
            if (!lp.isDecor && lp.widthFactor == 0.f) {
                // 0 means requery the adapter for this, it doesn't have a valid width.
                final ItemInfo ii = infoForChild(child);
                if (ii != null) {
                    lp.widthFactor = ii.widthFactor;
                    lp.position = ii.position;
                }
            }
        }
        sortChildDrawingOrder();

        if (hasFocus()) {
            View currentFocused = findFocus();
            ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
            if (ii == null || ii.position != mCurItem) {
                for (int i=0; i<getChildCount(); i++) {
                    View child = getChildAt(i);
                    ii = infoForChild(child);
                    if (ii != null && ii.position == mCurItem) {
                        if (child.requestFocus(focusDirection)) {
                            break;
                        }
                    }
                }
            }
        }
    }

方法較長,理解的甚為粗淺浪规,還望大神指點或听。

  • 另外,作為支持“左右切換功能”的布局管理器笋婿,谷歌也為其配套提供了“預(yù)加載”機制誉裆,防止下一頁內(nèi)容加載不及時影響體驗。
    設(shè)置預(yù)加載數(shù)量viewPager.setOffscreenPageLimit(2)(括號內(nèi)數(shù)字代表當(dāng)前元素左右兩邊各需預(yù)加載的數(shù)量)
    源碼內(nèi)部是這樣定義setOffscreenPageLimit方法的缸濒,詳見注釋足丢。
// DEFAULT_OFFSCREEN_PAGES = 1 默認預(yù)加載數(shù)為1
public void setOffscreenPageLimit(int limit) {
        // 若用戶設(shè)置的預(yù)加載數(shù)量小于1,則重置為默認值
        if (limit < DEFAULT_OFFSCREEN_PAGES) {
            Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +
                    DEFAULT_OFFSCREEN_PAGES);
            limit = DEFAULT_OFFSCREEN_PAGES;
        }
        if (limit != mOffscreenPageLimit) {
            // 設(shè)定預(yù)加載數(shù)绍填,填充頁面視圖
            mOffscreenPageLimit = limit;
            populate();
        }
    }

最后

ViewPager玄機深霎桅,本文僅僅窺一斑栖疑。往后會繼續(xù)叨叨ViewPager(二)讨永,理解越深用著越順。進擊遇革!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末卿闹,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子萝快,更是在濱河造成了極大的恐慌锻霎,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件揪漩,死亡現(xiàn)場離奇詭異旋恼,居然都是意外死亡,警方通過查閱死者的電腦和手機奄容,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門冰更,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人昂勒,你說我怎么就攤上這事蜀细。” “怎么了戈盈?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵奠衔,是天一觀的道長谆刨。 經(jīng)常有香客問我,道長归斤,這世上最難降的妖魔是什么痊夭? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮脏里,結(jié)果婚禮上生兆,老公的妹妹穿的比我還像新娘。我一直安慰自己膝宁,他們只是感情好鸦难,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著员淫,像睡著了一般合蔽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上介返,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天拴事,我揣著相機與錄音,去河邊找鬼圣蝎。 笑死刃宵,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的徘公。 我是一名探鬼主播牲证,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼关面!你這毒婦竟也來了坦袍?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤等太,失蹤者是張志新(化名)和其女友劉穎捂齐,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缩抡,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡奠宜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了瞻想。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片压真。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖内边,靈堂內(nèi)的尸體忽然破棺而出榴都,到底是詐尸還是另有隱情,我是刑警寧澤漠其,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布嘴高,位于F島的核電站竿音,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏拴驮。R本人自食惡果不足惜春瞬,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望套啤。 院中可真熱鬧宽气,春花似錦、人聲如沸潜沦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽唆鸡。三九已至涝影,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間争占,已是汗流浹背燃逻。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留臂痕,地道東北人伯襟。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像握童,于是被迫代替她去往敵國和親姆怪。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容