ViewPager源码不完全解读
ViewPager 继承自 ViewGroup,实现了水平分页滑动的效果。
首先介绍下常用的方法 dispatchOnPageScrolled, 回掉设置的所有接口的 onPageScrolled
/**
* 回调接口的 onPageScrolled 方法
*
* @param position
* @param offset
* @param offsetPixels
*/
private void dispatchOnPageScrolled(int position, float offset, int offsetPixels) {
if (mOnPageChangeListener != null) {
mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
}
if (mOnPageChangeListeners != null) {
for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) {
OnPageChangeListener listener = mOnPageChangeListeners.get(i);
if (listener != null) {
listener.onPageScrolled(position, offset, offsetPixels);
}
}
}
if (mInternalPageChangeListener != null) {
mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);
}
}
ViewPager的创建
我使用了使用 google nexus 4 - 4.4.4 - API 19 - 768*1280(320dpi)genymotion模拟器进行单步调试,这次测试ViewPager没有设置padding和margin,我设置了七个ImageView视图,在设置adapter后,我立刻调用了 viewpager.setCurrentItem(1).方法调用顺序大致如下:
- initViewPager();
- setAdapter(PagerAdapter adapter);
- setCurrentItem的2参3参4参方法;
- onAttachedToWindow();
- onMeasure(int widthMeasureSpec, int heightMeasureSpec); (onMeasure调用了8次)
- onMeasure(int widthMeasureSpec, int heightMeasureSpec);
- onMeasure(int widthMeasureSpec, int heightMeasureSpec);
- onMeasure(int widthMeasureSpec, int heightMeasureSpec);
- onMeasure(int widthMeasureSpec, int heightMeasureSpec);
- onMeasure(int widthMeasureSpec, int heightMeasureSpec);
- onMeasure(int widthMeasureSpec, int heightMeasureSpec);
- onMeasure(int widthMeasureSpec, int heightMeasureSpec);
- onSizeChanged(int w, int h, int oldw, int oldh);
- onLayout(boolean changed, int l, int t, int r, int b);
- onMeasure(int widthMeasureSpec, int heightMeasureSpec); (onMeasure调用了8次)
- onMeasure(int widthMeasureSpec, int heightMeasureSpec);
- onMeasure(int widthMeasureSpec, int heightMeasureSpec);
- onMeasure(int widthMeasureSpec, int heightMeasureSpec);
- onMeasure(int widthMeasureSpec, int heightMeasureSpec);
- onMeasure(int widthMeasureSpec, int heightMeasureSpec);
- onMeasure(int widthMeasureSpec, int heightMeasureSpec);
- onMeasure(int widthMeasureSpec, int heightMeasureSpec);
- onLayout(boolean changed, int l, int t, int r, int b)
- draw(Canvas canvas)
- onDraw(Canvas canvas)
- draw(Canvas canvas)
- onDraw(Canvas canvas)
- draw(Canvas canvas)
- onDraw(Canvas canvas)
###初始化 initViewPager
// 使用 google nexus 4 - 4.4.4 - API 19 - 768*1280(320dpi)genymotion模拟器
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; // density = 2.0f
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); // mTouchSlop = 32
mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); // mMinimumVelocity = 800
// 启动一个滑动的最大速率,像素/s
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); // mMaximumVelocity = 16000
mLeftEdge = new EdgeEffectCompat(context);
mRightEdge = new EdgeEffectCompat(context);
mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); // mFlingDistance = 50
mCloseEnough = (int) (CLOSE_ENOUGH * density); // mCloseEnough = 4
mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); // mDefaultGutterSize = 32
ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate());
if (ViewCompat.getImportantForAccessibility(this)
== ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
ViewCompat.setImportantForAccessibility(this,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
}
调用了 setCurrentItem
在为viewpager设置了adapter后我立刻调用了setCurrentItem(int item)方法,这个方法实现的时候ViewPager还没有进行测量。。。item是1.
/**
* 设置当前选中的页面。
* 如果 ViewPager 已经通过档当前的 adapter 设立了第一个布局,切换到指定的页面时,会有个平滑的切换动画。
* <p>凡是在代码中调用了该方法,则 ViewPager 的 getScrollX() 方法返回的scrollX 以该item页面为起点</p>
*
* @param item 选择的页面索引
*/
public void setCurrentItem(int item) {
mPopulatePending = false;
setCurrentItemInternal(item, !mFirstLayout, false); // item = 1, mFirstLayout = true
}
setCurrentItemInternal 三个参数的方法源码如下,最终调用了四个参的 setCurrentItenInternal方法:
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
// 这个时候 item = 1, smoothScroll = false, always = false
setCurrentItemInternal(item, smoothScroll, always, 0);
}
mAdapter里面有七个页面,mAdapter.getCount() = 7, 第一次进来只执行了下面的代码:
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
final int pageLimit = mOffscreenPageLimit; // mOffscreenPageLimit = 1
/* 判断要跳转的页面是不是当前页面 */
final boolean dispatchSelected = mCurItem != item; // dispatchSelected = true
if (mFirstLayout) { // mFirstLayout = true
// We don't have any idea how big we are yet and shouldn't have any pages either.
// Just set things up and let the pending layout handle things.
mCurItem = item; // 存储当前页面的索引
if (dispatchSelected) { // dispatchSelected = true
dispatchOnPageSelected(item); // 回调所有已经设置的接口的 OnPageSelected
}
requestLayout(); // 请求布局,即开始调用 measure 方法
}
}
onAttachedToWindow(),附加到 window
@Override
protected void onAttachedToWindow() {
Log.i(TAG, "onAttachedToWindow");
super.onAttachedToWindow();
mFirstLayout = true;
}
然后进行测量调用
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 对于简单的实现,内部的尺寸总是0。
// 我们依赖容器来指定我们的布局大小。我们不能真正的知道什么时候我们会添加或者删除任意的 view,
// 而且发生这种事时,我们不想视图有所改变。
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));
final int measuredWidth = getMeasuredWidth(); // measuredWidth = 768
final int maxGutterSize = measuredWidth / 10; // maxGutterSize = 76
// childWidthSize = 768, getPaddingLeft() = getPaddingRight() = 0
int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
// childHeightSize = 1042, getPaddingTop() = getPaddingBottom() = 0
int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
int size = getChildCount(); // size = 0
mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
// 确保我们已经创建了所有我们需要显示的 fragments。
mInLayout = true;
populate();
mInLayout = false;
// 遍历所有已缓存的页面.
size = getChildCount(); // size = 3
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);
}
}
}
}
void populate(int newCurrentItem) { // newCurrentItem = 1
ItemInfo oldCurInfo = null;
int focusDirection = View.FOCUS_FORWARD; // focusDirection = 2
mAdapter.startUpdate(this); // 显示页面开始改变
final int pageLimit = mOffscreenPageLimit; // pageLimit = 1
/* 预加载页面的起始位置 */
final int startPos = Math.max(0, mCurItem - pageLimit); // mCurItem = 1, startPos = 0
/* 当前页面的数量 */
final int N = mAdapter.getCount(); // N = 7
/* 预加载页面的结束位置 */
final int endPos = Math.min(N - 1, mCurItem + pageLimit); // endPos = 2
// 定位当前显示页面的 item
int curIndex = -1;
ItemInfo curItem = null;
if (curItem == null && N > 0) {
// 添加新的item
curItem = addNewItem(mCurItem, curIndex);
}
if (curItem != null) {
float extraWidthLeft = 0.f;
int itemIndex = curIndex - 1; // curIndex = 0, itemIndex = -1
/* 获取当前页面左边的 item */
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; // ii = null
final int clientWidth = getClientWidth(); // clientWidth = 768
// leftWidthNeeded = 1.0, curItem.widthFactor = 1.0, getPaddingLeft() = 0
final float leftWidthNeeded = clientWidth <= 0 ? 0 :
2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
/* 遍历当前页面左边的所有 item,创建并保存预加载范围内的页面,移除范围外的页面 */
for (int pos = mCurItem - 1; pos >= 0; pos--) { // pos = 0, mCurItem = 1
/* 如果页面在预加载页面的范围外,这里是左边,而且保存的item不为null时移除该页面 */
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { // startPos = 0
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为null,该pos位置的页面是当前页的左边的页面,而且在预存范围内,这个时候调用 addnewIteam(0,0);
ii = addNewItem(pos, itemIndex + 1);
extraWidthLeft += ii.widthFactor; // extraWidthLeft = 1.0
curIndex++; // curIndex = 1
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}
float extraWidthRight = curItem.widthFactor; // extraWidthRight = 1.0
/* 当前页面右面页面的索引 */
itemIndex = curIndex + 1; // itemIndex = 2
if (extraWidthRight < 2.f) {
// itemIndex = 2, mItems.size() = 2, ii = null
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
// clientWidth = 768, getPaddingRight() = 0, rightWidthNeeded = 2.f
final float rightWidthNeeded = clientWidth <= 0 ? 0 :
(float) getPaddingRight() / (float) clientWidth + 2.f;
/* 遍历当前页面右边的页面,创建并保存预加载范围内的页面,移除范围外的页面 */
// N = 7, mCurItem = 1, 初始化 pos = 2,itemIndex = 2,之后每次加1
for (int pos = mCurItem + 1; pos < N; pos++) {
// 初始化extraWidthright = 1之后每次加1, rightWidthNeeded = 2, endPos = 2
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 {
// 调用 addNewItem(2,2)
ii = addNewItem(pos, itemIndex);
itemIndex++; // itemIndex = 3
extraWidthRight += ii.widthFactor; // extraWidthRight = 1.0
ii = itemIndex < mItems.size() ? 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);
}
}
// 设置主要的view为当前 mCurItem
mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
mAdapter.finishUpdate(this); // 显示更新结束
}
/**
* 在 item的数组里 position位置添加个新的 Item
*
* @param position
* @param index
* @return
*/
ItemInfo addNewItem(int position, int index) { // index = 0, position = 1
ItemInfo ii = new ItemInfo(); // 创建一个ItemInfo对象
ii.position = position;
ii.object = mAdapter.instantiateItem(this, position); // 调用 addView 方法
ii.widthFactor = mAdapter.getPageWidth(position); // ii.widthFactor = 1, getPageWidth 方法默认返回1
if (index < 0 || index >= mItems.size()) {
mItems.add(ii);
} else {
mItems.add(index, ii);
}
if (index < 0 || index >= mItems.size()) { // mItems.size() = 0, index = 0
mItems.add(ii); // 添加到页面预存数组里
} else {
mItems.add(index, ii);
}
return ii;
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) { // index = -1
Log.i(TAG, "addView");
if (!checkLayoutParams(params)) { // 检查参数是否是 ViewGroup.LayoutParams实例
params = generateLayoutParams(params);
}
final LayoutParams lp = (LayoutParams) params;
lp.isDecor |= child instanceof Decor; // 判断是不是挂件视图,lp.isDecor = false
if (mInLayout) { // mInLayout = true
if (lp != null && lp.isDecor) {
throw new IllegalStateException("Cannot add pager decor view during layout");
}
lp.needsMeasure = true;
addViewInLayout(child, index, params); // 如果index为-1,则添加到 mChildrenCount 位置,mChildrenCount = 0
} else {
super.addView(child, index, params);
}
}
/**
* 计算所有页面的偏移量
*
* @param curItem
* @param curIndex
* @param oldCurInfo
*/
private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
final int N = mAdapter.getCount(); // N = 7
final int width = getClientWidth(); // width = 768
final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; // marginOffset = 0
// Fix up offsets for later layout.
// Base all offsets off of curItem.
final int itemCount = mItems.size(); // itemCount = 3
float offset = curItem.offset; // offset = 0
int pos = curItem.position - 1; // pos = 0
mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; // mFirstOffset = -3.4028235E38
mLastOffset = curItem.position == N - 1 ?
curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE; // mLastOffset = 3.4028235E38
// 遍历上一页,curIndex = 1
for (int i = curIndex - 1; i >= 0; i--, pos--) {
final ItemInfo ii = mItems.get(i);
while (pos > ii.position) {
offset -= mAdapter.getPageWidth(pos--) + marginOffset;
}
offset -= ii.widthFactor + marginOffset;
ii.offset = offset; // ii.offset = -1
if (ii.position == 0) mFirstOffset = offset; // mFirstOffset = -1
}
offset = curItem.offset + curItem.widthFactor + marginOffset; // offset = 1, 所有item的widthFactor = 1
pos = curItem.position + 1; // pos = 2
// 遍历下一页, curIndex = 1, itemCount = 3
for (int i = curIndex + 1; i < itemCount; i++, pos++) {
final ItemInfo ii = mItems.get(i);
while (pos < ii.position) {
offset += mAdapter.getPageWidth(pos++) + marginOffset;
}
if (ii.position == N - 1) {
mLastOffset = offset + ii.widthFactor - 1;
}
ii.offset = offset; //ii.offset = 1
offset += ii.widthFactor + marginOffset;
}
mNeedCalculatePageOffsets = false;
}
第一次onMeasure是初始化往父布局的 mSortedHorizontalChildren 存储 ViewPager,然后循环存储 adapter里的 childview,直到全部存储完毕,onMeasure循环测量了八次。
private View[] mSortedHorizontalChildren;
接下来是 onSizeChanged(int w, int h, int oldw, int oldh)
// h = 360, w = 768, 高是我固定的
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// oldh = 0, oldw = 0
super.onSizeChanged(w, h, oldw, oldh);
// 确保滚动的位置是设置正确的
// Make sure scroll position is set correctly.
if (w != oldw) {
// recomputeScrollPosition(768, 0, 0, 0);
recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin);
}
}
private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) {
if (oldWidth > 0 && !mItems.isEmpty()) {
...
}
} else {
// mCurItem = 1, 目前是以 索引为1的页面为起始页,即 offset 是1,索引0的 offset 是 -1
final ItemInfo ii = infoForPosition(mCurItem);
// scrollOffset = 0
final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0;
// scrollPos = 0
final int scrollPos = (int) (scrollOffset *
(width - getPaddingLeft() - getPaddingRight()));
if (scrollPos != getScrollX()) { // getScrollY() = 0, scrollPos = 0
completeScroll(false);
scrollTo(scrollPos, getScrollY());
}
}
}
onLayout(boolean changed, int l, int t, int r, int b)
t = 0, l = 0, b = 360, r = 768, 我把高固定为 180dp.
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.i(TAG, "onLayout");
final int count = getChildCount(); // 预存的子 view 的数量, 3
int width = r - l; // width = 768
int height = b - t; // height = 360
int paddingLeft = getPaddingLeft(); // paddingLeft = 0
int paddingTop = getPaddingTop(); // paddingTop = 0
int paddingRight = getPaddingRight(); // paddingRight = 0
int paddingBottom = getPaddingBottom(); // paddingBottom = 0
final int scrollX = getScrollX(); // scrollx = 0
int decorCount = 0;
// First pass - decor views. We need to do this in two passes so that
// we have the proper offsets for non-decor views later.
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 childWidth = width - paddingLeft - paddingRight; // childWidth = 768
// 页面视图。
// Page views. Do this once we have the right padding offsets from above.
for (int i = 0; i < count; i++) {
// 根据添加childe view的顺序,获取child,第索引为1的页面因为首先被添加,所以先被获取到,然后是左边页面,最后是右边页面
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
ItemInfo ii;
if (!lp.isDecor && (ii = infoForChild(child)) != null) { // 调用infoForChild(child)通过 view 获取 ItemInfo
int loff = (int) (childWidth * ii.offset); // ii.offset = 『0,-1,1』 , loff = 『0,-768,768』
int childLeft = paddingLeft + loff; // 页面的左边框位置, childLeft = 『0,-768,768』
int childTop = paddingTop; // childTop = 0
if (lp.needsMeasure) { // lp.needsMeasure = true
// 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), // childWidth = 768, lp.widthFactor = 1.0
MeasureSpec.EXACTLY);
final int heightSpec = MeasureSpec.makeMeasureSpec(
(int) (height - paddingTop - paddingBottom), // height = 360, paddingTop=paddingBottom=0
MeasureSpec.EXACTLY);
child.measure(widthSpec, heightSpec);
}
if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
+ ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
+ "x" + child.getMeasuredHeight());
// childLeft = 『0,-768,768』
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
}
}
}
mTopPageBounds = paddingTop; // mTopPageBounds = 0
mBottomPageBounds = height - paddingBottom; // mBottomPageBounds = 360
mDecorChildCount = decorCount; // mDecorChildCount = 0
// 如果是第一次布局,则滑动到当前设置的页面位置
if (mFirstLayout) { // mFirstLayout = true
scrollToItem(mCurItem, false, 0, false);
}
mFirstLayout = false; // 现在,第一次布局就结束了
}
/**
* 通过 view 获取 ItemInfo
**/
ItemInfo infoForChild(View child) {
for (int i = 0; i < mItems.size(); i++) {
ItemInfo ii = mItems.get(i);
if (mAdapter.isViewFromObject(child, ii.object)) {
return ii;
}
}
return null;
}
/**
* 滑动到 item 页
*
* @param item
* @param smoothScroll
* @param velocity
* @param dispatchSelected
*/
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(); // width = 768
// 获取 item 的水平 x 偏移值, destX = 0, mFirstOffset = -1, mLastOffset = 3.4028235E38, curInfo.offset = 0
destX = (int) (width * Math.max(mFirstOffset,
Math.min(curInfo.offset, mLastOffset)));
}
if (smoothScroll) { // smoothScroll = false
// 如果平滑的滑动
smoothScrollTo(destX, 0, velocity);
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
} else {
if (dispatchSelected) { // dispatchSelected = false
dispatchOnPageSelected(item);
}
completeScroll(false);
scrollTo(destX, 0); // 滚动到(0,0)位置
pageScrolled(destX);
}
}
private void completeScroll(boolean postEvents) {
boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING; // needPopulate = false
if (needPopulate) {
// ...
}
}
mPopulatePending = false;
// 遍历预存的所有页面
for (int i = 0; i < mItems.size(); i++) {
ItemInfo ii = mItems.get(i);
if (ii.scrolling) { // ii.scrolling 三个页面都是 false
needPopulate = true;
ii.scrolling = false;
}
}
if (needPopulate) { // needPopulate = false
if (postEvents) {
ViewCompat.postOnAnimation(this, mEndScrollRunnable);
} else {
mEndScrollRunnable.run();
}
}
}
private boolean pageScrolled(int xpos) {
// 当前滚动位置的页面的信息
final ItemInfo ii = infoForCurrentScrollPosition();
final int width = getClientWidth(); // width = 768
final int widthWithMargin = width + mPageMargin; // widthWithMargin = 768
final float marginOffset = (float) mPageMargin / width;
final int currentPage = ii.position; // currentPage = 1
// xpos = 0, width = 768, pageOffset = 0, 当前是第(索引1)页,ii.offset = 0
final float pageOffset = (((float) xpos / width) - ii.offset) /
(ii.widthFactor + marginOffset);
final int offsetPixels = (int) (pageOffset * widthWithMargin); // offsetPixels = 0
mCalledSuper = false;
onPageScrolled(currentPage, pageOffset, offsetPixels); // 调用这个方法后 mCalledSuper = true
if (!mCalledSuper) {
throw new IllegalStateException(
"onPageScrolled did not call superclass implementation");
}
return true;
}
/**
* @return 当前滚动位置的页面的信息。
* This can be synthetic for a missing middle page; the 'object' field can be null.
*/
private ItemInfo infoForCurrentScrollPosition() {
final int width = getClientWidth(); // width = 768
final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0; // scrollOffset = 0
final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; // marginOffset = 0
int lastPos = -1;
float lastOffset = 0.f;
float lastWidth = 0.f;
boolean first = true;
ItemInfo lastItem = null;
// 遍历所有预存的页面
for (int i = 0; i < mItems.size(); i++) {
ItemInfo ii = mItems.get(i); // mItems 的页面存储顺序是从左往右的顺序
float offset;
if (!first && ii.position != lastPos + 1) { // 第一次不进来,第二次开始判断这一页是否丢失
// Create a synthetic item for a missing page.
ii = mTempItem;
ii.offset = lastOffset + lastWidth + marginOffset;
ii.position = lastPos + 1;
ii.widthFactor = mAdapter.getPageWidth(ii.position);
i--;
}
offset = ii.offset; // ii.offset = {-1, 0, 1}
// bound 的意思 『-1』 页[0] 『0』 页[1] 『1』 页[2] 『2』 , 『n』代表边界
final float leftBound = offset; // leftBound = {-1, 0, 1}
final float rightBound = offset + ii.widthFactor + marginOffset; // rightBound = {0, 1, 2}
if (first || scrollOffset >= leftBound) { // scrollOffset = 0
if (scrollOffset < rightBound || i == mItems.size() - 1) {
return ii;
}
} else {
return lastItem;
}
first = false;
lastPos = ii.position;
lastOffset = offset;
lastWidth = ii.widthFactor;
lastItem = ii; // 存储已经检查过的 item
}
return lastItem;
}
/**
* 当页面被选择的时候,这个方法被调用。要么是代码启动平滑滚动或者用户触摸滑动。
* 如果你重写这个方法,一定要调用父类的实现方法(e.g. super.onPageScrolled(position, offset, offsetPixels))。
*
* @param position position 索引是当前展示页面的第一页索引,当offset为0时,position+1会展示出来。
* @param offset 页面位置的偏移,取值[0,1)。
* @param offsetPixels 页面偏移的像素大小。
*/
@CallSuper
protected void onPageScrolled(int position, float offset, int offsetPixels) {
// 保证挂件视图一直在屏幕上.
if (mDecorChildCount > 0) {
// ...
}
// 回调接口的 onPageScrolled 方法
dispatchOnPageScrolled(position, offset, offsetPixels);
// 切换动画, 这里我没有设置, 是null
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);
}
}
mCalledSuper = true;
}
当页面位置陈列好后,又进行了 8次测量,调用了8次 onMeasure 方法, 完成测量后,又调用了一次 onLayout 方法。
draw(Canvas canvas) 和 onDraw(Canvas canvas) 方法被调用
@Override
public void draw(Canvas canvas) {
Log.i(TAG, "draw");
/*
* super.draw(canvas) 会顺序执行以下几个功能:
* 1. 绘制background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. 调用 onDraw(Canvas canvas);
* 4. Draw children ,调用 dispatchDraw(canvas);
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance), 调用 onDrawForeground(canvas);
*/
super.draw(canvas); // 在这里先调用了 onDraw
// 主要用于初始化绘制边缘效果
boolean needsInvalidate = false;
final int overScrollMode = ViewCompat.getOverScrollMode(this);
if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
(overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&
mAdapter != null && mAdapter.getCount() > 1)) {
if (!mLeftEdge.isFinished()) {
final int restoreCount = canvas.save();
final int height = getHeight() - getPaddingTop() - getPaddingBottom();
final int width = getWidth();
canvas.rotate(270);
canvas.translate(-height + getPaddingTop(), mFirstOffset * width);
mLeftEdge.setSize(height, width);
needsInvalidate |= mLeftEdge.draw(canvas);
canvas.restoreToCount(restoreCount);
}
if (!mRightEdge.isFinished()) {
final int restoreCount = canvas.save();
final int width = getWidth();
final int height = getHeight() - getPaddingTop() - getPaddingBottom();
canvas.rotate(90);
canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width);
mRightEdge.setSize(height, width);
needsInvalidate |= mRightEdge.draw(canvas);
canvas.restoreToCount(restoreCount);
}
} else {
mLeftEdge.finish();
mRightEdge.finish();
}
if (needsInvalidate) {
// Keep animating
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
protected void onDraw(Canvas canvas) {
Log.i(TAG, "onDraw");
super.onDraw(canvas);
// 主要用于绘制页面之间的 margin drawable, 这里我没有设置,所以直接就跳过了
// Draw the margin drawable between pages if needed.
if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {
final int scrollX = getScrollX();
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;
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;
}
if (drawAt + mPageMargin > scrollX) {
mMarginDrawable.setBounds((int) drawAt, mTopPageBounds,
(int) (drawAt + mPageMargin + 0.5f), mBottomPageBounds);
mMarginDrawable.draw(canvas);
}
if (drawAt > scrollX + width) {
break; // No more visible, no sense in continuing
}
}
}
}
最后调用了 computeScroll()
@Override
public void computeScroll() {
if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { // mScroller.isFinished() = true
int oldX = getScrollX();
int oldY = getScrollY();
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
if (oldX != x || oldY != y) {
scrollTo(x, y);
if (!pageScrolled(x)) {
mScroller.abortAnimation();
scrollTo(0, y);
}
}
// Keep on drawing until the animation has finished.
ViewCompat.postInvalidateOnAnimation(this);
return;
}
// 完成滚动,清除状态
completeScroll(true);
}
private void completeScroll(boolean postEvents) {
// mScrollState = 0,即静止状态,不是滑动结束后固定下来的状态
boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;
if (needPopulate) {
// Done with scroll, no longer want to cache view drawing.
setScrollingCacheEnabled(false);
mScroller.abortAnimation();
int oldX = getScrollX();
int oldY = getScrollY();
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
if (oldX != x || oldY != y) {
scrollTo(x, y);
if (x != oldX) {
pageScrolled(x);
}
}
}
mPopulatePending = false;
for (int i = 0; i < mItems.size(); i++) {
ItemInfo ii = mItems.get(i);
if (ii.scrolling) {
needPopulate = true;
ii.scrolling = false;
}
}
if (needPopulate) {
if (postEvents) {
ViewCompat.postOnAnimation(this, mEndScrollRunnable);
} else {
mEndScrollRunnable.run();
}
}
}
到此为止,ViewPager 已经初始化创建完毕了,解下来就是手势监听的一些理解。
ViewPager 的手势监听
首先是三个标志位 /** * 表明这个页面是闲置状态。当前页面在view上是完整的,并且没有动画。 * 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;
onInterceptTouchEvent 事件拦截
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i(TAG, "onInterceptTouchEvent");
/*
* 这个方法决定我们是否需要拦截动作行为。
* 如果返回true,onMotionEvent方法就会被调用,在那里进行实际的滚蛋你个操作。
*/
final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
// 总是要判断触摸手势是否已经完成。
// Always take care of the touch gesture being complete.
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
// 释放拖拽.
if (DEBUG) Log.v(TAG, "Intercept done!");
resetTouch();
return false;
}
// 如果已经决定是否拖拽,则不要做更多的事。
// Nothing more to do here if we have decided whether or not we
// are dragging.
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, 否则事件已经被拦截。检查是否用户从原始的按下触摸点移动到足够远的地方。
*/
/*
* Locally do absolute value. mLastMotionY is set to the y value
* of the down event.
*/
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// 如果我们没有一个有效的id, 那么我们按下的触摸事件没有发生在content中。
break;
}
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float dx = x - mLastMotionX; // 手指移动的水平方向的距离
final float xDiff = Math.abs(dx); // 手指移动的距离的水平方向的绝对值
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);
if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
canScroll(this, false, (int) dx, (int) x, (int) y)) {
// Nested view has scrollable area under this point. Let it be handled there.
mLastMotionX = x;
mLastMotionY = y;
mIsUnableToDrag = true;
return false;
}
if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
// 开始拖拽,拦截事件
if (DEBUG) Log.v(TAG, "Starting drag!");
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
// 存储移动后的x,y点
mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
mInitialMotionX - mTouchSlop;
mLastMotionY = y;
setScrollingCacheEnabled(true);
} else if (yDiff > mTouchSlop) {
// 如果用户是垂直方向上的移动则不进行拦截,不能拦截
// The finger has moved enough in the vertical
// direction to be counted as a drag... abort
// any attempt to drag horizontally, to work correctly
// with children that have scrolling containers.
if (DEBUG) Log.v(TAG, "Starting unable to drag!");
mIsUnableToDrag = true;
}
if (mIsBeingDragged) {
// 执行手指触摸时的拖拽
if (performDrag(x)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
/*
* 记录按下触摸的位置。
* Remember location of down touch.
* ACTION_DOWN 总是指第0个索引的触摸点,即当前触摸的第一个手指的触摸点.
*/
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsUnableToDrag = false;
mScroller.computeScrollOffset();
if (mScrollState == SCROLL_STATE_SETTLING &&
Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
// 如果页面用户触摸的页面处于 SCROLL_STATE_SETTLING 状态
// mScroller.getFinalX()指页面移动结束的位置。
// 让用户“抓住”页面。
// Let the user 'catch' the pager as it animates.
mScroller.abortAnimation();
mPopulatePending = false;
populate();
mIsBeingDragged = true; // 开始拖拽
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;
}
onTouchEvent 触摸事件的处理
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.i(TAG, "onTouchEvent");
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;
}
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;
}
if (mAdapter == null || mAdapter.getCount() == 0) {
// Nothing to present or scroll; nothing to touch.
return false;
}
if (mVelocityTracker == null) {
// 速度检测器
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
final int action = ev.getAction();
boolean needsInvalidate = false;
switch (action & MotionEventCompat.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: { // 按下
mScroller.abortAnimation();
mPopulatePending = false;
populate();
// 记录按下触摸的点
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
break;
}
case MotionEvent.ACTION_MOVE:
if (!mIsBeingDragged) {
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (pointerIndex == -1) {
// A child has consumed some touch events and put us into an inconsistent state.
needsInvalidate = resetTouch();
break;
}
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float xDiff = Math.abs(x - mLastMotionX);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float yDiff = Math.abs(y - mLastMotionY);
if (DEBUG)
Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
if (xDiff > mTouchSlop && xDiff > yDiff) {
// 开始拖拽
if (DEBUG) Log.v(TAG, "Starting drag!");
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
mInitialMotionX - mTouchSlop;
mLastMotionY = y;
setScrollState(SCROLL_STATE_DRAGGING);
setScrollingCacheEnabled(true);
// 不允许parent拦截
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
}
// Not else! Note that mIsBeingDragged can be set above.
if (mIsBeingDragged) {
// Scroll to follow the motion event
final int activePointerIndex = MotionEventCompat.findPointerIndex(
ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, activePointerIndex);
// 执行手指拖拽
needsInvalidate |= performDrag(x);
}
break;
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) { // 如果是拖拽状态
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
// 获取滑动速度
int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
velocityTracker, mActivePointerId);
mPopulatePending = true;
final int width = getClientWidth();
final int scrollX = getScrollX();
// 获取当前页的item信息
final ItemInfo ii = infoForCurrentScrollPosition();
// 存储当前页面的位置索引
final int currentPage = ii.position;
final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor;
final int activePointerIndex =
MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, activePointerIndex);
// 获取手指滑动距离
final int totalDelta = (int) (x - mInitialMotionX);
// 通过手指滑动距离和速度计算会滑动到哪个页面
int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
totalDelta);
// 滑动到 nextPage 页
setCurrentItemInternal(nextPage, true, true, initialVelocity);
needsInvalidate = resetTouch();
}
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged) {
scrollToItem(mCurItem, true, 0, false);
needsInvalidate = resetTouch();
}
break;
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int index = MotionEventCompat.getActionIndex(ev);
final float x = MotionEventCompat.getX(ev, index);
mLastMotionX = x;
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionX = MotionEventCompat.getX(ev,
MotionEventCompat.findPointerIndex(ev, mActivePointerId));
break;
}
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
return true;
}
执行拖拽方法 performDrag(float x)
/**
* 执行拖拽
*
* @param x
* @return
*/
private boolean performDrag(float x) {
boolean needsInvalidate = false;
final float deltaX = mLastMotionX - x;
mLastMotionX = x;
float oldScrollX = getScrollX();
float scrollX = oldScrollX + deltaX;
final int width = getClientWidth();
float leftBound = width * mFirstOffset;
float rightBound = width * mLastOffset;
boolean leftAbsolute = true;
boolean rightAbsolute = true;
// 获取预存的第一个item和最后一个item
final ItemInfo firstItem = mItems.get(0);
final ItemInfo lastItem = mItems.get(mItems.size() - 1);
if (firstItem.position != 0) {
// 如果这个页面不是 adapter 需要展示的第一个页面
leftAbsolute = false;
leftBound = firstItem.offset * width;
}
if (lastItem.position != mAdapter.getCount() - 1) {
// 如果这个页面不是 adapter 需要展示的最后一个页面
rightAbsolute = false;
rightBound = lastItem.offset * width;
}
if (scrollX < leftBound) {
// 滑动到了第一个预存界面的左边界
if (leftAbsolute) {
// 显示边缘效果
float over = leftBound - scrollX;
needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width);
}
scrollX = leftBound;
} else if (scrollX > rightBound) {
// 滑动超过了最后一个预存界面的左边界
if (rightAbsolute) {
// 显示边缘效果
float over = scrollX - rightBound;
needsInvalidate = mRightEdge.onPull(Math.abs(over) / width);
}
scrollX = rightBound;
}
// Don't lose the rounded component
mLastMotionX += scrollX - (int) scrollX;
scrollTo((int) scrollX, getScrollY());
pageScrolled((int) scrollX);
return needsInvalidate;
}
总结
滑动的过程, mItems 会把移进预存范围内的页面初始化添加进来,并把移动到范围外的页面移除掉,mItems 的size 始终保持在 【0, 1 + mOffscreenPageLimit * 2】。 以上是我对 ViewPager 单步调试后的 源码理解,让我对其有了更深入的了解,或许有些地方还有纰漏,请指正。 谢谢! 祝,身体健康。