ViewPager源码分析(3):与PagerAdapter 交互
我的简书同步发布:ViewPager源码分析(3):与PagerAdapter 交互
转载请注明出处:【huachao1001的专栏:http://blog.csdn.net/huachao1001】
我们知道,ViewPager显示的页面离不开我们定义的适配器,正是因为我们编写了自己的适配器,才让ViewPager显示出满足你的需求的内容,那么ViewPager是如何与适配器(PagerAdapter)进行交互的呢?我们今天来研读一下ViewPager中与PagerAdapter交互的部分代码。本文对学习ViewPager很重要,请耐心往下仔细研读 …O(∩_∩)O~~
在分析源码之前,我们先看看ViewPager的最简单的示例用法,当然了,你也可以把你的所有View保存到一个List中。
先看看ViewPager简单使用
通过用法找到切入点,ViewPager的比较典型的示例用法如下:
public class MainActivity extends AppCompatActivity { private List<String> data; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); ViewPager viewPager = (ViewPager) findViewById(R.id.vp); viewPager.setAdapter(new MyAdapter()); } private void init() { data = new ArrayList<>(); for (int i = 0; i < 10; i++) { data.add("str" + i); } } class MyAdapter extends PagerAdapter { @Override public int getCount() { return data.size(); } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((View) object); } @Override public Object instantiateItem(ViewGroup container, int position) { TextView textView = new TextView(MainActivity.this); String s = data.get(position); textView.setText(s); container.addView(textView); return textView; } } }
可以看到ViewPager与我们的数据源之间是需要通过适配器来适配的。接下来我们去看看ViewPager源码中,是如何与PagerAdapter交互。
从setAdapter切入
上面简单示例代码中可用看到,调用ViewPager的setAdapter函数即可将ViewPager与PagerAdapter关联起来,我们先去查看ViewPager的setAdapter方法。
public void setAdapter(PagerAdapter adapter) { //1.如果已经设置过PagerAdapter,即mAdapter != null, // 则做一些清理工作 if (mAdapter != null) { //2.清除观察者 mAdapter.setViewPagerObserver(null); //3.回调startUpdate函数,告诉PagerAdapter开始更新要显示的页面 mAdapter.startUpdate(this); //4.如果之前保存有页面,则将之前所有的页面destroy掉 for (int i = 0; i < mItems.size(); i++) { final ItemInfo ii = mItems.get(i); mAdapter.destroyItem(this, ii.position, ii.object); } //5.回调finishUpdate,告诉PagerAdapter结束更新 mAdapter.finishUpdate(this); //6.将所有的页面清除 mItems.clear(); //7.将所有的非Decor View移除,即将页面移除 removeNonDecorViews(); //8.当前的显示页面重置到第一个 mCurItem = 0; //9.滑动重置到(0,0)位置 scrollTo(0, 0); } //10.保存上一次的PagerAdapter final PagerAdapter oldAdapter = mAdapter; //11.设置mAdapter为新的PagerAdapter mAdapter = adapter; //12.设置期望的适配器中的页面数量为0个 mExpectedAdapterCount = 0; //13.如果设置的PagerAdapter不为null if (mAdapter != null) { //14.确保观察者不为null,观察者主要是用于监视数据源的内容发生变化 if (mObserver == null) { mObserver = new PagerObserver(); } //15.将观察者设置到PagerAdapter中 mAdapter.setViewPagerObserver(mObserver); mPopulatePending = false; //16.保存上一次是否是第一次Layout final boolean wasFirstLayout = mFirstLayout; //17.设定当前为第一次Layout mFirstLayout = true; //18.更新期望的数据源中页面个数 mExpectedAdapterCount = mAdapter.getCount(); //19.如果有数据需要恢复 if (mRestoredCurItem >= 0) { //20.回调PagerAdapter的restoreState函数 mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); setCurrentItemInternal(mRestoredCurItem, false, true); //21.标记无需再恢复 mRestoredCurItem = -1; mRestoredAdapterState = null; mRestoredClassLoader = null; } else if (!wasFirstLayout) {//如果在此之前不是第一次Layout //22.由于ViewPager并不是将所有页面作为子View, // 而是最多缓存用户指定缓存个数*2(左右两边,可能左边或右边没有那么多页面) //因此需要创建和销毁页面,populate主要工作就是这些 populate(); } else { //23.重新布局(Layout) requestLayout(); } } //24.如果PagerAdapter发生变化,并且设置了OnAdapterChangeListener监听器 // 则回调OnAdapterChangeListener的onAdapterChanged函数 if (mAdapterChangeListener != null && oldAdapter != adapter) { mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); } }
从第2条注解中看到,需要清除观察者,另外从第14条注释中看到,需要设定观察者,那么这个观察者是干嘛的呢?显然,这里是通过使用观察者模式。就是说,当我们编写代码时,如果数据源发生变化,需要在代码里调用PagerAdapter的notifyDataSetChanged函数,即通知ViewPager数据源发生变化,ViewPager就是一个观察者,通过观察者类PagerObserver做相关应对操作。
另外,前面多次提到页面的抽象描述类ItemInfo,我们看看ItemInfo的定义:
static class ItemInfo { //object为PagerAdapter的instantiateItem函数返回的对象 Object object; //position为页面的序号,即第几个页面 int position; //是否正在滚动 boolean scrolling; //页面宽度,取值为0到1,表示为页面宽度与ViewPager显示区域宽度比例,默认为1 float widthFactor; //偏移量,页面移动的偏移量,默认为0 float offset; }
最后在第22条注释中,调用了populate()函数,而populate()函数是做什么的呢?可以说,我们在使用ViewPager之所以流畅不卡,绝大部分功劳属于populate函数。
大功臣populate函数
细心的童鞋会发现,早在上一篇文章《ViewPager源码分析(2):滑动及冲突处理 》的2.3 ViewPager 定义smoothScrollTo函数小节源码中的第33行中,就出现过populate函数,无参数的populate其内部是调用了有参的populate(int newCurrentItem)函数,而newCurrentItem表示当需要定位显示的页面。我们先看看源码:
void populate(int newCurrentItem) { ItemInfo oldCurInfo = null; if (mCurItem != newCurrentItem) { oldCurInfo = infoForPosition(mCurItem); mCurItem = newCurrentItem; } if (mAdapter == null) { //对子View的绘制顺序进行排序,优先绘制Decor View //再按照position从小到大排序 sortChildDrawingOrder(); return; } //如果我们正在等待populate,那么在用户手指抬起切换到新的位置期间应该推迟创建子View, // 直到滚动到最终位置再去创建,以免在这个期间出现差错 if (mPopulatePending) { if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); //对子View的绘制顺序进行排序,优先绘制Decor View //再按照position从小到大排序 sortChildDrawingOrder(); return; } //同样,在ViewPager没有attached到window之前,不要populate. // 这是因为如果我们在恢复View的层次结构之前进行populate,可能会与要恢复的内容有冲突 if (getWindowToken() == null) { return; } //回调PagerAdapter的startUpdate函数, // 告诉PagerAdapter开始更新要显示的页面 mAdapter.startUpdate(this); final int pageLimit = mOffscreenPageLimit; //确保起始位置大于等于0,如果用户设置了缓存页面数量,第一个页面为当前页面减去缓存页面数量 final int startPos = Math.max(0, mCurItem - pageLimit); //保存数据源中的数据个数 final int N = mAdapter.getCount(); //确保最后的位置小于等于数据源中数据个数-1, // 如果用户设置了缓存页面数量,第一个页面为当前页面加缓存页面数量 final int endPos = Math.min(N - 1, mCurItem + pageLimit); //判断用户是否增减了数据源的元素,如果增减了且没有调用notifyDataSetChanged,则抛出异常 if (N != mExpectedAdapterCount) { //resName用于抛异常显示 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; //遍历每个页面对应的ItemInfo,找出获焦页面 for (curIndex = 0; curIndex < mItems.size(); curIndex++) { final ItemInfo ii = mItems.get(curIndex); //找到当前页面对应的ItemInfo后,跳出循环 if (ii.position >= mCurItem) { if (ii.position == mCurItem) curItem = ii; break; } } //如果没有找到获焦的页面,说明mItems列表里面没有保存获焦页面, // 需要将获焦页面加入到mItems里面 if (curItem == null && N > 0) { curItem = addNewItem(mCurItem, curIndex); } //默认缓存当前页面的左右两边的页面,如果用户设定了缓存页面数量, // 则将当前页面两边都缓存用户指定的数量的页面 //如果当前没有页面,则我们啥也不需要做 if (curItem != null) { float extraWidthLeft = 0.f; //左边的页面 int itemIndex = curIndex - 1; //如果当前页面左边有页面,则将左边页面对应的ItemInfo取出,否则左边页面的ItemInfo为null ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; //保存显示区域的宽度 final int clientWidth = getClientWidth(); //算出左边页面需要的宽度,注意,这里的宽度是指实际宽度与可视区域宽度比例, // 即实际宽度=leftWidthNeeded*clientWidth final float leftWidthNeeded = clientWidth <= 0 ? 0 : 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; //从当前页面左边第一个页面开始,左边的页面进行遍历 for (int pos = mCurItem - 1; pos >= 0; pos--) { //如果左边的宽度超过了所需的宽度,并且当前当前页面位置比第一个缓存页面位置小 //这说明这个页面需要Destroy掉 if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { //如果左边已经没有页面了,跳出循环 if (ii == null) { break; } //将当前页面destroy掉 if (pos == ii.position && !ii.scrolling) { mItems.remove(itemIndex); //回调PagerAdapter的destroyItem mAdapter.destroyItem(this, pos, ii.object); if (DEBUG) { Log.i(TAG, "populate() - destroyItem() with pos: " + pos + " view: " + ((View) ii.object)); } //由于mItems删除了一个元素 //需要将索引减一 itemIndex--; curIndex--; ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } } else if (ii != null && pos == ii.position) { //如果当前位置是需要缓存的位置,并且这个位置上的页面已经存在 //则将左边宽度加上当前位置的页面 extraWidthLeft += ii.widthFactor; //mItems往左遍历 itemIndex--; //ii设置为当前遍历的页面的左边一个页面 ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } else {//如果当前位置是需要缓存,并且这个位置没有页面 //需要添加一个ItemInfo,而addNewItem是通过PagerAdapter的instantiateItem获取对象 ii = addNewItem(pos, itemIndex + 1); //将左边宽度加上当前位置的页面 extraWidthLeft += ii.widthFactor; //由于新加了一个元素,当前的索引号需要加1 curIndex++; //ii设置为当前遍历的页面的左边一个页面 ii = itemIndex >= 0 ? 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); } } //回调PagerAdapter的setPrimaryItem,告诉PagerAdapter当前显示的页面 mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); //回调PagerAdapter的finishUpdate,告诉PagerAdapter页面更新结束 mAdapter.finishUpdate(this); //检查页面的宽度是否测量,如果页面的LayoutParams数据没有设定,则去重新设定好 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(); //如果ViewPager被设定为可获焦的,则将当前显示的页面设定为获焦 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(View.FOCUS_FORWARD)) { break; } } } } } }
从populate函数源码我们看到,ViewPager缓存当前显示的页面左右两边的页面,这个页面个数默认为左右两边各1个(如果左右都有至少1个的话),或者是用户通过调用ViewPager的setOffscreenPageLimit(int limit)函数来设定左右两边保持(好吧,原谅我从头到尾用缓存这个词)的页面个数。看看setOffscreenPageLimit(int limit)源码:
public void setOffscreenPageLimit(int limit) { //DEFAULT_OFFSCREEN_PAGES=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(); } }
可以看到如果我们设置的值小于1,那么ViewPager会将缓存页面数量设置为1,即,缓存的页面数量至少为1,并且每次改变缓存数量后也会调用populate函数。
既然ViewPager真正的子View个数只是两边”缓存”的页面个数+1(当前显示的页面),那么ViewPager是如何做到无阻碍的从头滑到尾而不出问题呢?前面我们提到,smoothScrollTo函数里面也调用了populate函数,而populate函数维护了当前显示的页面和左右两边“缓存”的页面,这样就做到了能滑到结尾。那么populate是如何让ViewPager的子View一直保持为两边”缓存”的页面+当前显示的页面呢?其实,源码上面很明显,populate先判断页面是否不在缓存的范围内,如果不在缓存范围内,则Destroy掉(调用PagerAdapter的destroyItem),而如果在缓存范围,但是这个位置上页面不存在(即没有加入到ViewPager,作为ViewPager子View),则调用PagerAdapter的instantiateItem来添加新页面(通过addNewItem来调用)。假设我执行了setOffscreenPageLimit(2)函数,那么我们看看ViewPager的简单示意图:
如果你仔细想想,你会发现,这原理跟《打造属于你的LayoutManager 》一文中的RecyclerView很像,不同的是,RecyclerView有回收利用,而ViewPager没有View的回收利用。
好啦,今天的学习就到这里啦~