叨叨ViewPager那些事儿(一)

作者: 苹果tree | 来源:发表于2018-07-17 10:52 被阅读141次

    前言

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

    相关文章

      网友评论

      本文标题:叨叨ViewPager那些事儿(一)

      本文链接:https://www.haomeiwen.com/subject/eiekyftx.html