1.ViewPager
ViewPager是一个允许用户左右翻转数据页的布局管理器,具有出色的缓存机制。
ViewPager继承自ViewGroup:
public class ViewPager extends ViewGroup {}
ViewPager有一个内部类ItemInfo,它包含了一个页面的基本信息,调用Adapter的instantiateItem方法时,在ViewPager内部就会创建这个类的对象,结构如下:
static class ItemInfo {
Object object; //页卡展示的页卡对象,也就是instantiateItem方法返回的对象
int position; // 页卡下标
boolean scrolling; // 是否正在滑动
float widthFactor; // 当前页面宽度和ViewPager宽度的比例[0-1](默认是1,可通过重写adapter的getPageWidth(int position) 方法自定义页卡宽度)这个值可以设置一个屏幕显示多少个页面
float offset; //当前页面在所有已加载的页面中的索引(用于页面布局)
}
2.ViewPager缓存处理
通常在布局文件中使用ViewPager,当Activity中setContentView方法将Xml布局设置给视图的时候,ViewPager会被初始化,按照如下方法进行初始化:
ViewPager构造方法 -> initViewPager -> onAttachedToWindow -> onMeasure -> onSizeChanged -> onLayout -> draw -> onDraw
注意:以上方法有的不止调用一次,调用整体顺序按照上述步骤。
(1)ViewPager构造方法
public ViewPager(Context context) {
super(context);
initViewPager();
}
public ViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
initViewPager();
}
ViewPager在构造方法里调用了initViewPager初始化方法。
void initViewPager() {
setWillNotDraw(false);//重写onDraw需要调用,ViewGroup默认true
…
}
在初始化方法中调用了setWillNotDraw()方法,设置为false(ViewGroup中这个变量默认是true),意味着ViewPager会执行draw()的绘制方法。
初始化完成后还没有粘贴到真正的窗口Window上,所以之后会回调onAttachedToWindow方法。这个方法只是重置变量mFirstLayout。
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mFirstLayout = true;
}
mFirstLayout变量是一个标记位,对是否第一次执行布局操作layout进行标记。这个变量在布局方法、滚动判断和设置适配器时会用到。
接下来就是View绘制三部曲onMesure、onLayout、onDraw,由于打开了允许draw调用,所以在onDraw前还会调用draw方法。
(2)onMeasure
onMeasure方法主要做了四件事:
①对整个ViewPager的大小进行设置。设置的大小为父类传递过来的大小,也就是剩余的空间。
②测量DecorView,并对其设置大小。使用ViewPager.DecorView注释的视图被视为ViewPager“装饰”的一部分。
③通过populate()方法确保要显示的fragment已经被创建好了。
④测量Adapter的所有View,也就是每个Item,并设置其大小。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//①测量ViewPager自身大小,根据布局文件设置尺寸信息,默认大小为0(与普通自定义ViewGroup不同,普通的会先去测量子view)
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));
final int measuredWidth=getMeasuredWidth();
final int maxGutterSize = measuredWidth / 10;
mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
//ViewPager的显示区域只能显示一个view,childWidthSize和childHeightSize为一个view的宽高大小,即去除了ViewPager的内边距后的宽高
int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
//②对DecorView进行测量
int 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) { //是DecorView
//判断是不是设置了Gravity.LEFT或Gravity.RIGHT
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
//判断是不是设置了Gravity.TOP或Gravity.BOTTOM
final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
//默认DedorView宽高是wrap_content
int widthMode=MeasureSpec.AT_MOST;
int heightMode=MeasureSpec.AT_MOST;
//记录DecorView是在垂直方向上还是在水平方向上占用空间
boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
//如果在垂直方向上占用空间,那水平方向就是match_parent,即EXACTLY,而垂直方向上具体占用多少空间,由DecorView决定,consumeHorizontal同理
if (consumeVertical) {
widthMode = MeasureSpec.EXACTLY;
} else if (consumeHorizontal) {
heightMode =MeasureSpec.EXACTLY;
}
//DecorView宽高大小,初始化为ViewPager可视区域中页卡可用空间
int widthSize = childWidthSize;
int heightSize = childHeightSize;
//如果宽度不是wrap_content,那么width的测量模式就是EXACTLY,如果宽度既不是wrap_content又不是match_parent,那说明用户在布局文件写的具体尺寸,直接将widthSize设置为这个具体尺寸
if (lp.width != LayoutParams.WRAP_CO NTENT) {
widthMode = MeasureSpec.EXACTLY;
if (lp.width != LayoutParams.FILL_PA RENT) {
widthSize = lp.width;
}
}
if (lp.height != LayoutParams.WRAP_CO NTENT) {
heightMode =MeasureSpec.EXACTLY;
if (lp.height != LayoutParams.MATCH _PARENT) {
heightSize = lp.height;
}
}
//对DecorView进行设置长和宽
final int widthSpec = MeasureSpec. makeMeasureSpec(widthSize, widthMode);
final int heightSpec = MeasureSpec. makeMeasureSpec(heightSize, heightMode);
child.measure(widthSpec, heightSpec);
//如果DecorView占用了ViewPager的垂直方向的空间,需要将页卡的竖直方向可用的空间减去DecorView的高度,水平方向上同理
if (consumeVertical) {
childHeightSize -= child.getMeasuredHeight();
} else if (consumeHorizontal) {
childWidthSize -= child.getMeasuredWidth();
}
}
}
}
//ViewPager剩余的宽度MeasureSpec
mChildWidthMeasureSpec = MeasureSpec. makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
//ViewPager剩余的高度MeasureSpec
mChildHeightMeasureSpec = MeasureSpec. makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
//③确保要显示的fragment已经被创建好了。mInLayout标记是为了在计算排列子View的时候避免与添加和删除子View产生冲突
mInLayout = true;
populate(); //ViewPager原理的核心关键方法
mInLayout = false;
//④开始遍历设置每个item的宽高
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) {
//生成每个item宽的MeasureSpec。(childWidthSize * lp.widthFactor)表示当前页卡的实际宽度
final int widthSpec = MeasureSpec. makeMeasureSpec((int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
//对每个item的宽高进行测量
child.measure(widthSpec, mChildHeightMeasureSpec);
}
}
}
}
(3)onSizeChanged
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w != oldw) {
//如果两值不同,就重新计算滚动位置
recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin);
}
}
完成size改变的方法后,会触发onLayout执行操作,这是三部曲里边的布局方法。
(4)onLayout
onLayout主要做了三件事:
①设置Decor View的位置。先layout DecorView,它会占用一定空间,计算出四个padding值 提供给后面layout普通子View使用;
②设置普通View的位置。利用第一步得出的padding值得到一个可用的空间对子View进行布局,这里就涉及到对多个子View横向排列顺序的问题,这里就根据ItemInfo中的offset值来决定的,通过offset计算每个子View的Left值。
③将页面移动到第一个Item位置
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;
//①设置DecorView的位置
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childLeft = 0;
int childTop = 0;
if (lp.isDecor) {
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
//根据水平方向上的Gravity,确定childLeft的值
switch (hgrav) {
default:
childLeft = paddingLeft;
break;
case Gravity.LEFT:
childLeft = paddingLeft;
//累加左内边距(多个DecorView都居左边,肯定要累加啦
paddingLeft += child.getMeasuredWidth();
break;
case Gravity.CENTER_HORIZONTAL:
childLeft = Math.max((width - child.getMeasuredWidth()) / 2, paddingLeft);
break;
case Gravity.RIGHT:
//计算居右侧时的左边距=(viewPager可见宽-右边距-child测量宽)
childLeft = width - paddingRight - child.getMeasuredWidth();
//累加右内边距
paddingRight += child.getMeasuredWidth();
break;
}
//与上面水平方向的同理,据水平方向上的Gravity,确定childTop的值
switch (vgrav) {
default:
childTop = paddingTop;
break;
case Gravity.TOP:
childTop = paddingTop;
paddingTop += child.getMeasuredHeight();
break;
case Gravity.CENTER_VERTICAL:
childTop = Math.max((height - child.getMeasuredHeight()) / 2, paddingTop);
break;
case Gravity.BOTTOM:
childTop = height - paddingBottom - child.getMeasuredHeight();
paddingBottom += child.getMeasuredHeight();
break;
}
//上面计算的childLeft是相对ViewPager的左边计算的, 还需要加上x方向已经滑动的距离scrollX
childLeft += scrollX;
//确定DecorView的位置
child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight());
decorCount++; /DecorView的计数器加1
}
}
}
//普通页面(Adapter的View)可用宽度
final int childWidth = width - paddingLeft - paddingRight;
//②设置非Decor View的位置
//下面针对普通页面布局,在此onLayout之前已经得到正确的偏移量offset了
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
ItemInfo ii;
//如果子View不是DecorView,并且子View保存在mItems中
if (!lp.isDecor && (ii = infoForChild(child)) != null) {
计算左边偏移量。loff计算页面的左面大小。第一个View的ii.offset为0,loff为0;第二个View的ii.offset为1.0,loff为childWidth * ii.offset
int loff = (int) (childWidth * ii.offset);
//将左边距+左边偏移量得到左边最终的位置
int childLeft = paddingLeft + loff;
int childTop = paddingTop;
if (lp.needsMeasure) { //如果需要重新测量,则重新测量
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);
}
//child调用自己的layout方法来布局自己
child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight());
}
}
}
//mTopPageBounds全局变量,是普通View的Top(刨除掉Decor View)
mTopPageBounds = paddingTop;
//mBottomPageBounds全局变量,是普通View的Bottom(刨除掉Decor View)
mBottomPageBounds = height - paddingBottom;
//Decor View的个数
mDecorChildCount = decorCount;
//③第一次布局,将页面移动到第一个Item位置
if (mFirstLayout) {
scrollToItem(mCurItem, false, 0, false);
}
mFirstLayout = false; //第一次布局变量设置为false
}
onLayout执行完成之后,就完成了布局,测量、布局都完成之后,就可以绘制了。
(5)onDraw
绘制各个页面之间间隔和viewpager的边缘效应效果。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw the margin drawable between pages if needed.如有需要,在页与页之间绘制drawable
if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {
//获取当前X轴方向滚动偏移量
final int scrollX = getScrollX();
//viewPager的测量宽度
final int width = getWidth();
//页面间距比例
final float marginOffset = (float) mPageMargin / width;
int itemIndex = 0;
ItemInfo ii = mItems.get(0);
float offset = ii.offset;
final int itemCount = mItems.size();
final int firstPos = ii.position;
final int lastPos = mItems.get(itemCount - 1).position;
//遍历元素
for (int pos = firstPos; pos < lastPos; pos++) {
while (pos > ii.position && itemIndex < itemCount) {
ii = mItems.get(++itemIndex);
}
float drawAt;
//计算绘制区域的left,偏移量累加用于下一个元素
if (pos == ii.position) {
drawAt = (ii.offset + ii.widthFactor) * width;
offset = ii.offset + ii.widthFactor + marginOffset;
} else {
float widthFactor = mAdapter.getPageWidth(pos);
drawAt = (offset + widthFactor) * width;
offset += widthFactor + marginOffset;
}
//mTopPageBounds为顶部top,mBottomPageBounds为底部bottom,即普通View可用高度区间
//如果绘制区域在可见范围内,根据计算出来的区域,绘制页面间隔drawable
if (drawAt + mPageMargin > scrollX) {
mMarginDrawable.setBounds( Math.round(drawAt), mTopPageBounds, Math.round(drawAt + mPageMargin), mBottomPageBounds);
mMarginDrawable.draw(canvas);
}
//绘制区域超出 滚动偏移量+viewPager的宽,结束后序没意义的绘制(不可见)
if (drawAt > scrollX + width) {
break; // No more visible, no sense in continuing
}
}
}
}
(6)setAdapter
调用ViewPager的setAdapter函数可将ViewPager与PagerAdapter关联起来。这是最重要的一部分内容,涉及ViewPager的缓存机制,预加载机制,以及加载与销毁的时机。
这个方法的主要作用就是:
①清除旧的Adapter,对已加载的item调用destroyItem
②将自身滚动到初始位置this.scrollTo(0, 0)
③设置PagerObserver: mAdapter.setViewPagerObserver(mObserver);
④调用populate()方法计算并初始化View
⑤如果设置了OnAdapterChangeListener,进行回调
下面来看源码:
①setAdapter 销毁旧的Adapter数据,用新的Adaper更新UI
public void setAdapter(PagerAdapter adapter){
//①如果已经设置过PagerAdapter,则清除旧的Adapter,对已加载的item调用destroyItem
if (mAdapter != null) {
mAdapter.setViewPagerObserver(null);//清除数据观察者
// 开始更新标记。告诉PagerAdapter开始更新要显示的页面。如果在自定义Adapter里覆写这个方法,就可以在ViewPager正式显示子View之前干点什么事,比如在第一次加载特别耗时的情况下,显示进度条
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();//清空子View容器
//将所有的非Decor View移除,即将页面移除。DecorView是装饰ViewPager的,默认是一旦设置了就会一直存在
removeNonDecorViews();
mCurItem = 0;//当前显示页面重置到第一个
//②将自身滚动到初始位置
scrollTo(0, 0);
}
//将全局变量mAdapter保存到局部变量,即保存上一次的PagerAdapter
final PagerAdapter oldAdapter = mAdapter;
//把新设置的adapter赋值给全局变量mAdapter
mAdapter = adapter;
mExpectedAdapterCount = 0;
//如果设置的adapter为空,就忽视,并不报错
if (mAdapter != null) {
//③确保观察者不为null,观察者主要是用于监视数据源的内容发生变化
if (mObserver == null) {
mObserver = new PagerObserver();
}
//设置适配器数据监听,用于更新视图,外部类只能通过pagerAdapter.notifyDataSetChanged方法通知ViewPager更新视图
mAdapter.setViewPagerObserver( mObserver);
//mPopulatePending标志是为了避免下面populate这个方法状态冲突
mPopulatePending = false;
//全局变量mFirstLayout设为局部变量,即保存上一次是否是第一次Layout
final boolean wasFirstLayout = mFirstLayout;
//设定当前为第一次Layout
mFirstLayout = true;
mExpectedAdapterCount = mAdapter.getCount(); //期望显示的子View的数量
//④设置并初始化子view
if (mRestoredCurItem >= 0) { //如果有数据需要恢复,这个用于意外恢复数据
//如果有上次保存,mRestoredCurItem就一定不小于0,这个时候就会进入恢复流程,主要是跳转到上次保存的item位置
mAdapter.restoreState( mRestoredAdapterState, mRestoredClassLoader);
setCurrentItemInternal( mRestoredCurItem, false, true);
mRestoredCurItem = -1;//标记无需再恢复
mRestoredAdapterState = null;
mRestoredClassLoader = null;
} else if (!wasFirstLayout) { //如果在此之前不是第一次Layout,也就是onLayout方法已经执行过了(因为在onLayout方法中会将赋值给wasFirstLayout的全局变量mFirstLayout设置为false,标志着第一次layout的结束),这个时候会调用到核心方法populate()进行缓存计算
//由于ViewPager并不是将所有页面作为子View,而是最多缓存用户指定缓存个数*2(左右两边,可能左边或右边没有那么多页面),因此需要创建和销毁页面,populate主要工作就是这些
populate();
} else { //重新布局(Layout)
requestLayout(); //requestLayout引起重新的测量和布局,所以会执行onMeasure(), onLayout()方法,在onMeasure()方法中会调用populate()方法
}
}
//⑤如果PagerAdapter发生变化,并且设置了OnAdapterChangeListener监听器,则回调OnAdapterChangeListener的onAdapterChanged函数
if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) {
for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) {
mAdapterChangeListeners.get(i ).onAdapterChanged(this, oldAdapter, adapter);
}
}
}
给适配器设置PagerObserver,是用于客户端在数据改变的情况下,可以通过调用notifyDataSetChanaged方法进行数据变更。这个方法就是调用PagerObserver的onChanged方法进行,onChanged方法内部则是调用ViewPager的方法dataSetChanged,从而实现ViewPager数据变更。ViewPager里边设置Adapter,Adapter里设置了Observer,实现了双向绑定。
(7)populate()
populate直译过来是“增添数据、输入数据”。该方法是ViewPager非常重要的方法,主要根据参数newCurrentItem和mOffscreenPageLimit计算出需要初始化的页面和需要销毁的页面,然后通过调用Adapter的instantiateItem和destroyItem两个方法初始化新页面和销毁不需要的页面。
populate()主要做了这几件事:
①根据传入的当前界面更新mItems数组,该数组存放的是对应的ItemInfo对象
②如果左面超出了缓存限制,则删除左边,如果左边需要缓存却没有缓存,则在左边创建对应的ItemInfo
③如果右面超出了缓存限制,则删除右边,如果右边需要缓存却没有缓存,则在右边创建对应的ItemInfo
④设置当前选中item
⑤如果当前ViewPager可获得焦点,把焦点传递给子View
populate有两个方法,一个带参数,一个不带参数。
void populate() {
populate(mCurItem);
}
参数newCurrentItem表示当前需要定位显示的界面(也就是传入的Position)
void populate(int newCurrentItem) {
ItemInfo oldCurInfo = null;
//如果要定位显示的位置不是当前位置
if (mCurItem != newCurrentItem) {
//保存之前旧的ItemInfo
oldCurInfo = infoForPosition(mCurItem);
//更新当前视图index
mCurItem = newCurrentItem;
}
if (mAdapter == null) {
//对子View的绘制顺序进行排序,优先绘制Decor View,再按照position从小到大排序
sortChildDrawingOrder();
return;
}
//如果正在等待populate,那么在用户手指抬起切换到新的位置期间应该推迟创建子View,直到滚动到最终位置再去创建,以免在这个期间出现差错。即若滑动未停止,则暂停populate操作
if (mPopulatePending) {
if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
sortChildDrawingOrder();
return;
}
//在ViewPager没有attached到window之前,不要populate。因为如果在恢复View的层次结构之前进行populate,可能会与要恢复的内容有冲突。即若视图未依附于窗口则暂停populate操作
if (getWindowToken() == null) {
return;
}
mAdapter.startUpdate(this); //开始更新
final int pageLimit = mOffscreenPageLimit;//预加载数量
final int startPos = Math.max(0, mCurItem - pageLimit); //startPos是缓存页面的起始页,确保起始位置大于等于0
final int N = mAdapter.getCount();//需要显示的子View的数量,即ViewPager的所有Item
final int endPos = Math.min(N - 1, mCurItem + pageLimit);//endPos是缓存页面的结束页,确保最后的位置小于等于数据源中数据个数-1
if (N != mExpectedAdapterCount) {//数量不一致,说明数据发生变化,但是没有提示ViewPager更新,则抛出异常
throw new IllegalStateException("The application's PagerAdapter changed the adapter's contents without calling PagerAdapter#notifyDataSetChanged!" ;
}
//①下面开始定位当前获焦的页面。先从内存中定,如果mCurItem在mItems里面,就返回对应的ItemInfo,否则,就创建一个ItemInfo放到mItems里面,并赋值给curItem
int curIndex = -1;//存放mItems数组中的位置
ItemInfo curItem = null;
//遍历mItems中所有对象,找到当前获焦页面
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;
}
}
//如果curItem为空,说明mItems没有任何东西(比如第一次进来),或者说当前界面ItemInfo没有保存到mItems里面,就通过addNewItem调用instantiateItem加载当前页面
if (curItem == null && N > 0) {
//mCurItem是position的位置,curIndex是mCurItem所对应的ItemInfo在mItems中的位置
curItem = addNewItem(mCurItem, curIndex);
}
//默认缓存当前页面的左右两边的页面,如果用户设定了缓存页面数据,则将当前页面两边都缓存用户指定的数量的页面。如果当前没有页面,则啥也不做
if (curItem != null) {
float extraWidthLeft = 0.f;
// 当前视图左边的页面,即mCurItem所对应的ItemInfo在mItems中的上一个位置
int itemIndex = curIndex - 1;
//如果当前页面左边有页面,则将左边页面对应的ItemInfo取出;否则左边页面ItemInfo为null
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
//获取显示区域的宽度,即viewPager测量宽度-内边距
final int clientWidth = getClientWidth();
//算出左边页面需要的宽度。注意,这里的宽度指的是实际宽度与可视区域宽度的比例,即实际宽度=leftWidthNeeded*clientWidth
final float leftWidthNeeded = clientWidth <= 0 ? 0 : 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
//②遍历当前视图左边的所有页面,不包括自身。主要是删除左边不在缓存范围内的ItemInfo,或者如果左边没有缓存,在左边创建缓存
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);
mAdapter.destroyItem(this, pos, ii.object);
itemIndex--;//由于mItems删除了一个元素,需要将索引减一
curIndex--;//mItems里面位置变化
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;//ii设置为当前遍历的页面的左边一个页面
}
} else if (ii != null && pos == ii.position) {
// 当前位置是需要缓存的界面,并且这个位置上的页面已经存在,则将左边宽度加上当前页面的宽度
extraWidthLeft += ii.widthFactor;
itemIndex--;//mItems往左遍历
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;//ii设置为当前遍历的页面的左边一个页面
} else {
//这个位置需要缓存,并且这个位置没有页面。即该左侧元素不在内存中,则重新添加
ii = addNewItem(pos, itemIndex + 1);
extraWidthLeft += ii.widthFactor;//将左边宽度加上当前位置的页面
curIndex++;//由于新加了一个元素,当前的索引号需要加一
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;//ii设置为当前遍历的页面的左边一个页面
}
}
//③来到当前视图右侧,思路大致和左侧相同
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);
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);
}
//结束更新,如果是PagerAdapter则为空实现;如果是FragmentStatePagerAdapter或者FragmentPagerAdapter,则会调用FragmentTransaction的commitNowAllowingStateLoss方法提交fragemnt,进行detach或者attach之类的操作
mAdapter.finishUpdate(this);
// 遍历子视图,将mItems里面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) {
//没有有效的宽度,则获取内存中保存的信息给子视图的LayoutParams
final ItemInfo ii = infoForChild(child);
if (ii != null) {
lp.widthFactor = ii.widthFactor;
lp.position = ii.position;
}
}
}
//重新将子视图排序
sortChildDrawingOrder();
//⑤如果当前ViewPager是具有焦点的,将焦点分发给当前显示的页面
if (hasFocus()) {
//找到焦点View,如果存在则获取它的信息
View currentFocused = findFocus();
ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
//如果没找到或者找到的焦点view不是当前位置,则遍历元素,如果找到对应元素则请求焦点
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) {
//找到view,请求焦点
if (child.requestFocus( View.FOCUS_F ORWARD)){
break;
}
}
}
}
}
}
populate()方法首先会根据newCurrentItem和mOffscreenPageLimit计算要加载的page页面,计算出startPos和endPos。然后根据startPos和endPos初始化页面ItemInfo,先从缓存里面获取,如果没有就调用addNewItem方法,同时将不需要的ItemInfo移除:mItems.remove(itemIndex),并调用mAdapter.destroyItem方法。然后设置LayoutParams参数(包括position和widthFactor ),根据position排序待绘制的View列表。最后一步是获取当前显示View的焦点。
可以看出,ViewPager里面有多少界面都不会卡,因为它会不断的销毁和创建页面,默认不仅会创建当前页面,还会创建相邻的offscreenPageLimit个页面。
在整个populate中,计算缓存前,有四次对特殊情况的处理,有三次是在Adapter的startUpdate()标记方法之前,一次在其之后。这四种情况分别是:
1)mAdapter == null,用户还未设置适配器,这个时候停止处理并返回,这种情况是在ViewPager初始化的时候,在调用onMeasure()方法中,会调用populate()方法,而此时populate()方法的执行没有意义,避免浪费性能及其他错误,直接返回。
2)mPopulatePending == true 的情况,这个值默认是false,而且在setAdapter中也会重置为false,只有在以下两种情况会为true:一种是在onTouchEvent当手指离开屏幕的时候,另外一种是结束虚拟滑动endFakeDrag的时候,看这个两个方法,可知都是和滑动相关的。而且,从调用位置可以发现,正好是手指离开屏幕,或者模拟手指离开屏幕,而产生的fling到新的位置,这个时候避免出错,延迟绘制。
3)getWindowToken() == null,这个方法获得的token正是当前View粘贴到(attach)到的window的token,如果为空,则说明此时View树还未与window产生联系,主页面还未调用onResume方法。这个时候也不能计算缓存。
4)N != mExpectedAdapterCount ,上边刚刚在setAdapter中提到这个变量的赋值,是调用Adapter的getCount()方法,而N也是用相同的方法获得的,如果同一个方法获得的数据个数不同,那说明用户修改了数据源,而并未调用notifyDataSetChanged()方法来告知ViewPager同步,所以给开发者抛出了异常,从而让开发者意识到错误的原因并及时修改。
一个关键方法addNewItem(),来看源码:
ItemInfo addNewItem(int position, int index) {
ItemInfo ii = new ItemInfo();
ii.position = position;
ii.object = mAdapter.instantiateItem(this, position);//在adapter中重写,新创建的对象
ii.widthFactor = mAdapter.getPageWidth( position);
if (index < 0 || index >= mItems.size()) {
mItems.add(ii); //在mItems末尾依次添加
} else {
mItems.add(index, ii);//添加到指定位置
}
return ii;
}
里边有我们熟悉的方法instantiateItem,从而证明创建的子View确实就是从这个方法获得的,所以,我们在实现这个方法的时候,返回什么,他这就缓存什么。
另外还调用了getPageWidth,这个方法设置当前item的显示宽度,是个比例值。(这个宽度因子,不能设置2或者更大,否则就会显示异常,而如果设置太小的话,所有item能在一屏内显示的时候,就会发生鬼畜的现象)
刚开始,这部分可能不好理解,那我们来动态分析一下(以下示意图展示的都是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满足,并且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);
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,这个时候针对左边缓存又需要销毁一个元素,还是会先进入第二个条件,然后进入第一个条件,进行remove。这样我们就知道了这个setOffscreenPageLimit是如何影响到ViewPager的缓存策略的。
9.calculatePageOffsets
对于缓存的分析是很重要的一块,也是一个理解的难点。接下来就是计算偏移值,调用calculatePageOffsets方法。该方法主要分两大块,oldCurInfo != null和oldCurInfo == null,就是初始化和滑动的两个过程,实际的逻辑取决于mCurItem这个属性变量与newCurrentItem这个参数是否相等。
calculatePageOffsets方法主要用于计算每个页面对应ItemInfo的offset变量,这个变量用于记录当前view在所有缓存View中(包含当前显示页)的索引,用于布局的时候计算该View应该放在哪个位置。在populate方法中更新完页面数据后,会调用该方法计算所有页面的offset。
private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
final int N = mAdapter.getCount();
final int width = getClientWidth();//子View的可用宽大小,即viewPager测量宽度-内边距
final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; //mPageMargin是页面之间的间隔,marginOffset间隔比例,默认0
--------第一部分 计算当前位置的偏移offset-----
//根据上一次展示的页面,来确认此次当前页面的offset。只有在使用ViewPager.setCurrentItem的方法直接跳转到指定页面时才条件成立,靠滑动切换页面不会成立
if (oldCurInfo != null) {
final int oldCurPosition = oldCurInfo.position;
//根据oldItem.position与curItem.position的大小关系,来确定curItem的offset值是等于oldItem.offset加上还是减去它们之间间隔的页面(页面宽度+ marginOffset)之和
if (oldCurPosition < curItem.position) {//滑到下一张,加上
int itemIndex = 0;
ItemInfo ii = null;
//根据 old页面的offset+old页面的宽比(0f-1f)+每个页面的间隔比例 计算出old页面的下一个页面的offset值。例如假设marginOffset=0.2,widthFactor=1,一个5个页面,则每个页面的offset分别为0;1.2;2.4;3.6;4.8
float Offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;
//从old页面的下一个页面开始遍历,一直到当前要展示的页面的位置
for(int pos = oldCurPosition + 1; pos <= curItem.position && itemIndex < mItems.size;pos++) {
ii = mItems.get(itemIndex);
//循环,直到获取到pos当前位置的元素
while(pos > ii.position && itemIndex < mItems.size() - 1) {
itemIndex++;
ii = mItems.get(itemIndex);
}
while(pos < ii.position) {
// We don't have an item populated for this, ask the adapter for an offset.
offset += mAdapter.getPageWidth( pos) + marginOffset;
pos++;
}
ii.offset = offset;//设置当前页面的offset
offset += ii.widthFactor + marginOffset; //计算下一个页面的offset
}
} else if (oldCurPosition > curItem.position) { //滑到上一张,减去(逻辑相似)
int itemIndex = mItems.size() - 1;
ItemInfo ii = null;
float Offset = oldCurInfo.offset;
for(int pos = oldCurPosition - 1; pos >= curItem.position && itemIndex >=0;pos--) {
ii = mItems.get(itemIndex);
while(pos < ii.position && itemIndex>0) {
itemIndex--;
ii = mItems.get(itemIndex);
}
while(pos > ii.position) {
offset -= mAdapter.getPageWidth( pos) + marginOffset;
pos--;
}
offset -= ii.widthFactor + marginOffset;
ii.offset = offset;
}
}
}
//根据当前元素再次计算缓存列表中所有元素的偏移量。初始化操作或者刷新当前页从这里开始
final int itemCount = mItems.size();
float offset = curItem.offset;//当前要显示的页面的偏移量
int pos = curItem.position - 1;//前一个页面
mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;//如果当前要展示的页面是第0个位置,则设置mFirstOffset=curItem.offset
mLastOffset = curItem.position == N - 1? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;//如果当前要展示的页面是最后一个位置,则设置mLastOffset =curItem.offset
--------第二部分 计算缓存列表中当前页面左边页面的偏移量(根据当前页面计算)-----
for (int i = curIndex - 1; i >= 0; i--, pos--) {
final ItemInfo ii = mItems.get(i);
//如果pos跟ii.position之间有间隔页面,则减去间隔偏移量
while(pos > ii.position) {
offset -= mAdapter.getPageWidth(pos--) + marginOffset;
}
//ii元素的偏移量=它的下一个元素的偏移量-ii页面的宽比-每个页面的间隔比例
offset -= ii.widthFactoor + marginOffset;
ii.offset = offset;
//如果ii是第一个元素,则设置mFirstOffset值
if (ii.position == 0) mFirstOffset = offset;
}
//令offset=当前元素的下一个元素的偏移量
offset = curItem.offset + curItem.widthFactor + marginOffset;
pos = curItem.position + 1;//下一个元素的pos
--------第三部分 计算缓存列表中当前页面右边页面的偏移量(根据当前页面计算)------
for (int i = curIndex + 1; i < itemCount; i++, pos++) {
final ItemInfo ii = mItems.get(i);
while(pos < ii.position) {
offset += mAdapter.getPageWidth(pos++) + marginOffset;
}
//如果ii是最后一个元素,则设置mLastOffset值
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和curItem的position的大小,分成了两种情况,里边都是根据上边的算法矫正之后,给当前的item赋上正确的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设置了相应的计算关系。这个时候在滑动的时候就能很快判断是否到达了边缘,只要这两个变量不是默认值,就没有到边。
之后调用mAdapter.setPrimaryItem() 方法用来设置ViewPager要显示的Item信息。
在适配器创建对象的方法是instantiateItem,但是由于ViewPager的缓存策略导致创建的并不一定是要显示的,那么适配器为了解决用户回调当前显示的信息,而且是最及时的获取,就有了setPrimaryItem这个回调方法。
(8)ViewPager.dataSetChanged()
当调用Adapter的notifyDataSetChanged时,会触发这个方法,该方法会重新计算当前页面的position,移除需要销毁的页面的ItemInfo对象,然后再调用populate方法刷新页面
//PagerAdapter
public void notifyDataSetChanged() {
synchronized (this) {
//这个监听器就是ViewPager内的PagerObserver,是在setAdapter的时候通过Adapter.setViewPagerObserver()传入的
if (mViewPagerObserver != null) {
mViewPagerObserver.onChanged();
}
}
mObservable.notifyChanged();
}
//ViewPager
private class PagerObserver extends DataSetObserver {
PagerObserver() {
}
@Override
public void onChanged() {
//调用ViewPager的dataSetChanged方法
dataSetChanged();
}
@Override
public void onInvalidated() {
dataSetChanged();
}
}
void dataSetChanged() {
// This method only gets called if our observer is attached, so mAdapter is non-null.
final int adapterCount = mAdapter.getCount();
mExpectedAdapterCount = adapterCount;
//是否需要刷新页面,此处如果元素个数小于缓存页数,也小于适配器元素个数,则为true
boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && mItems.size() < adapterCount;
int newCurrItem = mCurItem;
boolean isUpdating = false;
//遍历容器中的元素
for (int i = 0; i < mItems.size(); i++) {
final ItemInfo ii = mItems.get(i);
// 返回元素相应位置是否发生变化的标志。POSITION_UNCHANGED = -1表示当前页面不需要更新,不用销毁;POSITION_NONE = -2需要更新,销毁。可以在初始化时为页面设置tag,在getItemPosition方法中根据tag判断仅更新当前页面视图。
final int newPos = mAdapter.getItemPosi tion(ii.object);
// 若返回POSITION_UNCHANGED,跳过
if (newPos == PagerAdapter.POSITION_UNCHANGED) {
continue;
}
if (newPos == PagerAdapter.POSITION_ NONE) {
// 返回POSITION_NONE时移除元素并记录标志。这里对元素先移除,后重新加载
mItems.remove(i);
i--;
if (!isUpdating) {//开始更新
mAdapter.startUpdate(this);
isUpdating = true;
}
mAdapter.destroyItem(this, ii.position, ii.object); //销毁视图
needPopulate = true;//设置为需要刷新页面
//如果当前位置元素被删除,则重新选出新的当前元素位置
if (mCurItem == ii.position) {
// Keep the current item in the valid range
newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
needPopulate = true;
}
continue;
}
//newPos不是默认的这两种的情况下,并且当前元素的position不等于它,则设置为它,需要刷新
if (ii.position != newPos) {
if (ii.position == mCurItem) {
// Our current item changed position. Follow it.
newCurrItem = newPos;
}
ii.position = newPos;
needPopulate = true;
}
}
if (isUpdating) {//isUpdating=true结束更新操作
mAdapter.finishUpdate(this);
}
//重新排序
Collections.sort(mItems, COMPARATOR);
//如果需要刷新,遍历子view,重置页面宽度,在populate方法中将重新计算它们
if (needPopulate) {
// Reset our known page widths; populate will recompute them.
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) {
lp.widthFactor = 0.f;
}
}
//更新UI
setCurrentItemInternal(newCurrItem, false, true);
//请求布局
requestLayout();
}
}
①循环mItems(每个page对应的ItemInfo对象),调用int newPos = mAdapter.getItemPosition方法。
②newPos等于PagerAdapter.POSITION_ UNCHANGED表示当前页面不需要更新,不用销毁;newPos等于PagerAdapter.POSITION_NONE时,需要更新,移除item,调用mAdapter.destroyItem。
③循环完成后,计算出显示页面的newCurrItem,调用setCurrentItemInternal( newCurrItem, false, true)方法更新UI(实际调用populate方法重新计算页面信息)
3.ViewPager的滑动控制
缓存处理是ViewPager很重要的一点,然而ViewPager不是一个静态控件,而是一个动态展示控件,所以需要了解它是如何在滑动中保证依然高效流畅的体验。
ViewPager是个ViewGroup,根据touch事件分发机制,事件会先传递到dispatchTouchEvent,该方法ViewPager没有重写,根据父类ViewGroup的dispatchTouchEvent方法可知,如果child没有主动调用requestDisallowInterceptTouchEvent,就会执行onInterceptTouchEvent(ev)方法,ViewPager重写了这个方法:
public boolean onInterceptTouchEvent( MotionEvent ev) {
final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; // 触摸动作
// 时刻要注意触摸是否已经结束
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
//Release the drag.
if (DEBUG) Log.v(TAG, "Intercept done!");
//重置一些跟判断是否拦截触摸相关变量
resetTouch();
return false;//触摸结束,无需拦截
}
// 如果当前不是按下事件,就判断一下是否是在拖拽切换页面
if (action != MotionEvent.ACTION_DOWN) {
//如果当前是正在拽切换页面,直接拦截掉事件,后面无需再做拦截判断
if (mIsBeingDragged) {
if (DEBUG) Log.v(TAG, "Intercept returning true!");
return true;
}
//如果标记为不允许拖拽切换页面,就不处理一切触摸事件
if (mIsUnableToDrag) {
if (DEBUG) Log.v(TAG, "Intercept returning false!");
return false;
}
}
//根据不同的动作进行处理
switch (action) {
//如果是手指移动操作
case MotionEvent.ACTION_MOVE: {
//代码能执行到这里,就说明mIsBeingDragged==false
final int activePointerId = mActivePointerId;//使用触摸点Id,主要是为了处理多点触摸
if (activePointerId == INVALID_POINTER) {
//如果当前的触摸点id不是一个有效的Id,无需再做处理
break;
}
//根据触摸点的id来区分不同的手指,只需关注一个手指就好
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
//根据这个手指的序号,来获取这个手指对应的x坐标
final float x = MotionEventCompat. getX(ev, pointerIndex);
//在x轴方向上移动的距离
final float dx = x - mLastMotionX;
//x轴方向的移动距离绝对值
final float xDiff = Math.abs(dx);
//与x轴同理
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float yDiff = Math.abs(y - mInitialMotionY);
//判断当前显示的页面是否可以滑动,如果可以滑动,则将该事件丢给当前显示的页面处理。isGutterDrag是判断是否在两个页面之间的缝隙内移动。canScroll是判断页面是否可以滑动
if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && canScroll(this, false, (int) dx, (int) x, (int) y)) {
mLastMotionX = x;
mLastMotionY = y;
//标记ViewPager不去拦截事件
mIsUnableToDrag = true;
return false;
}
//如果x移动距离大于最小距离,并且斜率小于0.5,表示在水平方向上的拖动
if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
if (DEBUG) Log.v(TAG, "Starting drag!");
//水平方向的移动,需要ViewPager去拦截
mIsBeingDragged = true;
//如果ViewPager还有父View,则还要向父View申请将触摸事件传递给ViewPager
requestParentDisallowInterceptTouchEv ent(true);
//设置滚动状态
setScrollState(SCROLL_STATE_DRA GGING);
//保存当前位置
mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop;
mLastMotionY = y;
//启用缓存
setScrollingCacheEnabled(true);
} else if (yDiff > mTouchSlop) {//27.否则的话,表示是竖直方向上的移动
if (DEBUG) Log.v(TAG, "Starting unable to drag!");
//竖直方向上的移动则不去拦截触摸事件
mIsUnableToDrag = true;
}
if (mIsBeingDragged) {
//跟随手指一起滑动
if (performDrag(x)) {
ViewCompat.postInvalidateOnAni mation(this);
}
}
break;
}
//如果手指是按下操作
case MotionEvent.ACTION_DOWN: {
//记录按下的点位置
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
//第一个ACTION_DOWN事件对应的手指序号为0
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
//重置允许拖拽切换页面
mIsUnableToDrag = false;
//标记开始滚动
mIsScrollStarted = true;
//手动调用计算滑动的偏移量
mScroller.computeScrollOffset();
//如果当前滚动状态为正在将页面放置到最终位置,且当前位置距离最终位置足够远
if (mScrollState == SCROLL_STATE_SETTLING && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {//可以看出在down事件里,ViewPager只是对如果之前是SCROLL_STATE_SETTLING这个状态的事件进行拦截,其余的不处理
//如果此时用户手指按下,则立马暂停滑动
mScroller.abortAnimation();
mPopulatePending = false;
populate();
mIsBeingDragged = true;
//如果ViewPager还有父View,则还要向父View申请将触摸事件传递给ViewPager
requestParentDisallowInterceptTou chEvent(true);
//设置当前状态为正在拖拽
setScrollState(SCROLL_STATE_DRA GGING);
} else {
//结束滚动
completeScroll(false);
mIsBeingDragged = false;
}
break;
}
case MotionEventCompat.ACTION_POINT ER_UP:
onSecondaryPointerUp(ev);
break;
}
//添加速度追踪
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
//只有在当前是拖拽切换页面时才会去拦截事件
return mIsBeingDragged;
}
ViewPager.scrollToItem
滑动到指定页面,内部会触发OnPageChangeListener 。
private void scrollToItem(int item, boolean smoothScroll, int velocity, boolean dispatchSelected) {
//拿到对应位置的元素信息
final ItemInfo curInfo = infoForPosition(item);
int destX = 0;
//如果元素信息不为空,则计算偏移的目的地
if (curInfo != null) {
//获取子View的可用宽大小,即viewPager测量宽度-内边距
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);
}
}
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布尔值来排序。其他直接按照参数前边的位置减后边的位置,来决定排序。按照位置升序排序。