对ViewPager的理解

文章目录

    • 一、高度设置wrap_content无效?
    • 二、setOffscreenPageLimit(0)不起作用?
    • 三、Viewpager+Fragment组合使用

一、高度设置wrap_content无效?

当给ViewPager的高度设置为wrap_content,并不会生效,原因在于onMeasure方法。onMeasure方法实用类测量宽高的,so每个View都会有自己的onMeasure方法。讲道理人家(的onMeasure方法)会先测量child的高度,最后再通过setMeasuredDimension方法测量并设置自己的宽高。

然而,ViewPager并没有这么做!上来就调用setMeasuredDimension方法设置自己的宽高,根本没有管它的child,简直就不是亲生的!

this.setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));

从这行代码可以看出,VP将自己默认的size设为0,优先使用父布局传过来的widthMeasureSpec、heightMeasureSpec。结果就是如果不指定宽高,就使用match_parent。 只要VP符合它自己的parent的要求,VP就会快速的它的parent填充满,其他的一概不管。也许VP如此对待它的child的原因在于child可被动态的添加、删除,宽高并不确定!

要想解决这个问题,其实也很简单:只需要自定义ViewPager并重写onMeasure方法即可。在onMeasure方法中,先遍历自己所有的child并计算它们的高度,最后再调用父类的onMeasure方法,将得到的宽高传入:

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

二、setOffscreenPageLimit(0)不起作用?

Fragment+ViewPager组合使用时,调用setOffscreenPageLimit(0)设置当前页面下左右两边缓存的item个数为0,无效。

public void setOffscreenPageLimit(int limit) {
        if (limit < 1) {
            Log.w("ViewPager", "Requested offscreen page limit " + limit + " too small; defaulting to " + 1);
            limit = 1;
        }

        if (limit != this.mOffscreenPageLimit) {
            this.mOffscreenPageLimit = limit;
            this.populate();
        }

    }

从以上代码中就能找到证据,当limit < 1时,limit = 1。也就是说即使设置缓存数为0,ViewPager始终还是会额外的去缓存下一个。对于VP+Fragment组合的使用场景而言,curItem是可见的,curItem+1无需展示,这样也会在请求网络上造成时间压力。

ViewPager + Adapter结合使用,VP是如何从Adapter处获取、销毁的数据呢?这就引出了一个神秘方法–populate() 。

populate() 是专门用来计算、处理缓存的,populate() 函数的生命周期是和Adapter的生命周期同步的。ViewPager能够顺利的对数据进行获取、销毁等操作,全部都依赖于populate() 。

 void populate(int newCurrentItem) {
        ViewPager.ItemInfo oldCurInfo = null;
        if (this.mCurItem != newCurrentItem) {
            oldCurInfo = this.infoForPosition(this.mCurItem);
            this.mCurItem = newCurrentItem;
        }

        if (this.mAdapter == null) {
            this.sortChildDrawingOrder();
        } else if (this.mPopulatePending) {
            this.sortChildDrawingOrder();
        } else if (this.getWindowToken() != null) {
            this.mAdapter.startUpdate(this);// --->更新
            int pageLimit = this.mOffscreenPageLimit;
            int startPos = Math.max(0, this.mCurItem - pageLimit);
            int N = this.mAdapter.getCount();// --->获取Adapter的数据量
            int endPos = Math.min(N - 1, this.mCurItem + pageLimit);
			// 缓存空间[startPos,endPos] ---> [mCurItem-pageLimit,mCurItem+pageLimit]
            if (N != this.mExpectedAdapterCount) {
                String resName;
                try {
                    resName = this.getResources().getResourceName(this.getId());
                } catch (NotFoundException var17) {
                    resName = Integer.toHexString(this.getId());
                }

                throw new IllegalStateException("The application's PagerAdapter changed the adapter's contents without calling PagerAdapter#notifyDataSetChanged! Expected adapter item count: " + this.mExpectedAdapterCount + ", found: " + N + " Pager id: " + resName + " Pager class: " + this.getClass() + " Problematic adapter: " + this.mAdapter.getClass());
            } else {
                int curIndex = true;
                ViewPager.ItemInfo curItem = null;

                int curIndex;
                for(curIndex = 0; curIndex < this.mItems.size(); ++curIndex) {
                    ViewPager.ItemInfo ii = (ViewPager.ItemInfo)this.mItems.get(curIndex);
                    if (ii.position >= this.mCurItem) {
                        if (ii.position == this.mCurItem) {
                            curItem = ii;
                        }
                        break;
                    }
                }

				//curItem没找到,即没有缓存,就先加载一个item
                if (curItem == null && N > 0) {
                    curItem = this.addNewItem(this.mCurItem, curIndex);
                }

                int itemIndex;
                ViewPager.ItemInfo ii;
                int i;
                if (curItem != null) {
					//先对左边的item进行缓存处理
                    float extraWidthLeft = 0.0F;
                    itemIndex = curIndex - 1;
                    ii = itemIndex >= 0 ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
                    i = this.getClientWidth();
                    float leftWidthNeeded = i <= 0 ? 0.0F : 2.0F - curItem.widthFactor + (float)this.getPaddingLeft() / (float)i;

                    for(int pos = this.mCurItem - 1; pos >= 0; --pos) {
                        if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                            if (ii == null) {
                                break;
                            }

							// 缓存区间之外的item,要destory掉
                            if (pos == ii.position && !ii.scrolling) {
                                this.mItems.remove(itemIndex);
                                this.mAdapter.destroyItem(this, pos, ii.object);// --->销毁item
                                --itemIndex;
                                --curIndex;
                                ii = itemIndex >= 0 ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
                            }
                        } else if (ii != null && pos == ii.position) {
                            extraWidthLeft += ii.widthFactor;
                            --itemIndex;
                            ii = itemIndex >= 0 ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
                        } else { //如果item没有出现过,就去addNewItem
                            ii = this.addNewItem(pos, itemIndex + 1);
                            extraWidthLeft += ii.widthFactor;
                            ++curIndex;
                            ii = itemIndex >= 0 ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
                        }
                    }

					//再对右边的item进行缓存处理
                    float extraWidthRight = curItem.widthFactor;
                    itemIndex = curIndex + 1;
                    if (extraWidthRight < 2.0F) {
                        ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
                        float rightWidthNeeded = i <= 0 ? 0.0F : (float)this.getPaddingRight() / (float)i + 2.0F;

                        for(int pos = this.mCurItem + 1; pos < N; ++pos) {
                            if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
                                if (ii == null) {
                                    break;
                                }

                                if (pos == ii.position && !ii.scrolling) {
                                    this.mItems.remove(itemIndex);
                                    this.mAdapter.destroyItem(this, pos, ii.object);
                                    ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
                                }
                            } else if (ii != null && pos == ii.position) {
                                extraWidthRight += ii.widthFactor;
                                ++itemIndex;
                                ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
                            } else {
                                ii = this.addNewItem(pos, itemIndex);
                                ++itemIndex;
                                extraWidthRight += ii.widthFactor;
                                ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
                            }
                        }
                    }

                    this.calculatePageOffsets(curItem, curIndex, oldCurInfo);
                    this.mAdapter.setPrimaryItem(this, this.mCurItem, curItem.object); // --->设置当前的item
                }

                this.mAdapter.finishUpdate(this);// --->完成更新
                int childCount = this.getChildCount();

                for(itemIndex = 0; itemIndex < childCount; ++itemIndex) {
                    View child = this.getChildAt(itemIndex);
                    ViewPager.LayoutParams lp = (ViewPager.LayoutParams)child.getLayoutParams();
                    lp.childIndex = itemIndex;
                    if (!lp.isDecor && lp.widthFactor == 0.0F) {
                        ViewPager.ItemInfo ii = this.infoForChild(child);
                        if (ii != null) {
                            lp.widthFactor = ii.widthFactor;
                            lp.position = ii.position;
                        }
                    }
                }

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

            }
        }
    }

首先关注一点:

curItem = this.addNewItem(this.mCurItem, curIndex);

一旦没找到curItem,即没有缓存,就先加载一个item。curItem是通过addNewItem方法来确定的:

ViewPager.ItemInfo addNewItem(int position, int index) {
        ViewPager.ItemInfo ii = new ViewPager.ItemInfo();
        ii.position = position;
		//ItemInfo中的object就是缓存的View
        ii.object = this.mAdapter.instantiateItem(this, position);
        ii.widthFactor = this.mAdapter.getPageWidth(position);
        if (index >= 0 && index < this.mItems.size()) {
            this.mItems.add(index, ii);
        } else {
            this.mItems.add(ii);
        }

        return ii;
    }

而在addNewItem函数中,我们可以发现Adapter的身影:this.mAdapter.instantiateItem(this, position)。也就是说创建新的Item要通过Adapter,恰巧instantiateItem方法的作用就是用来构建新的View并return。

其实,只要仔细观察Adapter的方法:

public abstract int getCount();
public void startUpdate(@NonNull ViewGroup container)
public Object instantiateItem(@NonNull ViewGroup container, int position)
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) 
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object)
public void finishUpdate(@NonNull ViewGroup container)
......

在populate()方法中都能看到他们被调用的身影:

this.mAdapter.startUpdate(this);// --->更新
int N = this.mAdapter.getCount();// --->获取Adapter的数据量
this.mAdapter.destroyItem(this, pos, ii.object);// --->销毁item
this.mAdapter.setPrimaryItem(this, this.mCurItem, curItem.object); // --->设置当前的item
this.mAdapter.finishUpdate(this);// --->完成更新
......

这意味着,populate()函数贯穿了整个Adapter的各个函数的调用过程,等同于populate()方法的生命周期和Adapter已经融合在一起了。换句话说,Adapter的所有动作都是由populate()方法操控的,即每个Item的管理实际上都是由populate()方法控制的。可以得出结论:VP的缓存的幕后黑手是populate()。就好比是宇智波带土一直在用写轮眼控制三尾人柱力矢仓,对鬼鲛下达命令。

说道缓存,仔细看populate()方法中的startPos和endPos。VP的缓存空间就是[startPos,endPos] —> [mCurItem-pageLimit,mCurItem+pageLimit]。这里的pageLimit就是我们在Java代码中调用setOffscreenPageLimit(n)时传入的值。这样计算来,VP缓存的是[当前Item-pageLimit,当前Item+pageLimit]。

在完成更新之前,要对左右两边的缓存进行处理:

if (curItem != null) {
					//先对左边的item进行缓存处理
                    float extraWidthLeft = 0.0F;
                    itemIndex = curIndex - 1;
                    ii = itemIndex >= 0 ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
                    i = this.getClientWidth();
                    float leftWidthNeeded = i <= 0 ? 0.0F : 2.0F - curItem.widthFactor + (float)this.getPaddingLeft() / (float)i;

                    for(int pos = this.mCurItem - 1; pos >= 0; --pos) {
                        if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                            if (ii == null) {
                                break;
                            }

							// 缓存区间之外的item,要destory掉
                            if (pos == ii.position && !ii.scrolling) {
                                this.mItems.remove(itemIndex);
                                this.mAdapter.destroyItem(this, pos, ii.object);// --->销毁item
                                --itemIndex;
                                --curIndex;
                                ii = itemIndex >= 0 ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
                            }
                        } else if (ii != null && pos == ii.position) {
                            extraWidthLeft += ii.widthFactor;
                            --itemIndex;
                            ii = itemIndex >= 0 ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
                        } else { //如果item没有出现过,就去addNewItem
                            ii = this.addNewItem(pos, itemIndex + 1);
                            extraWidthLeft += ii.widthFactor;
                            ++curIndex;
                            ii = itemIndex >= 0 ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
                        }
                    }

					//再对右边的item进行缓存处理
                    float extraWidthRight = curItem.widthFactor;
                    itemIndex = curIndex + 1;
                    if (extraWidthRight < 2.0F) {
                        ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
                        float rightWidthNeeded = i <= 0 ? 0.0F : (float)this.getPaddingRight() / (float)i + 2.0F;

                        for(int pos = this.mCurItem + 1; pos < N; ++pos) {
                            if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
                                if (ii == null) {
                                    break;
                                }

                                if (pos == ii.position && !ii.scrolling) {
                                    this.mItems.remove(itemIndex);
                                    this.mAdapter.destroyItem(this, pos, ii.object);
                                    ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
                                }
                            } else if (ii != null && pos == ii.position) {
                                extraWidthRight += ii.widthFactor;
                                ++itemIndex;
                                ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
                            } else {
                                ii = this.addNewItem(pos, itemIndex);
                                ++itemIndex;
                                extraWidthRight += ii.widthFactor;
                                ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
                            }
                        }
                    }

                    this.calculatePageOffsets(curItem, curIndex, oldCurInfo);
                    this.mAdapter.setPrimaryItem(this, this.mCurItem, curItem.object); // --->设置当前的item
                }

处理顺序为:先左右后。缓存区间之外的Item要及时destroyItem销毁掉。当然,如果Item没有出现,就先去addNewItem构建出一个Item来,并保存到mItems中。这个mItems是专门用来缓存的ArrayList:

//mItems是专门用来缓存的ArrayList
private final ArrayList<ViewPager.ItemInfo> mItems = new ArrayList();

泛型为ItemInfo:

    static class ItemInfo {
        Object object; // --->缓存的就是View
        int position;
        boolean scrolling;
        float widthFactor;
        float offset;

        ItemInfo() {
        }
    }

这个ItemInfo类中有众多信息,其中的 Object object 就是专门用来缓存View的。因此,ViewPager才会去缓存界面。仔细看addNewItem方法中的一行代码:

//ItemInfo中的object就是缓存的View
ii.object = this.mAdapter.instantiateItem(this, position);

想想看Adapter调用instantiateItem是干什么的?同时又返回了什么?
对ViewPager的理解_第1张图片这样一来就解释的通了:当需要构建View的时候,addNewItem函数会通过调用Adapter的instantiateItem方法来创建,return的View会被保存到专门用来缓存的ItemInfo的object中。

如果对方是Fragment,那么观察FragmentPagerAdapter:

   @NonNull
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        if (this.mCurTransaction == null) {
            this.mCurTransaction = this.mFragmentManager.beginTransaction();
        }

        long itemId = this.getItemId(position);
        String name = makeFragmentName(container.getId(), itemId);
        Fragment fragment = this.mFragmentManager.findFragmentByTag(name);
        if (fragment != null) {
            this.mCurTransaction.attach(fragment);
        } else {
            fragment = this.getItem(position);
            this.mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));
        }

        if (fragment != this.mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            fragment.setUserVisibleHint(false);
        }

        return fragment;
    }

新建并返回、保存的就是fragment了。至此,ViewPager的缓存原理就十分清晰了。

三、Viewpager+Fragment组合使用

缓存有好处,也有不友好的地方。对Fragment而言,如果不进行懒加载处理,那是非常糟糕的。一般对Fragment进行懒加载处理,不能单一的根据setUserVisibleHint或者onResume来判断。Fragment的界面可见状态分为3种:第一次可见、可见、不可见。这两个函数都不能很好的对其进行精准区分。

在这方面,我们需要关心Fragment的几个生命周期函数:onCreateView、onActivityCreated、onResume、onPause、onDestroyView。

onCreateView,最先想到的就是解析xml布局(这也是最根本的),其次就是初始化控件。
onActivityCreated,表示Activity创建完毕。
onResume、onPause和onDestoryView无需多言,常规认识。

关于setUserVisibleHint,它与Fragment的生命周期无关,仅仅是为了表达页面是否对用户可见。还记得FragmentPagerAdapter吗?这个适配器它内部的setPrimaryItem方法是用来设置当前Item是否可见的,方法内调用的就是setUserVisibleHint(true)方法:
对ViewPager的理解_第2张图片
关于setUserVisibleHint方法修改Fragment可见性,调用场景如下:

  • ① 切换tab时,会优先于所有Fragment生命周期函数调用
  • ② Fragment之前已经调用过该方法,但是后续要让Fragment的状态在可见与不可见之间切换

setUserVisibleHint(false)表示通知Fragment不可见,即终止一切网络请求!但是如果从未创建过就要怎样怎样,那就会报空指针异常。所以需要一个标志位来判断界面是否已经创建。只有当页面被创建出来了,才会去分发可见性。否则xml都还没加载,分发个球?不报空指针才怪呢。

所以,在setUserVisibleHint方法的处理上,我们根本不用考虑isViewCreated==false的情况。当isViewCreated为true时,我们再根据传进来的可见状态与当前页面可见状态,分发可见性。

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        //只考虑xml加载出来的情况
        if (isViewCreated) {
        	//根据传进来的状态 和 当前页面的可见性,判断分发是否可见
            if (isVisibleToUser && !currentVisibleState ) {
                dispatchUserVisibleHint(true);
            } else if (!isVisibleToUser && currentVisibleState) {
                dispatchUserVisibleHint(false);
            }
        }
    }

我们通过dispatchUserVisibleHint方法来统一处理用户可见信息的分发,在分发方法中,再区分是否是第一次可见。因为只有当第一次可见时,才会去主动做一些事的。否则其他情况的可见性,就通知用户可见状态,履行onResume()的职责。

当然,这里还需要用到currentVisibleState表示当前页面可见性,借助mIsFirstVisible 标记区分是否是第一次可见。

    private void dispatchUserVisibleHint(boolean isVisible) {
        //为了代码严谨
        if (currentVisibleState == isVisible) {
            return;
        }
        currentVisibleState = isVisible;
        if (isVisible) {
            if (mIsFirstVisible) {
                mIsFirstVisible = false;
                onFragmentFirstVisible();
            }
            onFragmentResume();
        } else {
            onFragmentPause();
        }
    }

另外,当FragmentTransaction来控制fragment的hide和show时,就会调用onHiddenChanged方法。此时,Fragment的生命周期函数不会再执行,任何生命周期都不会再走了(具体原因点击查看)。在这种情况下,数据就不能根据生命周期函数来判断刷新,此时可以依赖onHiddenChanged方法来继续判断处理:

    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        if (hidden) {
            dispatchUserVisibleHint(false);
        } else {
            dispatchUserVisibleHint(true);
        }
    }

剩下的工作,就是处理边边角角了。在onResume和onPause中根据是否第一次可见、当前页面可见性、getUserVisibleHint等对是否可见、是否第一次可见进行区分,并以此来分发可见性。

当然,如果考虑Fragment内嵌套Fragment,这里还得再多处理一层,那就是对内层Fragment分发可见性。代码不一样,但是性质差不多。首先要判断其Parent是否可见:

    private boolean isParentInvisible() {
        Fragment parentFragment = getParentFragment();
        if (parentFragment instanceof LazyFragment) {
            LazyFragment fragment = (LazyFragment)parentFragment;
            return !fragment.isSupportVisible();
        }
        return false;
    }

然后对统一分发进行小改,综合考虑Parent之后,再对Child的可见性向下分发:

    private void dispatchChildVisibleState(boolean visible) {
        FragmentManager fragmentManager = getChildFragmentManager();
        List<Fragment> fragments = fragmentManager.getFragments();
        if (fragments != null) {
            for (Fragment fragment: fragments) {
                if (fragment instanceof LazyFragment &&
                        !fragment.isHidden() &&
                        fragment.getUserVisibleHint()) {
                    ((LazyFragment)fragment).dispatchUserVisibleHint(visible);
                }
            }
        }
    }

你可能感兴趣的:(Android重点难点)