叨叨ViewPager那些事儿(一)

前言

问ViewPager为何物?
谷歌文档有云'Layout manager that allows the user to flip left and right through pages of data.'
提供左右切换功能的布局控制器是也
既然是控制器,必定要Adapter相以辅助,继续翻阅文档,只见
有PagerAdapter,正是'Base class providing the adapter to populate pages inside of a ViewPager'
填充ViewPager内部页面数据的基类适配器是也

路子都引出来了,自然沿着文档和源码步步深入,一探究竟


从使用说起 先谈适配器

谷歌很直接
When you implement a PagerAdapter, you must override the following methods at minimum:
要想实现PagerAdapter,必须覆写以下四个方法:
1.instantiateItem(ViewGroup container, int position):为容器指定位置创建页面
2.destroyItem(ViewGroup container, int position, Object object):销毁容器指定位置页面
3.getCount():返回容器内有效页面数量
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;
        }
    }
  • 关于刷新的“坑”

ViewPager一个众所周知的问题--数据源发生变化后调用 notifyDataSetChanged(),视图并不会立即刷新
虽然是谷歌为节省资源开销,为ViewPager承载大图的特点专门设计,初次碰到也着实犯难。

从源码看起,一步一问

首先,直奔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();
            }
        }
    }

好嘛,还是熟悉的观察者模式,回调onChanged方法
那ViewPager中是如何定义Observer类的呢?

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

矛头都指向dataSetChanged(),看来刷新玄机暗藏其中
这里暂时只留下解决刷新疑问的相关代码

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

            // 返回元素相应位置是否发生变化的标志
            // 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时移除元素并记录标志
                // 这里对元素先移除,后重新加载
                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方法内部又是怎样实现的呢?

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

没错,默认返回POSITION_UNCHANGED,原来如此!
那我们重写getItemPosition方法,让其返回POSITION_NONE刷新不就能生效了嘛?
答案是肯定的。
但同时要注意,全部返回POSITION_NONE意味着要刷新所有元素,是灰常浪费资源的,毕竟谷歌这么设计也总有道理。
一个稍加优化的思路是初始化时为页面设置tag,在getItemPosition方法中根据tag判断仅更新当前页面视图

@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

  • 我们继续一步一问,提出几个重要方法一探。
    先看看instantiateItem在ViewPager中的调用
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;
    }

嗯,添加新元素,顾名可以思义。但是该方法在何处调用,返回值又怎么使用呢?


都是在populate中调用,看来是个很厉害的方法了!

    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);
            // 更新当前视图index
            mCurItem = newCurrentItem;
        }

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

        // 若滑动未停止则暂停populate操作防止出现问题
        if (mPopulatePending) {
            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
            sortChildDrawingOrder();
            return;
        }

        // 若视图未依附于窗口则暂停populate操作
        if (getWindowToken() == null) {
            return;
        }

        mAdapter.startUpdate(this);
        // mOffscreenPageLimit为设定的预加载数,具体下边说
        // 根据当前视图位置和预加载数计算填充位置的起始点和终结点
        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());
        }

        // 在内存中定位所需视图元素,若不存在则重新添加
        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,若当前需填充的元素不在内存中则通过addNewItem调用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;
            // 当前视图左边的元素
            int itemIndex = curIndex - 1;
            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            final int clientWidth = getClientWidth();
            // 计算左侧预加载视图宽度
            final float leftWidthNeeded = clientWidth <= 0 ? 0 :
                    2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
            // 遍历当前视图左边的所有元素
            for (int pos = mCurItem - 1; pos >= 0; pos--) {
                // 若该元素在预加载范围外
                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) {
                    // 若该左侧元素在内存中,则更新记录
                    extraWidthLeft += ii.widthFactor;
                    itemIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                } else {
                    // 若该左侧元素不在内存中,则重新添加,再一次来到了addNewItem
                    ii = addNewItem(pos, itemIndex + 1);
                    extraWidthLeft += ii.widthFactor;
                    curIndex++;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            }
            // 来到当前视图右侧,思路大致和左侧相同
            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;
                        }
                    }
                }
            }
        }
    }

方法较长,理解的甚为粗浅,还望大神指点。

  • 另外,作为支持“左右切换功能”的布局管理器,谷歌也为其配套提供了“预加载”机制,防止下一页内容加载不及时影响体验。
    设置预加载数量viewPager.setOffscreenPageLimit(2)(括号内数字代表当前元素左右两边各需预加载的数量)
    源码内部是这样定义setOffscreenPageLimit方法的,详见注释。
// DEFAULT_OFFSCREEN_PAGES = 1 默认预加载数为1
public void setOffscreenPageLimit(int limit) {
        // 若用户设置的预加载数量小于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) {
            // 设定预加载数,填充页面视图
            mOffscreenPageLimit = limit;
            populate();
        }
    }

最后

ViewPager玄机深,本文仅仅窥一斑。往后会继续叨叨ViewPager(二),理解越深用着越顺。进击!

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

推荐阅读更多精彩内容