ViewPager 源码分析(一) —— setAdapter() 与 populate()
写在前面
做安卓也有一定时间了,虽然常用控件都已大致掌握,然而随着 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;
@Override
public 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;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mFirstLayout = true;
}
@Override
protected 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()的更多相关文章
- ViewPager源码分析——滑动切换页面处理过程
上周客户反馈Contacts快速滑动界面切换tab有明显卡顿,让优化. 自己验证又没发现卡顿现象,但总得给客户一个技术性的回复,于是看了一下ViewPager源码中处理滑动切换tab的过程. View ...
- 【Android 界面效果45】ViewPager源码分析
ViewPager概述: Layout manager that allows the user to flip left and right through pages of data. You s ...
- Spring源码分析——BeanFactory体系之抽象类、类分析(二)
上一篇分析了BeanFactory体系的2个类,SimpleAliasRegistry和DefaultSingletonBeanRegistry——Spring源码分析——BeanFactory体系之 ...
- Tomcat源码分析——SERVER.XML文件的加载与解析
前言 作为Java程序员,对于Tomcat的server.xml想必都不陌生.本文基于Tomcat7.0的Java源码,对server.xml文件是如何加载和解析的进行分析. 加载 server.xm ...
- 7、SpringMVC源码分析(2):分析HandlerAdapter.handle方法,了解handler方法的调用细节以及@ModelAttribute注解
从上一篇 SpringMVC源码分析(1) 中我们了解到在DispatcherServlet.doDispatch方法中会通过 mv = ha.handle(processedRequest, res ...
- documentsUI源码分析
documentsUI源码分析 本文基于Android 6.0的源码,来分析documentsUI模块. 原本基于7.1源码看了两天,但是Android 7.1与6.0中documentsUI模块差异 ...
- netty : NioEventLoopGroup 源码分析
NioEventLoopGroup 源码分析 1. 在阅读源码时做了一定的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限.为了方便 IDE 查看.跟踪.调试 代码,所以在 github ...
- 仿爱奇艺视频,腾讯视频,搜狐视频首页推荐位轮播图(二)之SuperIndicator源码分析
转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼:http://blog.csdn.net/hejjunlin/article/details/52510431 背景:仿爱奇艺视频,腾讯视频 ...
- Android base-adapter-helper 源码分析与扩展
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/44014941,本文出自:[张鸿洋的博客] 本篇博客是我加入Android 开源项 ...
随机推荐
- SDE注册版本失败,仅支持一个空间列
如果直接编辑SDE要素类与要素可以不需要版本,使用默认版本,如果要让用户通过界面编辑,即使用开启编辑.保存编辑和停止编辑,就需要注册为版本,而在注册版本弹出如下错误: 正如错误所说,一个要素类或shp ...
- Linux shell中一些参数与变量简介
linux中shell变量$#,$@,$0,$1,$2,$!,$$,$*,$-,$@......等很多个,很容易记错,这里再次整理一下,相关含义解释如下,并附上一个实践截图. 多看几次,多用几次,应该 ...
- vCenter Single Sign On 5.1 best practices
http://www.virtualizationteam.com/virtualization-vmware/vsphere-virtualization-vmware/vcenter-single ...
- Storm本地模式异常
来自:http://isuifengfei.iteye.com/blog/1998265 问题1 java.net.SocketException: Address family not supp ...
- Dwr 框架简单实例
Dwr 是一个 Java 开源库,帮助你实现Ajax网站. 它可以让你在浏览器中的Javascript代码调用Web服务器上的Java,就像在Java代码就在浏览器中一样. Dwr 主要包括两部分: ...
- ES6学习笔记八:类与继承
一:Class ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板.通过class关键字,可以定义类. 定义“类”的方法的时候,前面不需要加上function这个关键 ...
- JVM——字节码增强技术简介
Java字节码增强指的是在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修改.Java字节码增强主要是为了减少冗余代码,提高性能等. 实现字节码增强的主要步 ...
- Linux安装nginx并设置https(openssl
一.安装依赖包 1.$sudo apt-get install openssl 或者$sudo apt-get install libssl-dev 2.$sudo apt-get instal ...
- hbuilder mui uploader图片上传到服务器完整版(ASP.NET)
html布局,比较简单,模仿微信的: <div class="dynamic_images"> <ul> <!--<li><img ...
- MongoDB中的一些坑( 2.4.10 版本)
http://www.jb51.net/article/62654.htm 1.MongoDB 数据库级锁 MongoDB的锁机制和一般关系数据库如 MySQL(InnoDB), Oracle 有很大 ...