ViewPager 源码分析(一) —— setAdapter() 与 populate()
来源:互联网 发布:如何赚第一桶金 知乎 编辑:程序博客网 时间:2024/06/05 20:33
写在前面
做安卓也有一定时间了,虽然常用控件都已大致掌握,然而随着 Android N 的发布,不自觉的愈发焦虑起来。说来惭愧,Android L 的 Material Design 库里的许多控件都还没用过,照这样下去迟早要被新技术所淘汰。那该怎么办呢,偶然间我看到一篇博文如此说到:“不要觉得 android 里边控件繁杂多样,官方或第三方新控件层出不穷,其实真正的控件就只有两个View
和ViewGroup
。一旦有了它们的基础,不管来什么新控件,TabLayout
也好,CoordinatorLayout
也罢,花上一下午翻翻源码基本就掌握了(不仅仅是会用而已)。”
我明白了:新技术的精华还在新技术之外。抛开追寻新技术的浮躁,我决定补一补基础,这也是我写这篇文章的初衷。希望它能开一个好头,勉励自己沉下心来,read the fucking source code!
知识点
之所以选择 ViewPager 是因为它常常用到,大家对它足够熟悉。同时它有些难度,却又是自定义View的官方经典例子,涵盖了不少知识点:
- PagerAdapter、DataSetObserver 与观察者模式
- View 的生命周期(measure -> layout -> draw)
- View 的事件分发(滑动冲突的解决)
- View 滑动的工具类 (Scroller、VelocityTracker 等)
- …
阅读下文需要您已经有 ViewPager 、PagerAdapter 的使用经验,同时对 View 的绘制和事件分发流程有一定的了解。由于篇幅有限,本文只写到第一点;后几点回以续章的形式呈现。
源码分析
Adapter、DataSetObserver 与观察者模式
我们使用 ViewPager
,通常需要定义一个PagerAdapter
,然后setAdapter()
,用法上和ListView
很像。如图:
我们看到,PagerAdapter
持有数据集DataSetObservable
,同时包含一些回调。
setAdapter()
那么很自然的,我们从ViewPager
的setAdapter
开始分析把。
public void setAdapter(PagerAdapter adapter) { if (mAdapter != null) { // 1: 清空旧的 Adapter, 做一些初始化处理 mAdapter.unregisterDataSetObserver(mObserver); mAdapter.startUpdate(this); for (int i = 0; i < mItems.size(); i++) { final ItemInfo ii = mItems.get(i); mAdapter.destroyItem(this, ii.position, ii.object); } mAdapter.finishUpdate(this); mItems.clear(); removeNonDecorViews(); mCurItem = 0; scrollTo(0, 0); } // 2: 更新 mAdapter 字段 final PagerAdapter oldAdapter = mAdapter; mAdapter = adapter; mExpectedAdapterCount = 0; // 3: 给 mAdapter 添加数据 mObserver,恢复状态 if (mAdapter != null) { if (mObserver == null) { mObserver = new PagerObserver(); } // 3.1: 给 mAdapter 添加数据 mObserver mAdapter.registerDataSetObserver(mObserver); mPopulatePending = false; final boolean wasFirstLayout = mFirstLayout; mFirstLayout = true; mExpectedAdapterCount = mAdapter.getCount(); if (mRestoredCurItem >= 0) { // 3.2: 之前有状态保存下来,恢复状态 mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); setCurrentItemInternal(mRestoredCurItem, false, true); mRestoredCurItem = -1; mRestoredAdapterState = null; mRestoredClassLoader = null; } else if (!wasFirstLayout) { // 3.3: 没状态保存,且不是第一次被 Layout 出来 -> populate() 不知道要干嘛。。 populate(); } else { // 3.4: 没状态保存,且是第一次被 Layout 出来 -> 重新布局 requestLayout(); } } // 4: 回调监听器 if (mAdapterChangeListener != null && oldAdapter != adapter) { mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); }}
前面都好理解,其中ItemInfo
保存了每一项的信息。然后,mItems
其实是页面的缓存,adapter
变更的时候要先清空之前缓存。主要看 3.2 和 3.3 两处,有两个全局变量mRestoredCurItem
、mFirstLayout
不好理解,而且源码没有注释。。。
1. mRestoredCurItem
如代码所示,在onRestoreInstanceState
的时候保存了当前选中状态。
private int mRestoredCurItem = -1;@Overridepublic void onRestoreInstanceState(Parcelable state) { ... if (mAdapter != null) { ... } else { mRestoredCurItem = ss.position; ... }}
2. mFirstLayout ctrl + F
了一下,发现mFirstLayout
在这些地方被赋值。
private boolean mFirstLayout = true;public void setAdapter(PagerAdapter adapter) { ... mFirstLayout = true;}@Overrideprotected void onAttachedToWindow() { super.onAttachedToWindow(); mFirstLayout = true;}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) { ... mFirstLayout = false;}
这基本上是说,初始化为true
,onLayout()
之后变为false
,使得在setAdapter()
里:
如果已经onLayout()
过一次,可以用populate()
代替requestLayout()
。然后又重置了这个mFirstLayout
。
其实到这里还是一头雾水,这个populate()
到底要干嘛,源码一点注释都没有,想要答案还得继续分析。
populate()
先别急着看源码,这段比较长,要怎么分析呢。一个函数200多行,一开始我也懵逼了,多亏这片博客点醒了我:viewpager源码分析
要关注PagerAdapter
!!!是啊,绕来绕去怎么把这茬忘了,我们就是从setAdapter()
入手的,它才是我们的主角啊。这就好办了,抓住它发现populate()
几乎把mAdapter
的生命周期走了个遍。我用注释 // —— A~F做了标记:
- startUpdate()
- getCount()
- instantiateItem()
- destroyItem()
- setPrimaryItem()
- finishUpdate()
这样,populate()
的职能便呼之欲出了。它主要根据制定的页面缓存大小(mOffscreenPageLimit),做了页面的销毁和重建。除了,A~F这条线,还标注了0~2这条线。其中2部分有一些复杂的计算,主要做了页面销毁这项工作。本来还想分析一下calculatePageOffsets()
,现在想来没必要了。我们的主要目标Adapter
已经被我们搞定,想必对于PageAdapter
中页面如何创建也有了进一步的认识。
void populate(int newCurrentItem) { ... mAdapter.startUpdate(this); // ------ A // 0: 设置页数限制,[startPos, endPos]=>[mCurItem - pageLimit, mCurItem + pageLimit] // 对应 public void setOffscreenPageLimit(int limit); final int pageLimit = mOffscreenPageLimit; final int startPos = Math.max(0, mCurItem - pageLimit); final int N = mAdapter.getCount(); // ------ B final int endPos = Math.min(N-1, mCurItem + pageLimit); // 1: Locate the currently focused item or add it if needed. int curIndex = -1; ItemInfo curItem = null; for (curIndex = 0; curIndex < mItems.size(); curIndex++) { final ItemInfo ii = mItems.get(curIndex); if (ii.position >= mCurItem) { // 1.1: 便利找到第一个大于 mCurItem 的位置 if (ii.position == mCurItem) curItem = ii; break; } } // 1.2: 由于步骤0 处设置了缓存的页数限制,mItems 中可能会找不到 curItem, // 需要 addNewItem if (curItem == null && N > 0) { curItem = addNewItem(mCurItem, curIndex); // C: addNewItem()里边调用了 mAdapter.instantiateItem() } // 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. // 2: (译)根据 mOffscreenPageLimit 这个参数(默认为1), // 决定保留的页面范围,即[startPos, endPos] 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--) { // 2.1: 逆序遍历左边,累加 extraWidthLeft,并与 leftWidthNeeded 比较 // 同时,如果 pos 超出边界[startPos, endPos], 则销毁 view // 这里的参数计算比较复杂,只看了个大概。。。 if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { if (ii == null) { break; } if (pos == ii.position && !ii.scrolling) { mItems.remove(itemIndex); // ------ D mAdapter.destroyItem(this, pos, ii.object); // 2.2: 回调销毁 view ... itemIndex--; curIndex--; ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } } else if (ii != null && pos == ii.position) { extraWidthLeft += ii.widthFactor; // 2.3: 累加 extraWidthLeft itemIndex--; ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } else { ii = addNewItem(pos, itemIndex + 1); extraWidthLeft += ii.widthFactor; // 2.4: 累加 extraWidthLeft curIndex++; ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } } // 右边情况与左边完全对偶,不再详细贴出 ... // 2.6: 计算页面偏移 calculatePageOffsets(curItem, curIndex, oldCurInfo); } mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); // ------ E mAdapter.finishUpdate(this); // ------ F // 下面两部分分别是 LayoutParams 和 Focus 处理, // 感觉不太重要,已省略}
总结
还是小看它了,ViewPager
比我想像的要复杂。这一长篇才只分析到PagerAdapter
,连DataSetObservable
都没引入。然而我已有些困意,未完待续。。。
- ViewPager 源码分析(一) —— setAdapter() 与 populate()
- Recycleview之setAdapter源码分析
- GCC源码分析(一)——介绍与安装
- GCC源码分析(一)——介绍与安装
- ViewPager 源码分析(二) —— 关于 notifyDataSetChanged()
- viewPager.setAdapter(new MyFragmentPagerAdapter(getSupportFragmentManager()
- viewpager源码分析
- ViewPager源码分析
- ViewPager源码分析
- ViewPager实现源码分析
- ViewPager源码分析(3):与PagerAdapter 交互
- SwipeRefreshLayout与ViewPager滑动事件冲突源码分析及解决办法
- ffmpeg源码分析与应用示例(一)——H.264解码与QP提取
- ffmpeg源码分析与应用示例(一)——H.264解码与QP提取
- 从源码上分析ListView的addHeaderView和setAdapter的调用顺序
- Android之ViewPager源码分析
- perf_event源码分析(一)——cmd_record
- LIRE原理与源码分析(一)——代码结构
- 109Q游戏(8)123/133/146/163/168/172/177(9-15)
- java工程怎么应用其他工程的类中的方法或者本工程其他类中的方法?
- 详解Python的装饰器
- properties文件读取测试3
- 后置自增操作符与解引用,前置自增操作符
- ViewPager 源码分析(一) —— setAdapter() 与 populate()
- Android环境变量配置
- emWin 2天速成实例教程003_Framewin和Window窗口控件和2D绘图
- select 标签 properties文件读取值 未完待续
- 信号量实现线程之间的PV操作
- Linux下的通配符和特殊符号
- 在ShaderToy上研究水波的Shader
- ubuntu 安装与卸载应用总结
- Java任务调度框架Quartz入门教程指南(四)Quartz任务调度框架之触发器精讲SimpleTrigger和CronTrigger、最详细的Cron表达式范例