版权声明:本文为博主原创文章,转载请注明出处http://blog.csdn.net/u013132758。 https://blog.csdn.net/u013132758/article/details/53433565
前言
前一篇博客介绍了ViewPager的简单使用,这篇博客主要从源码的角度来解析ViewPager。
ViewPager的一些变量
ViewPager是一组视图,那么它的父类必然是ViewGroup,也就是说ViewPager继承了ViewGroup的所有属性。我们先看一下部分源码:
public class ViewPager extends ViewGroup {
private static final String TAG = "ViewPager";
private static final boolean DEBUG = false;
private static final boolean USE_CACHE = false;
private static final int DEFAULT_OFFSCREEN_PAGES = 1;
private static final int MAX_SETTLE_DURATION = 600; // ms
private static final int MIN_DISTANCE_FOR_FLING = 25; // dips
private static final int DEFAULT_GUTTER_SIZE = 16; // dips
private static final int MIN_FLING_VELOCITY = 400; // dips
static final int[] LAYOUT_ATTRS = new int[] {
android.R.attr.layout_gravity
};
/**
* Used to track what the expected number of items in the adapter should be.
* If the app changes this when we don't expect it, we'll throw a big obnoxious exception.
*用于监测项目中我们需要适配器的期望的页卡数
*/
private int mExpectedAdapterCount;
/**
* 该类用于保存页面信息
*/
static class ItemInfo {
Object object;//页面展示的页卡对象
int position;//页卡下标(页码)
boolean scrolling;//是否滚动
float widthFactor;//表示加载的页面占ViewPager所占的比例[0~1](默认返回1) ,这个值可以设置一个屏幕显示多少个页面
float offset;//页卡偏移量
}
//页卡排序
private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>(){
@Override
public int compare(ItemInfo lhs, ItemInfo rhs) {
return lhs.position - rhs.position;
}
};
//插值器:他的作用就是根据不同的时间控制滑动的速度。
private static final Interpolator sInterpolator = new Interpolator() {
@Override
public float getInterpolation(float t) {
t -= 1.0f;
return t * t * t * t * t + 1.0f;
}
};
//表示已经缓存的页面信息
private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
private final ItemInfo mTempItem = new ItemInfo();
PagerAdapter mAdapter;//页卡适配器
int mCurItem; // Index of currently displayed page.当前页面的下标
// Offsets of the first and last items, if known.
// Set during population, used to determine if we are at the beginning
// or end of the pager data set during touch scrolling.
private float mFirstOffset = -Float.MAX_VALUE;//第一个页卡的滑动偏移量
private float mLastOffset = Float.MAX_VALUE;//最后一个页卡的滑动偏移量
。。。。。。
省略部分代码
。。。。。。
/**
* Position of the last motion event.
最后页卡滑动事件的位置
*/
private float mLastMotionX;
private float mLastMotionY;
private float mInitialMotionX;
private float mInitialMotionY;
/**
* ID of the active pointer. This is used to retain consistency during
* drags/flings if multiple pointers are used.
*/
private int mActivePointerId = INVALID_POINTER;//活动指针标示 如果使用多个指针,这用于保持拖动/ flings期间的一致性。
/**
* Sentinel value for no current active pointer.
* Used by {@link #mActivePointerId}.
*/
private static final int INVALID_POINTER = -1;//没有活动的当前指针的哨兵值
/**
* Determines speed during touch scrolling
*这个速度追踪器用于触摸滑动时追踪滑动速度
*/
private VelocityTracker mVelocityTracker;
private int mMinimumVelocity;
private int mMaximumVelocity;
private int mFlingDistance;
private int mCloseEnough;
// If the pager is at least this close to its final position, complete the scroll
// on touch down and let the user interact with the content inside instead of
// "catching" the flinging pager.
//如果页面至少接近它的最终位置,完成向下滚动,让用户与内容中的内容进行交互,而不是“捕获”flinging页面。
private static final int CLOSE_ENOUGH = 2; // dp
private boolean mFakeDragging;
private long mFakeDragBeginTime;
private EdgeEffectCompat mLeftEdge;
private EdgeEffectCompat mRightEdge;
private boolean mFirstLayout = true;
private boolean mNeedCalculatePageOffsets = false;
private boolean mCalledSuper;
private int mDecorChildCount;
private List<OnPageChangeListener> mOnPageChangeListeners;
private OnPageChangeListener mOnPageChangeListener;
private OnPageChangeListener mInternalPageChangeListener;
private List<OnAdapterChangeListener> mAdapterChangeListeners;
private PageTransformer mPageTransformer;
private int mPageTransformerLayerType;
private Method mSetChildrenDrawingOrderEnabled;
private static final int DRAW_ORDER_DEFAULT = 0;
private static final int DRAW_ORDER_FORWARD = 1;
private static final int DRAW_ORDER_REVERSE = 2;
private int mDrawingOrder;
private ArrayList<View> mDrawingOrderedChildren;
private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator();
/**
* Indicates that the pager is in an idle, settled state. The current page
* is fully in view and no animation is in progress.
*/
public static final int SCROLL_STATE_IDLE = 0;//空闲
/**
* Indicates that the pager is currently being dragged by the user.
*/
public static final int SCROLL_STATE_DRAGGING = 1;//滑动
/**
* Indicates that the pager is in the process of settling to a final position.
*/
public static final int SCROLL_STATE_SETTLING = 2;//滑动结束
这段代码中我们我们看到ViewPager继承自ViewGroup,主要我们看上面注释的几个变量:
- mExpectedAdapterCount:这个变量用于监测项目中我们需要适配器的期望的页卡数,如果APP改变了它,当我们不期望它的时候,会抛出一个异常!
- ItemInfo:这个内部类是用来保存页卡信息的
- sInterpolator:插值器,它的主要作用是根据不同的时间来控制滑动速度。
- ArrayList<ItemInfo> mItems:表示已经缓存的页面信息(通常会缓存当前显示页面以前当前页面前后页面,不过缓存页面的数量由mOffscreenPageLimit决定)
- PagerAdapter mAdapter:页卡适配器
- int mCurItem:当前页面的下标
- mFirstOffset/mLastOffset 第/最后一个页卡的滑动偏移量
- mActivePointerId:活动指针标示如果使用多个指针,这用于保持拖动/ flings期间的一致性。
- mVelocityTracker:速度追踪器用于触摸滑动时追踪滑动速度
- SCROLL_STATE_IDLE = 0:表示ViewPager处于空闲,建立状态。 当前页面完全在视图中,并且没有正在进行动画。
- SCROLL_STATE_DRAGGING = 1:表示用户当前正在拖动ViewPager。
- SCROLL_STATE_SETTLING = 2:表示ViewPager正在设置到最终位置。
ViewPager的几个重要方法
1、initViewPager()
initViewPager 是初始化ViewPager,其实还是比较简单的,不难理解,源码如下:
void initViewPager() {
setWillNotDraw(false);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
setFocusable(true);
final Context context = getContext();
mScroller = new Scroller(context, sInterpolator);//创建Scroller对象
final ViewConfiguration configuration = ViewConfiguration.get(context);//一个标准常量
final float density = context.getResources().getDisplayMetrics().density;//获取屏幕密度
mTouchSlop = configuration.getScaledPagingTouchSlop();//获取TouchSlop:系统所能识别的被认为是滑动的最小距离
mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);//最小速度
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();//获取允许执行一个fling手势的最大速度值
mLeftEdge = new EdgeEffectCompat(context);
mRightEdge = new EdgeEffectCompat(context);
mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);
mCloseEnough = (int) (CLOSE_ENOUGH * density);
mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density);
ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate());
if (ViewCompat.getImportantForAccessibility(this)
== ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
ViewCompat.setImportantForAccessibility(this,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
//ViewCompat一个安卓官方实现兼容的帮助类
ViewCompat.setOnApplyWindowInsetsListener(this,
new android.support.v4.view.OnApplyWindowInsetsListener() {
private final Rect mTempRect = new Rect();
@Override
public WindowInsetsCompat onApplyWindowInsets(final View v,
final WindowInsetsCompat originalInsets) {
// First let the ViewPager itself try and consume them...
final WindowInsetsCompat applied =
ViewCompat.onApplyWindowInsets(v, originalInsets);
if (applied.isConsumed()) {
// If the ViewPager consumed all insets, return now
return applied;
}
// Now we'll manually dispatch the insets to our children. Since ViewPager
// children are always full-height, we do not want to use the standard
// ViewGroup dispatchApplyWindowInsets since if child 0 consumes them,
// the rest of the children will not receive any insets. To workaround this
// we manually dispatch the applied insets, not allowing children to
// consume them from each other. We do however keep track of any insets
// which are consumed, returning the union of our children's consumption
final Rect res = mTempRect;
res.left = applied.getSystemWindowInsetLeft();
res.top = applied.getSystemWindowInsetTop();
res.right = applied.getSystemWindowInsetRight();
res.bottom = applied.getSystemWindowInsetBottom();
for (int i = 0, count = getChildCount(); i < count; i++) {
final WindowInsetsCompat childInsets = ViewCompat
.dispatchApplyWindowInsets(getChildAt(i), applied);
// Now keep track of any consumed by tracking each dimension's min
// value
res.left = Math.min(childInsets.getSystemWindowInsetLeft(),
res.left);
res.top = Math.min(childInsets.getSystemWindowInsetTop(),
res.top);
res.right = Math.min(childInsets.getSystemWindowInsetRight(),
res.right);
res.bottom = Math.min(childInsets.getSystemWindowInsetBottom(),
res.bottom);
}
// Now return a new WindowInsets, using the consumed window insets
return applied.replaceSystemWindowInsets(
res.left, res.top, res.right, res.bottom);
}
});
}
2、onLayout()
ViewPager继承了ViewGroup那么肯定就要重写onLayout()方法,该方法的主要作用是布局,那么当然也复写了onMeasure()方法测量。关于View的原理可以看看View的工作原理(三)--View的Layout和Draw过程。ViewPager的子View是水平摆放的,所以在onLayout中,大部分工作的就是计算childLeft,即子View的左边位置,而顶部位置基本上是一样的。Viewpager的onlayout其实就根据populate()方法中计算出的当前页面的offset来绘制当前页面,和其他页面.下面我们仔细去研究内部滑动源码或者setCurrentPage源码都可以发现实际上是调用了populate()方法。当我们需要有View更新的时候比如addView()、removeView()都会进行requestLayout()重新布局、以及invalidate()重新绘制界面。
@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();
//DecorView 数量
int decorCount = 0;
//首先对DecorView进行layout,再对普通页卡进行layout,之所以先对DecorView布局,是为了让普通页卡(页卡)能有合适的偏移
//下面循环主要是针对DecorView
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
//visibility不为GONE才layout
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//左边和顶部的边距初始化为0
int childLeft = 0;
int childTop = 0;
if (lp.isDecor) {//只针对Decor View
//获取水平或垂直方向上的Gravity
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
//根据水平方向上的Gravity,确定childLeft以及paddingRight
switch (hgrav) {
default://没有设置水平方向Gravity时(左中右),childLeft就取paddingLeft
childLeft = paddingLeft;
break;
case Gravity.LEFT://水平方向Gravity为left,DecorView往最左边靠
childLeft = paddingLeft;
paddingLeft += child.getMeasuredWidth();
break;
case Gravity.CENTER_HORIZONTAL://将DecorView居中摆放
childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
paddingLeft);
break;
case Gravity.RIGHT://将DecorView往最右边靠
childLeft = width - paddingRight - child.getMeasuredWidth();
paddingRight += child.getMeasuredWidth();
break;
}
//与上面水平方向的同理,据水平方向上的Gravity,确定childTop以及paddingTop
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());
//将DecorView数量+1
decorCount++;
}
}
}
//普通页卡的宽度
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 是ViewPager静态内部类,前面介绍过它保存了普通页卡(也就是页卡)的position、offset等信息,是对普通页卡的一个抽象描述
ItemInfo ii;
//infoForChild通过传入View查询对应的ItemInfo对象
if (!lp.isDecor && (ii = infoForChild(child)) != null) {
//计算当前页卡的左边偏移量
int loff = (int) (childWidth * ii.offset);
//将左边距+左边偏移量得到最终页卡左边位置
int childLeft = paddingLeft + loff;
int childTop = paddingTop;
//如果当前页卡需要进行测量(measure),当这个页卡是在Layout期间新添加新的,
// 那么这个页卡需要进行测量,即needsMeasure为true
if (lp.needsMeasure) {
//标记已经测量过了
lp.needsMeasure = false;
//下面过程跟onMeasure类似
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());
//对普通页卡进行layout
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
}
}
}
//将部分局部变量保存到实例变量中
mTopPageBounds = paddingTop;
mBottomPageBounds = height - paddingBottom;
mDecorChildCount = decorCount;
//如果是第一次layout,则将ViewPager滑动到第一个页卡的位置
if (mFirstLayout) {
scrollToItem(mCurItem, false, 0, false);
}
//标记已经布局过了,即不再是第一次布局了
mFirstLayout = false;
}
3,onMeasure()
前面,onLayout()方法中布局,用到了measure测量的结果,下面我们就来看下ViewPager的onMeasure()。该方法主要做了四件事:
- 测量DecorView
- 确定页卡默认宽高的测量规格MeasureSpec(包含尺寸和模式的整数)
- 确保我们需要显示的fragment已经被我们创建好了
- 再对子View进行测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//根据布局文件,设置尺寸信息,默认大小为0
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));
final int measuredWidth = getMeasuredWidth();
final int maxGutterSize = measuredWidth / 10;
//设置mGutterSize的值,后面再讲mGutterSize
mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
// ViewPager的显示区域只能显示对于一个View
//childWidthSize和childHeightSize为一个View的可用宽高大小
//即去除了ViewPager内边距后的宽高
int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
//先对DecorView进行测量
//下面这个循环是只针对DecorView的,即用于装饰ViewPager的View
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();
//如果该View是DecorView,即用于装饰ViewPager的View
if (lp != null && lp.isDecor) {
//获取Decor View的在水平方向和竖直方向上的Gravity
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
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;
//consumeHorizontal:如果是在垂直方向上占用空间,
// 那么水平方向就是match_parent,即EXACTLY
//而垂直方向上具体占用多少空间,还得由DecorView决定
//consumeHorizontal也是同理
if (consumeVertical) {
widthMode = MeasureSpec.EXACTLY;
} else if (consumeHorizontal) {
heightMode = MeasureSpec.EXACTLY;
}
//宽高大小,初始化为ViewPager可视区域中页卡可用空间
int widthSize = childWidthSize;
int heightSize = childHeightSize;
//如果宽度不是wrap_content,那么width的测量模式就是EXACTLY
//如果宽度既不是wrap_content又不是match_parent,那么说明是用户
//在布局文件写的具体的尺寸,直接将widthSize设置为这个具体尺寸
if (lp.width != LayoutParams.WRAP_CONTENT) {
widthMode = MeasureSpec.EXACTLY;
if (lp.width != LayoutParams.FILL_PARENT) {
widthSize = lp.width;
}
}
if (lp.height != LayoutParams.WRAP_CONTENT) {
heightMode = MeasureSpec.EXACTLY;
if (lp.height != LayoutParams.FILL_PARENT) {
heightSize = lp.height;
}
}
//确定页卡默认宽高的测量规格MeasureSpec(包含尺寸和模式的整数)
final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
//对DecorView进行测量
child.measure(widthSpec, heightSpec);
//如果Decor View占用了ViewPager的垂直方向的空间
//需要将页卡的竖直方向可用的空间减去DecorView的高度,
//同理,水平方向上也做同样的处理
if (consumeVertical) {
childHeightSize -= child.getMeasuredHeight();
} else if (consumeHorizontal) {
childWidthSize -= child.getMeasuredWidth();
}
}
}
}
//确定页卡默认宽高的测量规格MeasureSpec(包含尺寸和模式的整数)
mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
//确保我们需要显示的fragment已经被我们创建好了
mInLayout = true;
populate();//后面再详细介绍
mInLayout = false;
//再对页卡进行测量
size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
//visibility为GONE的无需测量
if (child.getVisibility() != GONE) {
if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
+ ": " + mChildWidthMeasureSpec);
//获取页卡的LayoutParams
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//只针对页卡而不对Decor View测量
if (lp == null || !lp.isDecor) {
//LayoutParams的widthFactor是取值为[0,1]的浮点数,
// 用于表示页卡占ViewPager显示区域中页卡可用宽度的比例,
// 即(childWidthSize * lp.widthFactor)表示当前页卡的实际宽度
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
//对当前页卡进行测量
child.measure(widthSpec, mChildHeightMeasureSpec);
}
}
}
}
4、populate()
前面多处用到了populate()方法,下面我们就来研究一下populate()方法,看这个方法我看得有点懵逼!!!主要是之前一直没将重点放在PagerAdapter,与PagerAdapter联系起来后发现原来还是比较容易的,populate()方法主要的作用是:根据制定的页面缓存大小,做了页面的销毁和重建。
1.更新items,将items中的内容换成当前展示页面以及预缓存页面。我们从下面的源码中可以看到,这里会调用PagerAdapter的startUpdate()、instantiateItem()、destroyItem()、setPrimaryItem()、finishUpdate()等方法,基本是把PagerAdapter的所有生命周期从头走到尾。
2.计算每个items的off(偏移量),这个就是布局时onLayout()方法中起作用的。
2.计算每个items的off(偏移量),这个就是布局时onLayout()方法中起作用的。
void populate(int newCurrentItem) {
ItemInfo oldCurInfo = null;
if (mCurItem != newCurrentItem) {
oldCurInfo = infoForPosition(mCurItem);
mCurItem = newCurrentItem;
}
if (mAdapter == null) {
//对页卡的绘制顺序进行排序,优先绘制DecorView
//再按照position从小到大排序
sortChildDrawingOrder();
return;
}
//如果我们正在等待populate,那么在用户手指抬起切换到新的位置期间应该推迟创建页卡,
// 直到滚动到最终位置再去创建,以免在这个期间出现差错
if (mPopulatePending) {
if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
//对页卡的绘制顺序进行排序,优先绘制Decor View
//再按照position从小到大排序
sortChildDrawingOrder();
return;
}
//同样,在ViewPager没有attached到window之前,不要populate.
// 这是因为如果我们在恢复View的层次结构之前进行populate,可能会与要恢复的内容有冲突
if (getWindowToken() == null) {
return;
}
//回调PagerAdapter的startUpdate函数,
// 告诉PagerAdapter开始更新要显示的页面
mAdapter.startUpdate(this);
final int pageLimit = mOffscreenPageLimit;
//确保起始位置大于等于0,如果用户设置了缓存页面数量,第一个页面为当前页面减去缓存页面数量
final int startPos = Math.max(0, mCurItem - pageLimit);
//保存数据源中的数据个数
final int N = mAdapter.getCount();
//确保最后的位置小于等于数据源中数据个数-1,
// 如果用户设置了缓存页面数量,第一个页面为当前页面加缓存页面数量
final int endPos = Math.min(N - 1, mCurItem + pageLimit);
//判断用户是否增减了数据源的元素,如果增减了且没有调用notifyDataSetChanged,则抛出异常
if (N != mExpectedAdapterCount) {
//resName用于抛异常显示
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());
}
//定位到当前获焦的页面,如果没有的话,则添加一个
int curIndex = -1;
ItemInfo curItem = null;
//遍历每个页面对应的ItemInfo,找出获焦页面
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;
}
}
//如果没有找到获焦的页面,说明mItems列表里面没有保存获焦页面,
// 需要将获焦页面加入到mItems里面
if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex);
}
//默认缓存当前页面的左右两边的页面,如果用户设定了缓存页面数量,
// 则将当前页面两边都缓存用户指定的数量的页面
//如果当前没有页面,则我们啥也不需要做
if (curItem != null) {
float extraWidthLeft = 0.f;
//左边的页面
int itemIndex = curIndex - 1;
//如果当前页面左边有页面,则将左边页面对应的ItemInfo取出,否则左边页面的ItemInfo为null
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
//保存显示区域的宽度
final int clientWidth = getClientWidth();
//算出左边页面需要的宽度,注意,这里的宽度是指实际宽度与可视区域宽度比例,
// 即实际宽度=leftWidthNeeded*clientWidth
final float leftWidthNeeded = clientWidth <= 0 ? 0 :
2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
//从当前页面左边第一个页面开始,左边的页面进行遍历
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);
//回调PagerAdapter的destroyItem
mAdapter.destroyItem(this, pos, ii.object);
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
" view: " + ((View) ii.object));
}
//由于mItems删除了一个元素
//需要将索引减一
itemIndex--;
curIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
//如果当前位置是需要缓存的位置,并且这个位置上的页面已经存在
//则将左边宽度加上当前位置的页面
extraWidthLeft += ii.widthFactor;
//mItems往左遍历
itemIndex--;
//ii设置为当前遍历的页面的左边一个页面
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
} else {//如果当前位置是需要缓存,并且这个位置没有页面
//需要添加一个ItemInfo,而addNewItem是通过PagerAdapter的instantiateItem获取对象
ii = addNewItem(pos, itemIndex + 1);
//将左边宽度加上当前位置的页面
extraWidthLeft += ii.widthFactor;
//由于新加了一个元素,当前的索引号需要加1
curIndex++;
//ii设置为当前遍历的页面的左边一个页面
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}
//同理,右边需要添加缓存的页面
/*........................*
* *
* 省略右边添加缓存页面代码 *
* *
*........................*/
calculatePageOffsets(curItem, curIndex, oldCurInfo);
}
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);
}
}
//回调PagerAdapter的setPrimaryItem,告诉PagerAdapter当前显示的页面
mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
//回调PagerAdapter的finishUpdate,告诉PagerAdapter页面更新结束
mAdapter.finishUpdate(this);
//检查页面的宽度是否测量,如果页面的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();
//如果ViewPager被设定为可获焦的,则将当前显示的页面设定为获焦
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;
}
}
}
}
}
}
5、setAdapter()
这个方法很容易理解,就是设置ViewPager所需要的适配器。我们看下面的源码:
/**
* Set a PagerAdapter that will supply views for this pager as needed.
*
* @param adapter Adapter to use
*/
public void setAdapter(PagerAdapter adapter) {
//如果已经设置过PagerAdapter,即mAdapter != null,做一些清理工作
if (mAdapter != null) {
//清除观察者
mAdapter.setViewPagerObserver(null);
//回调startUpdate函数,告诉PagerAdapter开始更新要显示的页面
mAdapter.startUpdate(this);
//4如果之前保存有页面,则将之前所有的页面destroy掉
for (int i = 0; i < mItems.size(); i++) {
final ItemInfo ii = mItems.get(i);
mAdapter.destroyItem(this, ii.position, ii.object);
}
//回调finishUpdate,告诉PagerAdapter结束更新
mAdapter.finishUpdate(this);
//将所有的页面清除
mItems.clear();
//将所有的非Decor View移除,即将页面移除
removeNonDecorViews();
//当前的显示页面重置到第一个
mCurItem = 0;
//滑动重置到(0,0)位置
scrollTo(0, 0);
}
//保存上一次的PagerAdapter
final PagerAdapter oldAdapter = mAdapter;
//设置mAdapter为新的PagerAdapter
mAdapter = adapter;
//设置期望的适配器中的页面数量为0个
mExpectedAdapterCount = 0;
//如果设置的PagerAdapter不为null
if (mAdapter != null) {
//确保观察者不为null,观察者主要是用于监视数据源的内容发生变化
if (mObserver == null) {
mObserver = new PagerObserver();
}
//将观察者设置到PagerAdapter中
mAdapter.setViewPagerObserver(mObserver);
mPopulatePending = false;
//保存上一次是否是第一次Layout
final boolean wasFirstLayout = mFirstLayout;
//设定当前为第一次Layout
mFirstLayout = true;
//更新期望的数据源中页面个数
mExpectedAdapterCount = mAdapter.getCount();
//如果有数据需要恢复
if (mRestoredCurItem >= 0) {
//回调PagerAdapter的restoreState函数
mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
setCurrentItemInternal(mRestoredCurItem, false, true);
//标记无需再恢复
mRestoredCurItem = -1;
mRestoredAdapterState = null;
mRestoredClassLoader = null;
} else if (!wasFirstLayout) {//如果在此之前不是第一次Layout
//由于ViewPager并不是将所有页面作为页卡,
// 而是最多缓存用户指定缓存个数*2(左右两边,可能左边或右边没有那么多页面)
//因此需要创建和销毁页面,populate主要工作就是这些
populate();
} else {
//重新布局(Layout)
requestLayout();
}
}
//如果PagerAdapter发生变化,并且设置了OnAdapterChangeListener监听器
//则回调OnAdapterChangeListener的onAdapterChanged函数
if (mAdapterChangeListener != null && oldAdapter != adapter) {
mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
}
}
6、onPageScrolled()
当滚动当前页时,将调用此方法,作为程序启动的平滑滚动或用户启动的触摸滚动的一部分。如果你重写这个方法,你必须调用到超类实现(例如,在onPageScrolled之前的super.onPageScrolled(position,offset,offsetPixels))。这段代码也比较好理解,就是控制ViewPager的滚动,将我们需要的内容显示在屏幕上,比如滑动到中间时,一半是position另一半是position+1.同时这个方法也是非常重要的,我们如若改造优化ViewPager,就需要重写该方法。
/**
* This method will be invoked when the current page is scrolled, either as part
* of a programmatically initiated smooth scroll or a user initiated touch scroll.
* If you override this method you must call through to the superclass implementation
* (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled
* returns.
*
* @param position 表示当前是第几个页面
*
* @param offset 表示当前页面移动的距离,其实就是个相对实际宽度比例值,取值为[0,1)。0表示整个页面在显示区域,1表示整个页面已经完全左移出显示区域。
* @param offsetPixels 表示当前页面左移的像素个数。
*/
@CallSuper
protected void onPageScrolled(int position, float offset, int offsetPixels) {
// Offset any decor views if needed - keep them on-screen at all times.
//如果有DecorView,则需要使得它们时刻显示在屏幕中,不移出屏幕
if (mDecorChildCount > 0) {
//根据Gravity将DecorView摆放到指定位置。
//这部分代码与onMeasure()方法中的原理一样,这里就不做解释了
final int scrollX = getScrollX();
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
final int width = getWidth();
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 int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
int childLeft = 0;
switch (hgrav) {
default:
childLeft = paddingLeft;
break;
case Gravity.LEFT:
childLeft = paddingLeft;
paddingLeft += child.getWidth();
break;
case Gravity.CENTER_HORIZONTAL:
childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
paddingLeft);
break;
case Gravity.RIGHT:
childLeft = width - paddingRight - child.getMeasuredWidth();
paddingRight += child.getMeasuredWidth();
break;
}
childLeft += scrollX;
final int childOffset = childLeft - child.getLeft();
if (childOffset != 0) {
child.offsetLeftAndRight(childOffset);
}
}
}
//分发页面滚动事件,类似于事件的分发
dispatchOnPageScrolled(position, offset, offsetPixels);
//如果mPageTransformer不为null,则不断去调用mPageTransformer的transformPage函数
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;
//计算child位置
final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();
//调用transformPage
mPageTransformer.transformPage(child, transformPos);
}
}
//标记ViewPager的onPageScrolled函数执行过
mCalledSuper = true;
}
ViewPager的重点是滑动,那么我们来看一下ViewPager的触摸事件,我们主要看事件的拦截onInterceptTouchEvent(),以及事件的消耗onTouchEvent()。我们这里就只看onInterceptTouchEvent(),明白了这段代码,onTouchEvent()也就很容易理解了。
7、onInterceptTouchEvent()
关于ViewPager对于事件的拦截,我们只有当拖动ViewPager时ViewPager才会变化,也就是只有当我们拖拽ViewPager时,才会拦截该触摸事件。
@Override
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,否则的话,在第7个注释处就已经执行结束了
//使用触摸点Id,主要是为了处理多点触摸
final int activePointerId = mActivePointerId;
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);
if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
//判断当前显示的页面是否可以滑动,如果可以滑动,则将该事件丢给当前显示的页面处理
//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
requestParentDisallowInterceptTouchEvent(true);
//设置滚动状态
setScrollState(SCROLL_STATE_DRAGGING);
//保存当前位置
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.postInvalidateOnAnimation(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) {
//如果此时用户手指按下,则立马暂停滑动
mScroller.abortAnimation();
mPopulatePending = false;
populate();
mIsBeingDragged = true;
//如果ViewPager还有父View,则还要向父View申请将触摸事件传递给ViewPager
requestParentDisallowInterceptTouchEvent(true);
//设置当前状态为正在拖拽
setScrollState(SCROLL_STATE_DRAGGING);
} else {
//结束滚动
completeScroll(false);
mIsBeingDragged = false;
}
if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
+ " mIsBeingDragged=" + mIsBeingDragged
+ "mIsUnableToDrag=" + mIsUnableToDrag);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
//添加速度追踪
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
//只有在当前是拖拽切换页面时我们才会去拦截事件
return mIsBeingDragged;
}
总结
ViewPager的主要原理我的理解就是,保存缓存的数组mItems的大小永远都在[0,mOffscreenPageLimit*2+1]范围内,我们滑动下一页卡时,它将前一页卡移出数组,将下一页卡加入缓存。本来打算一片文章写完ViewPager的结果写的时候发现,我对ViewPager的认识还是不足,ViewPager比我想象的要强大许多。以上有什么不准确的地方,希望大家多多指正。