ViewPager系列文章,从入门,到习得,再到精通应用技巧,本篇迎来了大结局——ViewPager源码剖析。
传送门:
ViewPager系列一:初相识
ViewPager系列二: Adapter的爱恨情仇
ViewPager系列三:两个熊孩子天生不一样
ViewPager系列四:让ViewPager用起来更顺滑——换页监听及换页方法
ViewPager系列五:让ViewPager用起来更顺滑——懒加载及预加载定制
ViewPager系列六:让ViewPager用起来更顺滑——设置间距与添加转场动画
ViewPager系列七:让ViewPager用起来更顺滑——轮播、禁止滑动与指示器的配合
ViewPager系列八:一篇彻底读懂ViewPager源码(完结)
温馨提示:本篇博客内容较多,包括源码在内四万多字,文章分三个部分,读者可以每次看一个部分,或者在工作中遇到源码不懂的地方,想要在博客中找到答案,可以选择全局搜索关键字,给你最快速的解答。
像其他源码类文章一样,分析源码是为了让我们更深入了解工作机制,以及解决我们开发过程中可能遇到的各种奇葩问题。此外,理解源码才能实现更高级的定制,甚至学习一些安卓UI应用技巧(限控件源码)。
本篇源码探究有别于其他源码类文章,主要分以下三个部分来分析:
1、ViewPager初始化
2、ViewPager显示item
3、ViewPager滑动控制
为什么说有别于其他文章呢?因为通常源码都是从我们应用的代码入口进行分析的(以ViewPager为例:是从ViewPager.setAdapter()这个方法开始的),而对其他部分抱着“不求甚解”的态度。这实际上马克思主义理论中矛盾论之抓住主要矛盾理论的运用,所以这也是无可厚非的。我这里硬是要重新布局文章结构,加了其他内容,这难免有“画蛇添足”之嫌,但是真的是这样的吗?
下面我们正式进入源码分析,开车了!
我们都知道ViewPager是一个出色的容器,也就是ViewGroup的孩子。ViewGroup虽然是View的子类,但是他拓展了View做不到的功能。View不能包含其他View,只用负责自己的一亩三分地就行,位置定下来的话,也不会在绘制的时候受到其他控件的影响。而ViewGroup则可以包含View,也可以包含ViewGroup,所以他需要给他的孩子划出一亩三分地(在ViewGroup给出准确的宽高值时),虽然责任多了,但是真正绘制他只需要绘制自己的一些装饰,而子View的绘制,他将委托给View自身绘制。(关于View的绘制可以看相关博客,或者下文也还有提及)。
1、 ViewPager初始化
当我们在Activity的onCreate的setContentView方法将Xml布局设置给视图的时候,此时,ViewPager会被初始化,会按照如下方法进行初始化:
ViewPager构造方法 -> initViewPager -> onAttachedToWindow -> onMeasure -> onSizeChanged -> onLayout -> draw -> onDraw
*注:以上方法有的不止调用一次,调用整体顺序按照上述步骤。想知道为什么的可以移步“最详细的安卓绘制分析”。 *
ViewPager在构造方法里调用了initViewPager这个初始化方法
void initViewPager() {
setWillNotDraw(false);//重写onDraw需要调用,ViewGroup默认true
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);//孩子没有的时候才有
setFocusable(true);
final Context context = getContext();
mScroller = new Scroller(context, sInterpolator);
final ViewConfiguration configuration = ViewConfiguration.get(context);
final float density = context.getResources().getDisplayMetrics().density;
mTouchSlop = configuration.getScaledPagingTouchSlop();//滑动阈值
mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
//左右边界效果
mLeftEdge = new EdgeEffect(context);
mRightEdge = new EdgeEffect(context);
//其他省略。。。。。。
}
在初始化方法中,首先调用了 setWillNotDraw()这个方法,并且将其设置false,这个默认是true,意味着,ViewPager这个ViewGroup会执行draw()的绘制方法。
然后是setDescendantFocusability(FOCUS_AFTER_DESCENDANTS),这个方法对获取焦点规则作了明确,就是在孩子都没有获取焦点的时候才获取(after descendants);
还配置了一些初始化的变量,涉及滑动的阈值,速度的范围,边界效果(EdgeEffect),以及边界的处理(ViewCompat.setOnApplyWindowInsetsListener),以及辅助功能分发配置(ViewCompat.setAccessibilityDelegate),这些在View详解系列文章会提到,这里不是重点,暂时不提 。
初始化完成后还没有粘贴到真正的窗口Window上,所以之后还会调用回调 onAttachedToWindow方法。这个方法很简单,只是重置了一个变量mFirstLayout
@Override
protected void onAttachedToWindow() {
Log.e(TAG,"onAttachedToWindow start");
super.onAttachedToWindow();
mFirstLayout = true;
Log.e(TAG,"onAttachedToWindow end");
}
这个变量是一个标记位,对是否第一次执行布局操作layout进行标记,这个时候没执行,所以重置为true。这个变量,读者先记着,在布局方法,滚动判断和设置适配器后会用到。
然后老生常谈的View绘制三部曲onMesure,onLayout,onDraw,当然由于上边打开了允许draw调用,所以在onDraw前还会调用draw方法。
我们依次来看(记住,这个时候,我们还没有给ViewPager添加子View,所以只是在测量绘制自身),首先是onMeasure()方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//存储宽高测量值
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));
//获取子View的宽高运算省略
/*
* Make sure all children have been properly measured. Decor views first.
* Right now we cheat and make this less complicated by assuming decor
* views won't intersect. We will pin to edges based on gravity.
*/
int size = getChildCount();
for (int i = 0; i < size; ++i) {
//由于此时没有子View,所以这个对子View的测量放在下面分析,这里省略
}
//组装子View的宽高省略,下边分析
// Make sure we have created all fragments that we need to have shown.
//这里标记的mInLayout是为了在计算排列子View的时候避免在添加和删除子View而产生冲突
mInLayout = true;
//这里透露一下,populate是ViewPager原理的核心关键方法,下边会重点说
//这里是没有子View。所以对子View的计算很快就会结束。
populate();
mInLayout = false;
// Page views next.下边是委托子View去测量自己,这个时候子View所以先省略
size = getChildCount();
for (int i = 0; i < size; ++i) {
//子View不存在
}
}
这样看来,在一开始的onMeasure方法里并没有做什么,测量了下自身,这也符合我们的理解,并且还计算了子View的位置,这里也是没什么必要的。由于开始的size是0,0,这个时候有了新的值,所以之后执行onSizeChanged()方法
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// Make sure scroll position is set correctly.
if (w != oldw) {
//如果两值不同,就重新计算滚动位置,这里刚初始化时就是原始位置
recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin);
}
}
在完成size改变的方法之后,会触发onLayout执行操作,这是三部曲里边的布局方法,在这个时候没有子View的话,就是在为自身做布局,在之后提供了子View之后,才会引发复杂的布局过程,这个时候不需要,我这里省略:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int count = getChildCount();
int width = r - l;
int height = b - t;
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
final int scrollX = getScrollX();
int decorCount = 0;
for (int i = 0; i < count; i++) {
//这个时候没有子View,内部布局暂时省略。。。
}
final int childWidth = width - paddingLeft - paddingRight;
// Page views. Do this once we have the right padding offsets from above.
for (int i = 0; i < count; i++) {
//这个时候也不需要分析子View的布局
}
mTopPageBounds = paddingTop;
mBottomPageBounds = height - paddingBottom;
mDecorChildCount = decorCount;
if (mFirstLayout) {
//在第一次布局的时候将位置滚动到当前全局变量的位置
scrollToItem(mCurItem, false, 0, false);
}
//这个时候,将第一次布局设置为false,每次都重置,实际上没有必要,不过不耗时
mFirstLayout = false;
Log.e(TAG,"onLayout end");
}
onLayout执行完成之后,就完成了布局,测量布局都完成之后,就可以绘制了。下面看onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw the margin drawable between pages if needed.
if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {
//这个时候mItems没有添加任何item进去,所以size一定为0
//(从上边初始化的过程并没有对mItems进行设置,添加,所以为0)
}
}
好的,在初始化的全过程我们已经分析完成了。所以我们第一阶段分析就到这里,在这个部分因为没有设置子View,所以省略了全部关于子View的部分,但是唯一一点的是mFirstLayout这个时候的boolean值为false,已经完成了第一次布局。
2、 ViewPager显示item
上边的分析我们走了一遍,因为没有子View,所以整个绘制的过程,并没有做出实质性的绘制。把容器给配置好了,现在需要添加孩子了。而且,这也是最重要的一部分内容,涉及ViewPager的缓存机制,预加载机制,以及加载与销毁的时机的分析。
在代码中如果我们没有调用ViewPager的setAdapter()方法,ViewPager并不会显示出子View,即使你已经创建了PagerAdapter适配器。所以我们断言:ViewPager生效,触发子View加载与绘制。
下面我们来看,源码
public void setAdapter(@Nullable PagerAdapter adapter){
//mAdapter全局变量一开始是null,所以这个时候不会走下面这个重置方法
//在重新给ViewPager设置新的Adapter的时候就会触发下面的重置
//这里我们虽然是第一次设置,但是由于重置也是通过这个方法,所以这里也一并分析
if (mAdapter != null) {
//下面这个方法是给PagerAdapter设置观察者,这个是双向监听的重要方法
//这里由于之前设置的置为空
mAdapter.setViewPagerObserver(null);
//标记PagerAdapter方法的状态,这个是标记开始
//可以我们在页面实现的PagerAdapter里覆写这个父类方法,可以在ViewPager正式显示子View之前干点什么事,在第一次加载的时候特别耗时的情况下,显示进度条什么的。
mAdapter.startUpdate(this);
for (int i = 0; i < mItems.size(); i++) {
//这里我们假设是更新子View,所以这个时候mItems长度不为0
//也就是有子View,所以这个时候需要销毁
//而具体怎么销毁,我们已经非常熟练了,是我们实现PagerAdapter必须实现的方法,销毁的具体逻辑交给用户来处理
//
final ItemInfo ii = mItems.get(i);
mAdapter.destroyItem(this, ii.position, ii.object);
}
//这里结束更新
mAdapter.finishUpdate(this);
//清空子View容器
mItems.clear();
//这个时候是移除所有子View,当然要刨除掉DecorView
//这个DecorView是装饰ViewPager的,默认是一旦设置了就会一直存在
//在我们开发过程中较少使用到,后边我们会介绍下用法
removeNonDecorViews();
//重置当前位置为0,滚动到0,0
mCurItem = 0;
scrollTo(0, 0);
}
//以上的逻辑是重置Adapter过程,到这里就结束了
//下面的过程就是在设置新的了
//将全局变量保存到局部变量
final PagerAdapter oldAdapter = mAdapter;
//把我们设置给的赋值给全局变量
mAdapter = adapter;
mExpectedAdapterCount = 0;
//如果我们设置了空的,就会忽视,并不会报错
if (mAdapter != null) {
//初始化观察者
if (mObserver == null) {
mObserver = new PagerObserver();
}
//上边重置设为null了,这个时候又重新设置进去了
mAdapter.setViewPagerObserver(mObserver);
//这个标志是为了避免下面populate这个方法状态冲突
//populate这个方法我们下面着重介绍
mPopulatePending = false;
//全局变量设为局部变量
final boolean wasFirstLayout = mFirstLayout;
//这个时候将首次布局置为true
//我们在onLayout的时候把这个设为false,这个时候我们又重置为false了
mFirstLayout = true;
上边这个变量置为0了,这个时候就把PagerAdapter我们实现的getCount()方法,这个之所以没起名叫mChildCount是因为,可能用户设置了很多子View,但是由于我们实现的getCount()方法并没有返回全部的数量,不能显示全部数量,所以起名叫mExpectedAdapterCount,也就是开发者期望显示的子View的数量。
mExpectedAdapterCount = mAdapter.getCount();
if (mRestoredCurItem >= 0) {
//当保存的当前位置 >0
//重新保存状态,这个是关于状态值得保存,用于意外恢复数据
mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
//设置当前的位置为保存的位置
setCurrentItemInternal(mRestoredCurItem, false, true);
//这个时候就可以把这个状态值设置为-1
mRestoredCurItem = -1;
//然后把状态值,和类加载器置为null
mRestoredAdapterState = null;
mRestoredClassLoader = null;
} else if (!wasFirstLayout) {
//这个变量是之前的mFirstLayout的Boolean值,我们知道首次初始化之后是false。再取反,就是true,所以第一次设置之后就会进入这个方法
//当然,这个时候是没有保存的当前位置需要恢复
//populate在上边说到那个mPopulatePending变量的时候说了
//这个方法是整个ViewPager的灵魂方法,也是ViewPager在处理子View逻辑的关键方法
populate();
} else {
//如果上边的条件全不满足,就会调用下边的引起重新测量重新布局的方法
requestLayout();
}
}
//下面这个方法就是在我们设置AdapterChangeListener监听Adapter变化的时候,希望我们收到的监听过程
//如果有设置监听,mAdapterChangeListeners一定不为空
//所以下边就会从这个列表中遍历通知所有的listener
if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) {
for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) {
mAdapterChangeListeners.get(i).onAdapterChanged(this, oldAdapter, adapter);
}
}
}
setAdapter()这个方法,既可以第一次设置,也可以重置,所以针对这两种状态,还要区别对待,如果是更新的话,就先走更新的逻辑,如果不是更新的话,就跳过更新的步骤,直接进入配置新的Adapter。而Adapter的变换我们有个关于Adapter改变的监听,这个时候需要给调用者返回旧的Adapter和新的Adapter(通过如下方法回调)。
mAdapterChangeListeners.get(i).onAdapterChanged(this, oldAdapter, adapter);
接着Adapter的设置:
将adapter赋值给mAdapter全局变量,并且在不为空的情况下进行处理(如果为空就忽视)
1、实现双向绑定:初始化PagerObserver,并且将其设置给适配器,用于客户端在数据改变的情况通过调用notifyDataSetChanaged方法进行数据变更。这个方法就是调用我们初始化的PagerObserver的onChanged方法进行,onChanged方法内部则是调用ViewPager的包方法dataSetChanged,从而实现ViewPager数据变更。ViewPager里边设置Adapter,Adapter里设置了Observer,从而实现双向绑定。
2、重置变量:重置mPopulatePending变量为false,这个变量是用于在执行核心方法populate()的时候避免与滑动事件产生冲突;重置mFirstLayout为true,这个是在执行到下边条件判断的第一个条件时用到的。而当前的重置前的mFirstLayout方法会保存到局部变量wasFirstLayout里。条件判断的第二个条件正是和这个变量相关;还有重置mExpectedAdapterCount这个变量为我们实现的Adapter里getCount()方法返回的值。
3、条件判断,分情况执行:这里分了三种情况,第一种是处理意外,保存了上次的状态,这个是我们在ViewPager中覆写View的onSaveInstanceState和onRestoreInstanceState方法中收到的影响,在save方法中我们保存意外关闭的重要状态值,而在重新恢复的restore方法中我们会将mRestoredCurItem设置为上次保存的位置,所以一旦有上次保存,所以mRestoredCurItem一定不小于0,这个时候就会进入恢复流程,进入第一个条件语句。里边主要是跳转到上次保存的item位置,具体这里不分析了;第二种是wasFirstLayout为false的情况,也就是在onLayout方法执行过了的时候(因为在onLayout方法中会将赋值给wasFirstLayout的全局变量mFirstLayout设置为false,标志着第一次layout的结束),这个时候会调用到核心方法populate()进行缓存计算,populate是ViewPager的灵魂方法,下边会着重分析;最后一种情况就是当setAdapter执行方法在onLayout方法中变量置为false之前执行了,这个时候就会requestLayout引起重新的测量和布局。所以会执行onMeasure(),onLayout()方法。而在onMeasure()方法中会调用populate()方法。
4、Adapter改变监听分发:最后一部分就是在对Adapter改变的监听,这里需要注意一点,当Adapter是第一次设置,这个时候oldAdapter为null,在监听回调那里我们可以通过null值来判断是否是第一次。
整体的setAdapter方法流程我们捋了一遍,下面我们就进入上边一直强调的populate()方法:
populate()方法--------上代码
void populate() {
populate(mCurItem);
}
调用带参数的方法,传入的是全局变量mCurItem,第一次设置Adapter的时候mCurItem为0;
提示:下边为完整源码,可以快速浏览,源码结束会有详细分析
void populate(int newCurrentItem) {
//创建一个信息类,包含了子item构建所需要的全部信息
ItemInfo oldCurInfo = null;
//因为传进来的正是mCurItem,所以下边条件不符合
if (mCurItem != newCurrentItem) {
oldCurInfo = infoForPosition(mCurItem);
mCurItem = newCurrentItem;
}
//如果用户设置空适配器,排序子View然后拦截返回
if (mAdapter == null) {
sortChildDrawingOrder();
return;
}
//因为这个方法是在多种情况下调用的,所以为了避免和滑动引起的冲突,需要延迟这部分绘制
// Bail now if we are waiting to populate. This is to hold off
// on creating views from the time the user releases their finger to
// fling to a new position until we have finished the scroll to
// that position, avoiding glitches from happening at that point.
if (mPopulatePending) {
if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
sortChildDrawingOrder();
return;
}
//在粘贴到窗口上之前也不能去构建,否则就会使得我们保存的视图层次状态不生效
// Also, don't populate until we are attached to a window. This is to
// avoid trying to populate before we have restored our view hierarchy
// state and conflicting with what is restored.
if (getWindowToken() == null) {
return;
}
mAdapter.startUpdate(this);//调用开始更新的方法,是一个标记方法
final int pageLimit = mOffscreenPageLimit;//获取预加载数量,这里开始出现了缓存策略相关的变量了
final int startPos = Math.max(0, mCurItem - pageLimit); //确定绘制子View的起始位置,起始位置是0和当前位置-缓存数量的较小值
final int N = mAdapter.getCount();//获得用户需要显示的子View的数量
final int endPos = Math.min(N - 1, mCurItem + pageLimit);//确定绘制子View的结束位置,结束位置是最后一页和当前位置+缓存数量的较大值
if (N != mExpectedAdapterCount) {//数量不一致时,数据发生变化,但是没有提示ViewPager更新
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());
}
// Locate the currently focused item or add it if needed. 定位当前位置,并且及时添加新的子View
int curIndex = -1;
ItemInfo curItem = null;
//第一次加载mItems的size为0
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) {
//关键代码,增加子View,只添加当前View
curItem = addNewItem(mCurItem, curIndex);
}
// 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.
//判断执行一侧循环次数有两个关键,必须同时满足才会销毁子View,否则将会不断创建
//第一个是:必须满足至少两倍的ViewPager宽度占有率
//第二个是:循环的子View索引是否超出缓存的边界
if (curItem != null) {
//左边判断
float extraWidthLeft = 0.f;
int itemIndex = curIndex - 1;//前一个
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
final int clientWidth = getClientWidth();//ViewPager宽度-左右padding的值
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);
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;
}
}
//右边判断
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);
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;
}
}
}
//为后续布局和绘制做偏移校准,计算起始偏移量
calculatePageOffsets(curItem, curIndex, oldCurInfo);
//告诉适配器当前的item位置及信息
mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
}
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);
}
}
//标记配置基本参数结束,可以通过适配器回调
mAdapter.finishUpdate(this);
// Check width measurement of current pages and drawing sort order.
// Update LayoutParams as needed.
//将保存在ItemInfo里的信息赋值给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();
//遍历获取焦点
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;
}
}
}
}
在上边的源码中,解释了主要代码的功能作用,但是这样过一遍,很容易会忘记,所以我们总结一下:
分为以下几个部分:
A、特殊情况的拦截及处理
B、计算缓存边界
C、获取需要显示的Item信息
D、分别循环计算显示Item左边和右边需要缓存的Item信息,创建没有的,销毁多余的
E、计算偏移值,为布局做准备
F、将保存在ItemInfo里的信息赋值给LayoutParams
G、为转场动画准备,遍历获取焦点
在整个populate中,在计算缓存前,有四次对特殊情况的处理,有三次是在Adapter的startUpdate()标记方法之前,一次在其之后。这四种情况分别是:
1、mAdapter == null,用户还未设置适配器,这个时候停止处理并返回,这种情况是在ViewPager初始化的时候,在调用onMeasure()方法中,会调用populate(),方法,而此时populate()方法的执行没有意义,避免浪费性能及其他错误,这里直接返回。
2、mPopulatePending == true 的情况,这个值默认是false,而且在setAdapter中也会重置为false,只有在以下两种情况会为true:一种是在onTouchEvent当手指离开屏幕的时候,另外一种是结束虚拟滑动endFakeDrag的时候,看这个两个方法,可知都是和滑动相关的,而且,从调用位置我们发现,正好是手指离开屏幕,或者模拟手指离开屏幕,而产生的fling到新的位置,这个时候避免出错,延迟绘制。(执行sortChildDrawingOrder(),这个方法和我们转场动画有关)
3、getWindowToken() == null,这个方法获得的token正是当前View粘贴到(attach)到的window的token,如果为空,则说明此时View树还未与window产生联系,主页面还未调用onResume方法。这个时候也不能计算缓存。
4、N != mExpectedAdapterCount ,我们上边刚刚在setAdapter中提到这个变量的赋值,是调用Adapter的getCount()方法,而N也是用相同的方法获得的,如果同一个方法获得的数据个数不同,那说明用户修改了数据源,而并未调用notifyDataSetChanged()方法来告知ViewPager同步,所以谷歌这个给开发者抛出了异常,从而让开发者意识到错误的原因并及时修改。
对特殊情况处理完就正式进入缓存计算了,其实上边总结的B、C、D这三部分是一部分,但是代码过多为了便于理解,我拆开了。
缓存计算是在两个方法中进行的:
mAdapter.startUpdate(this);
mAdapter.finishUpdate(this);
这两个方法,是标记方法,我们可以在实现的Adapter中覆写这两个方法,来掌握时机,做一些事情。。。
缓存计算正式开始了————
一个重要的缓存配置变量mOffscreenPageLimit,在ViewPager(五)中我们介绍过这个变量的配置方法,可以定制我们的缓存数量
根据这个配置变量,我们计算了startPos,endPos,顾名思义!就是缓存的起始边界咯
边界有了就要加载Item了,但是在此之前还是要介绍几个重要的功能,重要哦~
ItemInfo 缓存条目的详细信息类,类中有几个属性分变量,分别是object(这个信息类中的View对象),position(item的位置),scrolling(boolean值,是否滑动状态),widthFactor(宽度因子,覆写Adapter的getPageWidth方法的返回值,默认为1f),offset(item偏移量)
mItems 一个属性变量,ArrayList类型,里边存放的为ItemInfo
在找当前要显示的item信息的时候,首先是从容器里找。因为populate是多个地方调用,不一定是setAdapter才调用的,所以可能mItems这个变量开始有元素,那么久尝试去里边找,找到就命中
for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
final ItemInfo ii = mItems.get(curIndex);
if (ii.position >= mCurItem) {
if (ii.position == mCurItem) curItem = ii;
break;
}
}
如果找不到就去创建,即curItem为null
if (curItem == null && N > 0) {
//关键代码,增加子View,只添加当前View
curItem = addNewItem(mCurItem, curIndex);
}
好了一个关键方法出来了,就是addNewItem(),我们来看源码
ItemInfo addNewItem(int position, int index) {
ItemInfo ii = new ItemInfo();//创建信息类
ii.position = position;//赋值位置信息
ii.object = mAdapter.instantiateItem(this, position);//赋值我们包含的对象,这个方法很熟悉
ii.widthFactor = mAdapter.getPageWidth(position);//赋值子View的宽度因子,方法也是我们覆写的,默认是1.0f
if (index < 0 || index >= mItems.size()) {
//依次添加
mItems.add(ii);
} else {
//添加到指定位置
mItems.add(index, ii);
}
return ii;
}
里边有我们熟悉的方法instantiateItem,从而证明我们创建的子View确实就是从这个方法获得的,所以,我们在实现这个方法的时候,返回什么,他这就缓存什么。
另外还调用了getPageWidth,这个方法是实现ViewPager高级功能时实现的,就是设置当前item的显示宽度,是个比例值。(这里先剧透下,这个宽度因子,你不能设置2或者跟大,否则就会显示异常,而如果你设置太小的话,所有item能在一屏内显示的时候,就会发生鬼畜的现象,哈哈,对,是鬼畜。为什么呢,下边的源码能找到答案)
这个时候如果我们正确设置实现了instantiateItem这个方法并返回,那么此时的
curItem将不再为空,需要显示的实现结束了就在这个判空里边我们实现他的左右缓存,由于左和右实现原理相同,只是方向相反,这里只分析左实现,感兴趣的读者,可以自行分析右实现(源码是先左,后右)。
for循环是实现的关键,但是在这个之前是一些准备工作
储存一个循环处理变量,用于和比较:
float extraWidthLeft = 0f;
获得了当前位置的前一个位置
int itemIndex = curIndex - 1
在mItem缓存列表中,找一找,有没有符合条件的,没有返回null
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
计算一下这个item的左边还需要剩余空间(leftWidthNeeded )多少,是个比例关系,我们按照默认来看(即curItem.widthFactor == 1.0f),leftWidthNeeded就是1+左padding占的宽度比例值,如果我们假设padding也没有,那么就是1了。
final int clientWidth = getClientWidth();
final float leftWidthNeeded = clientWidth <= 0 ? 0 :2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
准备工作就绪,就进入循环,循环是从当前位置减1开始的,依次递减,到0结束,循环分三种情况(三个条件对应)
1、extraWidthLeft >= leftWidthNeeded && pos < startPos:extraWidthLeft 是一个起始为0,不断增加的量,所以&&符号左边开始一定不符合,而&&符号右边就是控制缓存边界,而startPos正是我们计算的缓存左边界;
2、ii != null && pos == ii.position :&&符号左边是我们循环获得的ItemInfo不为空,右边是保存的位置和当前的循环位置相等
3、第三个条件时else,即除1、2外的其他情况
针对以上三个条件,我们动态来分析(以下示意图展示的都是mItems变化)
第一种分析:我们是第一次设置Adapter,这个时候mItem除了有当前位置一个元素外,没有其他。并且,循环一次都不会执行
我们按照缓存策略,默认缓存为1,右边虽然我们不分析,但是可以知道这次populate之后会缓存第二个Item。
第二种分析:这个时候我们在设置到第二个页面时(或滑动到下一页),当前的position为1,也就是第二个位置,我们假设分析一直到达不了右边界,这个时候的左侧有一个item,可以进入循环,我们发现符合第二个条件,因为ii已经缓存到Items里边了,所以不为null,并且position和pos均为0,这时我们看下符合条件我们做了什么
else if (ii != null && pos == ii.position) {
extraWidthLeft += ii.widthFactor;
itemIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
将widthFactor,给extraWidthLeft变量加上去,此时itemIndex为0,再自减1的话,就是-1,所以下边的ii为null。然后进入下次循环,发现循环变量为-1,不符合循环条件,退出循环。
第三种分析:我们假设来到了第三页,这个时候的curIndex = 2,并且itemIndex = 1,循环可以执行,循环变量pos=1,这个时候命中第二个条件,把宽度因子叠加到extraWidthLeft 上,将itemIndex自减,这个时候itemIndex为0了,并且也是可以从mItems里边获得到的;然后下一次循环,pos=0;这个时候,我们发现命中了第一个条件(extraWidthLeft >= leftWidthNeeded按照leftWidthNeeded = 1满足,并且pos < startPos也满足,startPos此时为1),所以看第一个条件里边做了些什么:
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, pos, ii.object);
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos
+ " view: " + ((View) ii.object));
}
itemIndex--;
curIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}
这是三个条件里最多代码的,首先判断ii是否空,为空就退出当前循环,不为空继续向下,呵,又一个判断,&&左边的是相等的,假设这个时候已经结束滑动了,所以&&右边的也符合,执行判断内部逻辑,首先我们移除了mItems中itemIndex的位置的缓存,这个时候ItemIndex为0。然后看到了我们另一个熟悉的方法destroyItem(),这个方法也很重要,正是我们实现适配器的重要方法,我们可以在元素销毁的时候干点什么,定义销毁策略。然后是两个变量的自减,所以这个时候curIndex为1(因为mItems进行了remove操作,导致index和position错位,为了下边右缓存计算不出现问题,需要矫正这个变化,差距是1,所以这里curIndex进行了自减),itemIndex为-1。获取到的ii为null。然后循环不符合条件了,所以循环结束。
第四种分析:有的读者一定会问,为什么没有执行第三个条件,这个时候在第三种分析的基础上,我们换方向,这个时候我们假设从第三页回到了第二页,这个时候由于我们之前删除了一个元素,所以我们回到之前确定curIndex的代码的地方,由于这段代码很容易让人不理解,所以这里我们不厌其烦的再copy以下咯:
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;
}
}
对的,就是这个for循环,因为我们在第三种分析的时候将本来mItems的第0个元素remove掉了,导致原来实际位置为1的元素,在mItems集合中,放到了第0个位置,所以我们此时要显示实际位置为1的item,循环的第一个curIndex为0的情况获取到的ii就会命中(他的位置position恰好和mCurItem一致,这个时候mCurItem为1)。所以局部变量在进入下边的左右缓存计算处理时的curIndex =0;
接着进入左右缓存,此时curIndex = 0,itemIndex = -1,所以准备就绪后的ii,为null,所以左边缓存的条件判断,前两个条件都将不符合,那么就会命中第三个条件,下面我们来看,最后一个条件的源码:
else {
ii = addNewItem(pos, itemIndex + 1);
extraWidthLeft += ii.widthFactor;
curIndex++;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
由于此时itemIndex为-1,而实际上应该0,所以为了纠正,这里加了个1,刚好重新获得了在第三种分析中被销毁的实际位置是0的Item。另外还需要注意一点是,由于此时curIndex是0,而实际上应该是1.所以这里又做了一次自增调整,而下边的ii是null,此时循环变量pos是0,下次循环是-1,不满足循环条件,退出循环。这里需要提醒的是,从右往左返回时,由于mItems数组会删除前边的额元素,导致错位,所以,这里在第三个条件里边做了矫正。也正因为上边的矫正,才会使得下边的右边缓存的计算才不会出现错误。
第五种分析:其实第五种分析是针对开发者在代码中调用setOffscreenPageLimit,改变缓存配置变量mOffscreenPageLimit,我们知道这个值,默认是1,正如我们前面四种分析的采用的缓存策略一样,但是我们想象这样一种情况,如果我们当前显示的是第四页,也就是mCurIndex = 3,这个时候我们设置了缓存配置变量为2,这个时候mItems中间应该保存有序号为1,2,3,4,5(假设右边没有到头),而由于默认的策略是1,而mItems中保存的仅是序号为2,3,4三个Item元素,这个时会先触发第二个条件,从缓存中获取,然后将itemIndex自减,针对左边接下来就会就会触发第三个条件,导致创建元素并插入到正确的位置;而如果又重新设置了缓存配置变量为1,这个时候针对左边缓存又需要销毁一个元素,还是会先进入第二个条件,itemIndex,然后进入第一个条件,进行remove。这样我们就知道了这个setOffscreenPageLimit是如何影响到ViewPager的缓存策略的。(这里如果不明白的读者,可以按照上边几种分析,结合代码再走一遍)
ps:左右缓存分析一样,右缓存读者可以自行分析~
对于缓存的分析是很重要的一块,也是一个理解的难点。我们已经突破了。接下来就是计算偏移值,调用calculatePageOffsets这个方法。方法主要分两大块,oldCurInfo!=null和oldCurInfo == null。就是初始化和滑动的两个过程,实际的逻辑取决于mCurItem这个属性变量与newCurrentItem这个参数是否相等。下面我们来分析这部分源码(源码较长,我把删减细节用文字代替):
private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
final int N = mAdapter.getCount();
final int width = getClientWidth();
final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
// Fix up offsets for later layout.
//为之后的布局修复调整偏移
--------第一部分--------
if (oldCurInfo != null) {
final int oldCurPosition = oldCurInfo.position;
// Base offsets off of oldCurInfo.
if (oldCurPosition < curItem.position) {
//省略源码
ii.offset = offset;
offset += ii.widthFactor + marginOffset;
}
} else if (oldCurPosition > curItem.position) {
//省略源码
offset -= ii.widthFactor + marginOffset;
ii.offset = offset;
}
}
}
//如果是初始化操作或者刷新当前页从这里开始执行
// Base all offsets off of curItem.
final int itemCount = mItems.size();
float offset = curItem.offset;
int pos = curItem.position - 1;
mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
mLastOffset = curItem.position == N - 1
? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;
// Previous pages前边的页,计算偏移
--------第二部分--------
for (int i = curIndex - 1; i >= 0; i--, pos--) {
//省略源码
ii.offset = offset;
if (ii.position == 0) mFirstOffset = offset;
}
offset = curItem.offset + curItem.widthFactor + marginOffset;
pos = curItem.position + 1;
// Next pages后边的页计算偏移
--------第三部分--------
for (int i = curIndex + 1; i < itemCount; i++, pos++) {
//省略源码
if (ii.position == N - 1) {
mLastOffset = offset + ii.widthFactor - 1;
}
ii.offset = offset;
offset += ii.widthFactor + marginOffset;
}
mNeedCalculatePageOffsets = false;
}
以上代码我分了三部分,第一部分是对当前的item的offset偏移量进行计算,第二部分是对当前位置的左边所有的item的offset偏移量依次进行计算赋值,第三部分是对当前位置的右边所有的item的offset偏移量依次进行计算赋值。
有个ViewPager属性关注下:
mPageMargin 这个变量是我们通过设置setPageMargin方法设置的,默认0;
如果第一次设置,或者刷新当前页,即oldCurInfo == null。这个时候当前位置不变,这个当前位置不用改变,但是由于有一种情况是针对缓存策略的改变,所以即使当前位置不变,但是左右两边的偏移会有变化,所以需要重新计算。
这里我们需要注意的是,代码中出现的curIndex,和pos两个变量,这里解释下:
curIndex:是针对mItems的序号,是当前要显示的item在List里对应的位置
pos:这个是针对ViewPager的所有Item,是真实的位置。
只有我们清楚的了解以上两点,才不会被不断的while循环搞混,而while循环只是在不断矫正list的curIndex和pos的匹配。他两虽然没有关系,但是通过mItems集合获取curIndex位置的ItemInfo,这个对象的position属性记录的正是真实位置。所以这样就建立起了联系,所以curIndex位置获取到的ItemInfo的postion属性比pos变量小,那就增大curIndex或者减小pos;反之,curIndex位置获取到的ItemInfo的postion属性比pos变量大,那就减小curIndex或者增大pos,之所以可以连续处理,是因为mItems存的item虽然对不上号,但是他们是连续的。
有了以上这个算法的基础,我们再来看calculatePageOffsets的源码,计算当前位置的偏移offset的时候先是判断了下是向左滑,还是向右滑,这个是通过对比
oldCurInfo的position和curItme的position的大小关系,分成了两种情况,里边都是根据上边的算法矫正之后,给当前的itme赋上正确的offset的值。
然后第二部分和第三部分是计算左边右边的偏移量。其实都是直接算mItems里保存的item的偏移量,但是都是相对真实的第0个item开始计算的,虽然有可能缓存策略会把前边的回收,但相对位置还是按照有他计算的。举个栗子:如果缓存是1的话,当前位置是3(右边没到头),那么mItems里边这时,会只有实际位置2,3,4。这个时候0,1会回收,但是计算的实际偏移值offset,这三个分别是:2,3,4,而不是0,1,2。(这个offset是宽度倍数关系)
随后在calculatePageOffsets还需要关注两个属性变量
private float mFirstOffset = -Float.MAX_VALUE;//用来标记是否到达了最左边
private float mLastOffset = Float.MAX_VALUE;//用来标记是否到达了最右边
这两个变量会在这个时候做矫正,而在滑动的时候做判断
mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
mLastOffset = curItem.position == N - 1? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;
只要当前显示的不是第一个或者最后一个,他们值会和curItem的offset设置了相应的计算关系。这个时候在滑动的时候就能很快判断是否到达了边缘,只要这两个变量不是默认值,就没有到边!
分析完calculatePageOffsets,我们返回调用他的地方(populate方法),接着向下:
连续调用
mAdapter.setPrimaryItem() 这个方法表征ViewPager马上要显示的Item信息
同样在适配器有个创建对象的方法instantiateItem,但是由于ViewPager的缓存策略导致创建的并不是要显示的,那么适配器为了解决用户回调当前显示的信息,而且是最及时的获取,就有了setPrimaryItem这个回调方法
接着调用
mAdapter.finishUpdate(this); 结束更新缓存,标记方法,与startUpdate成对出现
然后就是通过一个循环,将所有子View中的ItemInfo对象的widthFactor和position属性赋值给LayoutParams。如下:
lp.widthFactor = ii.widthFactor;
lp.position = ii.position;
然后是一个关于转场或者叫翻页动画的方法sortChildDrawingOrder();
代码不长:
private void sortChildDrawingOrder() {
//只有在设置transform的时候才会涉及这部分内容
if (mDrawingOrder != DRAW_ORDER_DEFAULT) {
if (mDrawingOrderedChildren == null) {
mDrawingOrderedChildren = new ArrayList<View>();
} else {
mDrawingOrderedChildren.clear();
}
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
mDrawingOrderedChildren.add(child);
}
//排序,位置关系及是否是decor
Collections.sort(mDrawingOrderedChildren, sPositionComparator);
}
}
整个方法执行的先决条件是mDrawingOrder != DRAW_ORDER_DEFAULT,只有满足这个条件才执行方法体的逻辑,看过之前博客的童鞋,也就是在转场动画的应用里,提到过,mDrawingOrder默认就是DRAW_ORDER_DEFAULT,而只有当调用了setPageTransform函数设置transform的时候,才会被赋值成DRAW_ORDER_REVERSE或者DRAW_ORDER_FORWARD,不管是哪个,都会导致条件满足。
代码很简单,依次添加子View到mDrawingOrderedChildren集合中,然后排序,所以我们只要知道他排序的规则是什么就行,看sPositionComparator(LayoutParams的内部类ViewPositionComparator):
static class ViewPositionComparator implements Comparator<View> {
@Override
public int compare(View lhs, View rhs) {
final LayoutParams llp = (LayoutParams) lhs.getLayoutParams();
final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams();
if (llp.isDecor != rlp.isDecor) {
return llp.isDecor ? 1 : -1;
}
return llp.position - rlp.position;
}
}
简单说下:如果比较的两个View一个是DecorView,一个不是,就按照参数前边的View的isDecor布尔值来排序。其他直接按照参数前边的位置减后边的位置,来决定排序。按照位置升序排序。
populate最后一步,遍历子View让其获得焦点,很简单设置获取焦点的规则为FOCUS_FORWARD:
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到这里就分析完了,但是读者你认为执行完populate就完事大吉,就能显示子View了吗?
答案是:还远远没完,还不能显示,至少我们分析还没有到位。那么还缺什么?这里提醒下,我们在执行完这个标记方法mAdapter.finishUpdate(this)后,我们多次调用了getChildCount(),这个方法是干嘛的,对,获取子View的个数,ViewPager是什么时候加载进来的子View的呢?你可能会觉得,我们不是调用addNewItem这个方法加进来了吗,说对了一半!为什么这样说?因为addNewItem只是将View加到了List集合里,而且还只是对象集合的一个属性,所以ViewGroup并没有收到他的子View。这个时候你一定有想法了,ViewGroup添加子View的方法:addView。你有想到这个方法什么时候调用的吗?也许你并不留意,把这句代码习惯性的写了。
揭晓:你在实现Adapter的时候,或者FragmentAdapter的默认的instantiateItem方法中,我们往往是从创建了View并返回,在这之中还有一句很关键的代码就是container.addView(),而container就是ViewGroup,他将自身传递过来,就是让开发者往里边添加子View的。而如果你一不小心漏了这句话,你会发现ViewPager什么都没有显示出来!
addView是ViewGroup的方法,ViewPager虽然有实现,但是重载的三个参数的,我们一般调用的是一个参数的,熟悉addView源码都童鞋,都知道,addView一个参数,两个参数,最终都会调用到三个参数的,所以还是会调用ViewPager实现的三个参数的addView方法,下面来看:
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (!checkLayoutParams(params)) {
params = generateLayoutParams(params);
}
final LayoutParams lp = (LayoutParams) params;
// Any views added via inflation should be classed as part of the decor
lp.isDecor |= isDecorView(child);
if (mInLayout) {
if (lp != null && lp.isDecor) {
throw new IllegalStateException("Cannot add pager decor view during layout");
}
lp.needsMeasure = true;
addViewInLayout(child, index, params);
} else {
super.addView(child, index, params);
}
if (USE_CACHE) {
if (child.getVisibility() != GONE) {
child.setDrawingCacheEnabled(mScrollingCacheEnabled);
} else {
child.setDrawingCacheEnabled(false);
}
}
}
其实这个实现是为了处理ViewPager搞出来的这个DecorView的,然后会根据是否在执行从Measure中调用的populate的方法(mInLayout在onMeasure方法中做了赋值操作),分成了两个分支addViewInLayout和super.addView。
addViewInLayout只是将View加到ViewGroup的View[]数组中而不会调用requestLayout引起重新测量布局,但是这部分也不能不管,刚刚通过这种方法加进去的View还没有测量,所以没办法layout,这个时候在onLayout里边会有相应的处理。这里先提一嘴,等会到onLayout方法里看源码就知道了。
我们看ViewGroup的addView方法
public void addView(View child, int index, LayoutParams params) {
//省略部分源码
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
我留了最关键的几行代码,没错requestLayout,invalidate,这个有过自定义View的经验的童鞋一定不陌生,这个时候就会引起重新测量,布局,绘制。
在博客第一部分的初始化里边说过onMeasure方法,onLayout方法和onDraw方法,这里就不说说过了,我们只看其中对子View处理的部分:
首先是onMeasure:
//之前省略了DecorView的处理源码
//这个就是我们在addView里边用于判读添加子View执行方式时用到的变量
//是通过这里赋值的,为True说明正在执行下边的或者即将执行populate方法
//默认为false
mInLayout = true;
populate();
mInLayout = false;
// Page views next.
size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
if (DEBUG) {
Log.v(TAG, "Measuring #" + i + " " + child + ": " + mChildWidthMeasureSpec);
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp == null || !lp.isDecor) {
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
child.measure(widthSpec, mChildHeightMeasureSpec);
}
}
}
到这里开始我们才真正开始绘制View的流程,遍历所有不是decorView的子View,委托他们自己去测量
这里onSizeChanged就不分析了,就是重新滑动到正确的位置
然后是onLayout方法:
//省略对decorView的处理源码
final int childWidth = width - paddingLeft - paddingRight;
// Page views. Do this once we have the right padding offsets from above.
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
ItemInfo ii;
if (!lp.isDecor && (ii = infoForChild(child)) != null) {
int loff = (int) (childWidth * ii.offset);
int childLeft = paddingLeft + loff;
int childTop = paddingTop;
if (lp.needsMeasure) {
// This was added during layout and needs measurement.
// Do it now that we know what we're working with.
lp.needsMeasure = false;
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidth * lp.widthFactor),
MeasureSpec.EXACTLY);
final int heightSpec = MeasureSpec.makeMeasureSpec(
(int) (height - paddingTop - paddingBottom),
MeasureSpec.EXACTLY);
child.measure(widthSpec, heightSpec);
}
if (DEBUG) {
Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
+ ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
+ "x" + child.getMeasuredHeight());
}
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
}
}
}
mTopPageBounds = paddingTop;
mBottomPageBounds = height - paddingBottom;
我们看到了计算的offset派上用场了,由于offset是倍数值,所以它在layout计算距离的时候是先乘上一个childWidth,而这个宽度值时将padding减去的值,所以也就保证了padding能生效。
其他部分就是常规的计算相对位置的,并委托给View的layout完成最终测量,上边提到过addView的两种添加方法,addViewInLayout并不能引起测量和布局,但是也不能不量啊,这里就给了答案,我们在addViewInLayout前将子View的params的needsMeasure变量设置为true。在onLayout里我们在判断needsMeasure的值为true,在这里我们让其测量,然后再布局。
draw和onDraw方法我们之前都有分析,这里就不做分析,invalidate方法会让ViewGroup向下引起子View的绘制,位置大小有改变的才绘制。
最终见到我们ViewPager显示的子View
3、ViewPager的滑动控制
缓存处理时ViewPager很重要的一点,然而ViewPager不是一个静态控件,而是一个动态展示控件,那么它是如何在滑动中保证依然高效流畅的体验呢?这个是我们在本篇博客最后需要分析的问题,涉及很多处理滑动方面的知识,如果还不具备可以先看些基础的东西,等有了储备再看更容易,不过我尽量通俗的说,希望新手也能看的明白大概流程。
------------------我们简单回顾下基础知识--------------
我们都知道手指触动屏幕会产生触摸事件,这个是由硬件层捕获,回传给软件层的,在我们熟悉的界面中,事件是从Activity开始的(这里以Activity举例),然后依次ViewGroup -> ViewGroup -> … ViewGroup -> View 按照这条线不断向下传,这个方向的重点是View(如果中间有事件消费,就不会再往下传了),如果到了View仍没有事件消费,会反方向向上。
第一个方向传递涉及dispatchTouchEvent和onInterceptTouchEvent(这个方法ViewGroup有,View没有)(这个方向上的onInterceptTouchEvent方法返回true会导致onTouchEvent方法执行)
第二个回传的方法涉及到除上边一个方向的方法,还有onTouchEvent方法,如果返回时super的话,也就是默认,那就原路返回。
整个事件分发就ViewPager源码分析来讲,他是个ViewGroup,所以会先传递到dispatchTouchEvent,ViewPager没有覆写,在父类ViewGroup中有:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//删除了大部分的源码
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
return handled;
}
事件的处理是从down事件开始的,所以只截取了down事件的源码,如果child没有主动调用requestDisallowInterceptTouchEvent,就会执行下一个重要的时间分发方法
intercepted = onInterceptTouchEvent(ev);
好了我们接着看onInterceptTouchEvent方法,这个方法ViewPager有实现,代码较多这里我就不贴了,直接分析
首先看down事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//删除部分源码
case MotionEvent.ACTION_DOWN: {
//删除部分源码
if (mScrollState == SCROLL_STATE_SETTLING
&& Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
//删除部分源码
mIsBeingDragged = true;
} else {
mIsBeingDragged = false;
}
return mIsBeingDragged;
}
这里ViewPager只是对如果之前是SCROLL_STATE_SETTLING个状态的事件进行拦截,其余的一概不拦截(在down事件)
下面我们再看move事件
case MotionEvent.ACTION_MOVE: {
//删除部分源码
if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
mIsBeingDragged = true;
//删除部分源码
} else if (yDiff > mTouchSlop) {
mIsUnableToDrag = true;
}
break;
}
这部分ViewPager会在移动距离大于mTouchSlop,并前横向移动距离大小是竖向的两倍的情况也会拦截,这就意味着横向滑动将全部交给ViewPager处理。
然而这个move事件基本上不会传到onInterceptTouchEvent,这个方法,我们知道,如果子View不处理的话,或者直接拦截返回true,都会重新调用当前ViewGroup的onTouchEvent方法,这是个重点方法,我们去看下,ViewPager对于onTouchEvent也实现了:
这里有几个特殊情况的判断
1、官方注释的很清楚,就是我们在执行fakeDrag的时候,也要消耗这个事件,并且不往下执行
if (mFakeDragging) {
// A fake drag is in progress already, ignore this real one
// but still eat the touch events.
// (It is likely that the user is multi-touching the screen.)
return true;
}
2、到屏幕的边界,我们不处理
if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
// Don't handle edge touches immediately -- they may actually belong to one of our
// descendants.
return false;
}
3、适配器还未设置,或者需要显示数量为0
if (mAdapter == null || mAdapter.getCount() == 0) {
// Nothing to present or scroll; nothing to touch.
return false;
}
除以上三种情况外,事件会顺利进入对时间类型的判断
首先是onTouchEvent的down事件处理
case MotionEvent.ACTION_DOWN: {
mScroller.abortAnimation();
mPopulatePending = false;
populate();
// Remember where the motion event started
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
mActivePointerId = ev.getPointerId(0);
break;
}
很简单,停止滚动,恢复变量,执行一次populate,初始化手指初始的位置,id
接下来:onTouchEvent的move事件,如果在Intercept方法里边拦截了,mIsBeingDragged == true,那么在move里将不判断事件方向,而Intercept一般不拦截(除了down事件那种情况),所以都会执行:
if (!mIsBeingDragged) {
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) {
// A child has consumed some touch events and put us into an inconsistent
// state.
needsInvalidate = resetTouch();
break;
}
final float x = ev.getX(pointerIndex);
final float xDiff = Math.abs(x - mLastMotionX);
final float y = ev.getY(pointerIndex);
final float yDiff = Math.abs(y - mLastMotionY);
}
if (xDiff > mTouchSlop && xDiff > yDiff) {
if (DEBUG) Log.v(TAG, "Starting drag!");
Log.e(TAG,"onTouchEvent drag start");
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
mInitialMotionX - mTouchSlop;
mLastMotionY = y;
setScrollState(SCROLL_STATE_DRAGGING);
setScrollingCacheEnabled(true);
// Disallow Parent Intercept, just in case
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
}
计算了下移动量,是否满足,x>mTouchSlop且x>y,设置mIsBeingDragged = true;
然而这个时候都还没有对ViewPager进行滑动,页面还是禁止不动,接着:
if (mIsBeingDragged) {
// Scroll to follow the motion event
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(activePointerIndex);
needsInvalidate |= performDrag(x);
}
我们看到了一个方法performDrag,从名字看出应该和拖动有关系,下面我们看这个方法:
private boolean performDrag(float x) {
boolean needsInvalidate = false;
//省略部分源码
scrollTo((int) scrollX, getScrollY());
pageScrolled((int) scrollX);
return needsInvalidate;
}
主要是计算scrollX,水平滚动距离,然后分别调用scrollTo方法和pageScrolled方法,ViewPager并没有实现scrollTo方法,所以这个调用的是View的scrollTo方法,熟悉自定义View的童鞋,知道scrollTo就是滑动控件。所以这个时候控件动起来了。pageScrolled方法在ViewPager的部分实现:
private boolean pageScrolled(int xpos) {
//省略部分源码
final ItemInfo ii = infoForCurrentScrollPosition();
final int width = getClientWidth();
final int widthWithMargin = width + mPageMargin;
final float marginOffset = (float) mPageMargin / width;
final int currentPage = ii.position;
final float pageOffset = (((float) xpos / width) - ii.offset)
/ (ii.widthFactor + marginOffset);
final int offsetPixels = (int) (pageOffset * widthWithMargin);
onPageScrolled(currentPage, pageOffset, offsetPixels);
return true;
}
计算了滑动偏移量,然后调用这个方法:onPageScrolled,这个方法看起来像是一个回调,我们看看里边干了什么(以下方法名省略,都是onPageScrolled,方法体适当删减):
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.isDecor) continue;
if (childOffset != 0) {
child.offsetLeftAndRight(childOffset);
}
看上边粘贴的代码,先对子View是DecorView进行滑动同步
dispatchOnPageScrolled(position, offset, offsetPixels);
这是对滑动监听的回调分发,也就是我们设置的OnPageChangeListener。
if (mPageTransformer != null) {
final int scrollX = getScrollX();
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.isDecor) continue;
final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();
mPageTransformer.transformPage(child, transformPos);
}
}
看上边的代码,这是onPageScrolled的最后一部分代码,是关于我们的转场动画的,如果我们设置了页面转换器,就会通过 mPageTransformer.transformPage(child, transformPos)这个方法调用我们实现的transformPage,我们通过传给我们的View执行属性动画,这样我们的动画才生效了。
final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();
我们曾经在ViewPager技巧的转场动画博客中有说过结论,就是下面这张图片
当时只说了结论,没有解释为什么,现在结合上边那句代码,我简单说下:
分三点
child.getLeft() 滑动的View相对他的父布局的左边的距离
scrollX ViewPager横向滑动距离
getClientWidth() ViewPager的显示宽度
当不滑动的时候前两个相等,所以为0,这个时候居正中显示
当手指向左滑动时,View向左,这个时候View与相对ViewPager的getLeft不变,scrollX不断增大,当完全消失,即增加一个ViewPager的显示宽度,所以再除以一个getClientWidth()就会得出position的值时【0,-1】
当手指向右滑动时,View向右,这个时候View与相对ViewPager的getLeft依旧不变,scrollX不断减小,当完全消失,即减少一个ViewPager的显示宽度,所以再除以一个getClientWidth()就会得出position的值时【0,1】
分析完这个我们接着回到onTouchEvent事件处理方法,在onTouchEvent的move事件处理中我们说了很多。还差最后一个up事件的处理,我们接着看源码:
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
mPopulatePending = true;
final int width = getClientWidth();
final int scrollX = getScrollX();
final ItemInfo ii = infoForCurrentScrollPosition();
final float marginOffset = (float) mPageMargin / width;
final int currentPage = ii.position;
final float pageOffset = (((float) scrollX / width) - ii.offset)
/ (ii.widthFactor + marginOffset);
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(activePointerIndex);
final int totalDelta = (int) (x - mInitialMotionX);
int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
totalDelta);
setCurrentItemInternal(nextPage, true, true, initialVelocity);
needsInvalidate = resetTouch();
}
break;
这个条件处理都是在mIsBeingDragged为true的情况下进行的。里边涉及一些自定义View的知识,开始是获取加速度计算当前速度,然后通过两个重要的方法进行下一页预测和惯性滑动到准确位置。预测的方法是determineTargetPage,而实现惯性滑动的方法是setCurrentItemInternal。
首先看determineTargetPage这个方法:
if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
targetPage = velocity > 0 ? currentPage : currentPage + 1;
} else {
final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
targetPage = currentPage + (int) (pageOffset + truncator);
}
如果fling距离超过mFlingDistance,并且速度也超过mMinimumVelocity,那么如果速度为正,那就直接到下一页(这里就是当前页+1)。如果上边两个条件不能同时满足,那么谷歌工程师有提供了一个4:6的方法因子,然后与参数pageOffset相加足不足1来判断是否到下一页。当然代码的鲁棒性也考虑了,也做了边界判断
targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
有了这个nextPage(返回的targetPage)就能调用均匀滚动的方法了,接下来看setCurrentItemInternal,其实这个方法,我们在代码翻页的时候最终也是调用的这个方法,这里实现了复用和统一:
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
//省略部分源码
//再一次对边界矫正,鲁棒性
if (item < 0) {
item = 0;
} else if (item >= mAdapter.getCount()) {
item = mAdapter.getCount() - 1;
}
final int pageLimit = mOffscreenPageLimit;
if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
//为了不出现故障,遇到跳转超出一屏的情况,要将他们锁死,保存当前的item,直到滑动结束
for (int i = 0; i < mItems.size(); i++) {
mItems.get(i).scrolling = true;
}
}
final boolean dispatchSelected = mCurItem != item;
if (mFirstLayout) {
mCurItem = item;
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
requestLayout();
} else {
populate(item);
scrollToItem(item, smoothScroll, velocity, dispatchSelected);
}
}
这个方法主要是又对边界进行了校验,然后也考虑到了一次跳转距离超过一屏的情况,最后如果是第一次布局,就会请求重新测量,布局,并且分发当前选中项给OnPageChangeListener。最终在moMeasure也会执行populate方法和相应的滚动方法;而如果不是第一次布局,那就直接执行populate,不用再测量布局了,然后再调用均匀滚动方法。但是选中分发的回调没有执行,会不会在scrollToItem中调用呢?下面我们来看scrollToItem这个方法:
private void scrollToItem(int item, boolean smoothScroll, int velocity,
boolean dispatchSelected) {
final ItemInfo curInfo = infoForPosition(item);
int destX = 0;
if (curInfo != null) {
final int width = getClientWidth();
destX = (int) (width * Math.max(mFirstOffset,
Math.min(curInfo.offset, mLastOffset)));
}
if (smoothScroll) {
smoothScrollTo(destX, 0, velocity);
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
} else {
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
completeScroll(false);
scrollTo(destX, 0);
pageScrolled(destX);
}
}
看到了吗,这个方法里边有选中的分发,这样就不会遗漏掉分发。在这个方法也见到了在populate的calculatePageOffsets中初始化的mFirstOffset,mLastOffset两个属性,用他们来和当前item的offset取中间值,传给对应的smoothScrollTo和scrollTo方法,还有我们在上边move事件中分析的pageScrolled方法。smoothScrollTo和scrollTo是两个具体滚动的方法,一个是一步到位,一个是采用Scroller这个帮助类,进行均匀滑动,这个是基本知识了,就不展开讲了。等整个滚动结束,我们就看到了ViewPager正确的归位了。
补充部分 ------ViewPager之DecorView
在ViewPager源码中几乎每个地方都会出现关于decorView的处理,我们甚至连用都没用过,其实不是我们没用过,是谷歌也有实现的,在上一篇ViewPager系列文章中,在指示器部分提到过两个原生谷歌控件,PagerTitleStrip和PagerTabStrip,这两个其实就是DecorView,也正是因为他们是DecorView并且作为ViewPager的子View才能与ViewPager的具体页面显示同步,为什么这么说,我们看下PagerTitleStrip的类源码:
@ViewPager.DecorView
public class PagerTitleStrip extends ViewGroup {
ViewPager mPager;
TextView mPrevText;
TextView mCurrText;
TextView mNextText;
我截取了一部分,关键的出来了,在类的上边我们看到了一个注解,而且这个注解实在ViewPager中定义的:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface DecorView {
}
那么ViewPager又是如何辨别那个View是DecorView呢?通过下面的方法:
private static boolean isDecorView(@NonNull View view) {
Class<?> clazz = view.getClass();
return clazz.getAnnotation(DecorView.class) != null;
}
对,反射获取注解。DecorView这里在最后给大家简单的说这么几句。如果我们想让我们的自定义View实现DecorView的功能,就需要在类上加上上面的注解。
后记:ViewPager的源码到此就分析完了,这篇博客前前后后花费了笔者接近两周的时间,全都是碎片化的时间。本篇博客文字数量包括源码在内达到40000多字,自己也没有想到会花费这么久,写了这么多。就是按照自己脑子里的思路以及程序运行的流程走了一遍源码。其中包含了大量的细节,其中在setAdapter这部分尤其详细,笔者尽可能多的考虑了实际运行中出现的各种情况来分析代码。
其实代码是谷歌工程师写的,我们读源码,不仅要学里边的代码逻辑,来解决实际工作中的问题,更需要学习代码规范,以及一个健壮的代码都需要考虑哪些地方,代码是怎么很好复用的。对滑动冲突他们是怎么解决的,ViewPager对于缓存策略的运用非常的优秀,运用大量的算法,来保证计算正确,另外为了保证滑动的流畅,ViewPager是在onTouchEvent的move事件的时候专注滑动,而由于至少一侧一屏的缓存策略,保证了用户一次滑动都可以正常进行,然后在up事件中,根据他定义的算法规则判断将惯性滑向哪一侧,然后及时populate更新缓存,然后滑动到指定的item。这样设计既避免了重复调用populate浪费性能,也能让整个滑动和缓存处理很好的配合起来。
ViewPager系列博客结尾:这不仅是本篇博客的结尾,也是笔者设计的ViewPager系列博客的结尾。
第一篇,我们从市面上的产品来引出了VIewPager这个神奇,然后介绍了简单的用法;
第二篇我们由易入难地介绍了实现适配器的方法,以及每个方法的作用;
第三篇我们对比了谷歌提供的针对子VIew是Fragment的两个PagerAdapter的实现原理和区别;
第四篇,我们开始了高阶用法教程,首先是换页监听和方法,是多于我们常规使用的api的;
第五篇,还是高阶用法,ViewPager的懒加载机制,实现,和缓存的原理和实现;
第六篇,是高阶进程动画部分,以及配合动画使用的padding和margin设置,及原理;
第七篇是从比较大的层面的设计进阶,包括了轮播实现,禁止滑动以及各类指示器的配合;
第八篇也就是本篇,从初始化,显示子View和滑动三个方面剖析了ViewPager的源码,让ViewPager的源码暴露在读者面前。
系列博客耗时三个月,也都是些零零散散的时间。到今天终于完成了,由于字数过多或者打字过快,有可能会有一些明显的问题,也希望读者能够帮助指出并改正,最后祝大家:bug少,工资高!
下一系列会是什么主题呢?这里先透露下,笔者接下来会研究-------安卓Framework层部分重要代码剖析,以及安卓源码中蕴藏的设计思想和算法思想
敬请期待,谢谢