ViewPager 源码简要分析

ViewPager 在 Android 开发中是最常用的控件之一,最近刚好有时间,于是看了一下它的源码,顺便做个笔记记录一下。

看源码,我们一般会带着问题去看,这样效果可能会更好一些。看 ViewPager 源码,我主要是想弄清一下的几个问题。

1.疑问

  • ViewPager 是直接继承 ViewGroup 的,继承 ViewGroup 的自定义控件要在 onMeasure() 和 onLayout() 方法中对 childView 进行处理的,ViewPager 是如何做的?
  • ViewPager 是如何计算滑动的距离,并滑动到相应的位置?
  • ViewPager 是如何处理点击事件的拦截,并处理相应的事件冲突的?
  • ViewPager#setOffscreenPageLimit(int limit) 是如何实现界面缓存数量的限制的?

-

2. 分析

入口 setAdapter –> onMeasure / populate –> onLayout

1. 对 setAdapter(…) 的分析

ViewPager#setAdapter(PagerAdapter adapter) 是对 ViewPager 分析的入口,这个方法里面的代码比较简单,我们只需要知道如果是第一次布局,它会调用 requestLayout() 对 ViewPager 重新进行布局。

2. 对 onMeasure(…) 的分析

先看分析代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1.存储 ViewPager 自身的测量宽高
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));

final int measuredWidth = getMeasuredWidth();
final int maxGutterSize = measuredWidth / 10;
mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize); // 间隔(槽)的尺寸???

// Children are just made to fill our space.
// ChildView 的宽高, 要减去两边 padding 的距离
int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();


// ChildView 是 decorview 的时候,一般涉及不到 可忽略
....

// 获取在 MeasureSpec.EXACTLY 去条件下,childview 的 MeasureSpec
mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);

// Make sure we have created all fragments that we need to have shown.
mInLayout = true;
populate(); // 2. 这个方法很重要
mInLayout = false;

// Page views next.
// 3. 对 childView 的 measure
size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {

....

final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp == null || !lp.isDecor) { // ChildView 不是 DecorView
// 获取 ChildView 的 widthSpec
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
// childView 自身的测量
child.measure(widthSpec, mChildHeightMeasureSpec);
}
}
}
}

具体的分析在上面的源码已经加了注释,主要是:
. 存储 ViewPager 自身测量的宽高.
. 调用 populate() 方法,稍后分析.
. 对 childView 进行测量,这是继承 ViewGroup 自定义 View 都要做的.

3. 对 papulate(…) 分析

注释的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
void populate(int newCurrentItem) {

...

// mOffscreenPageLimit 默认是 1
// 设置页数限制,[startPos, endPos]=>[mCurItem - pageLimit, mCurItem + pageLimit] (很好的越界判断方法,可以借用)
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);

...

// 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) {
if (ii.position == mCurItem) curItem = ii;
break;
}
} // 这两段写法可以借鉴,先从 List 中取,如果取到的结果为空,
// 则生成一个并存放到 List 中,并返回。这样下次取的时候就可以用了
// 这样避免了一次生成多个对象,减少了申请内存空间的分配,减少内存抖动。

if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex); // 里面会调用 Adapter.instantiateItem(ViewGroup container, int position)
}

// 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) {
// 左边的 ChildView
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); // 销毁 View
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 {
ii = addNewItem(pos, itemIndex + 1);
extraWidthLeft += ii.widthFactor;
curIndex++;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}

// 右边的 ChildView
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); // 销毁 View
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;
}
}
}

// 计算页面的偏移量, 计算 ItemInfo.offset 的值
calculatePageOffsets(curItem, curIndex, oldCurInfo);
}

...

mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);

mAdapter.finishUpdate(this);

...
}

populate(…) 主要做了:
. 设置了缓存界面数量的限制.
. 生成 ItemInfo, ItemInfo 是对 childView 的包装,方便 ViewPager 进行管理.
. 对左边和右边的 childView 进行处理.
. 对用 calculatePageOffsets,计算界面的偏移量,通过计算 ItemInfo.offset 的值,得出偏移量.

4. 对 onLayout(…) 分析

onLaout(…) 中是按普通 ViewGroup 对每个 childView 进行 layout 即可

5. 其他重要的方法

  • scrollToItem(…) 滑动
    使用 Scroller 来完成滑动的

  • onInterceptTouchEvent() 处理事件拦截
    只有在拖动的情况下才会进行拦截.

  • 预加载的问题 ViewPager#setOffscreenPageLimit
    默认是 1.
    在 populate(…) 方法中计算 starPos 和 endPos 用到了

1
2
3
4
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);

形成了

1
[startPos, currenPos, endPos]

-

3. 其他收获

. 下面的代码写法可以借鉴,先从 List 中取,如果取到的结果为空,则生成一个并存放到 List 中,并返回。这样下次取的时候就可以用了这样避免了一次生成多个对象,减少了申请内存空间的分配,减少内存抖动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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) {
curItem = addNewItem(mCurItem, curIndex);
}

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;
}
  • 另外 Arrays.sor() 排序使用到了 DulPivotQuickSort.sort() 快速排序算法。有时间要去研究一下。
yxhuang wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客