Android开发Android开发经验谈Android进阶之路

叨叨ViewPager那些事儿(一)

2018-07-17  本文已影响141人  苹果tree

前言

问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

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;
                        }
                    }
                }
            }
        }
    }

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

// 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(二),理解越深用着越顺。进击!

上一篇下一篇

猜你喜欢

热点阅读