人生得意須盡歡,
桃花塢里桃花庵。
對于ViewPager
其實本來沒啥事也不想研究它的,無奈最近在開發一款宇宙無敵吊炸天的控件 《開發一款商業級Banner控件》時碰到了一些天坑,強大的google和度娘沒能解決我的問題,懷著不怪網上連能人少,只恨問題太奇葩
沉痛的心情,只有自己對著 ViewPager
源碼擼一發了。
本文遵循以下節點 依次展開內容。
1、測量 onMeasure
自定義View三部曲 ,那自然是onMeasure
了;
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 沒有指定的情況下寬高為0 ;
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));
...
// 得到可用空間
int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
// 得到孩子總數量 遍歷
int size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) { //如果屬性不為GONE
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//如果是裝飾view
if (lp != null && lp.isDecor) {
//得到重力位置
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
int widthMode = MeasureSpec.AT_MOST; //默認測量模式
int heightMode = MeasureSpec.AT_MOST;//默認測量模式
// 計算是否消費 縱向 空間
boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
//計算是否消費 橫向 空間
boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
// 設置測量模式
if (consumeVertical) {
widthMode = MeasureSpec.EXACTLY;
} else if (consumeHorizontal) {
heightMode = MeasureSpec.EXACTLY;
}
// 父類最大可提供的大小
int widthSize = childWidthSize;
int heightSize = childHeightSize;
//計算測量模式 與 父類可提供的大小
if (lp.width != LayoutParams.WRAP_CONTENT) {
// 測量模式為 EXACTLY
widthMode = MeasureSpec.EXACTLY;
//如果width為 match_parent
if (lp.width != LayoutParams.MATCH_PARENT) {
//測量寬度就等于 實際設置的寬度
widthSize = lp.width;
}
}
//與上面 width 測量類似
if (lp.height != LayoutParams.WRAP_CONTENT) {
heightMode = MeasureSpec.EXACTLY;
if (lp.height != LayoutParams.MATCH_PARENT) {
heightSize = lp.height;
}
}
//通過makeMeasureSpec方法 合成測量規格
final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
// 調用child.measure方法
child.measure(widthSpec, heightSpec);
// 如果被消費,那么可用空間減去消費掉的空間
if (consumeVertical) {
childHeightSize -= child.getMeasuredHeight();
} else if (consumeHorizontal) {
childWidthSize -= child.getMeasuredWidth();
}
}
}
}
// 合成測量規格
mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
mInLayout = true;
//這個方法很厲害
populate();
mInLayout = false;
// 遍歷孩子VIew
size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
if (DEBUG) {
Log.v(TAG, "Measuring #" + i + " " + child + ": " + mChildWidthMeasureSpec);
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp == null || !lp.isDecor) {
//做成新的規格 其中寬度=childWidthSize * lp.widthFactor
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
//調用孩子的 measure 方法
child.measure(widthSpec, mChildHeightMeasureSpec);
}
}
}
}
1、可以看到就是先是給自己設定了大小;其中用了getDefaultSize
方法,所以我們給viewPager設置的 wrap_content最終還是 match_parent的效果;
2、然后就是遍歷所有的裝飾view 測量它們的大小 與 計算它們消耗的空間,與計算剩余空間;
3、調用populate
方法
4、測量孩子; 其中給孩子的寬度為 剩余寬度*lp.widthFactor
也是這段代碼final int widthSpec = MeasureSpec.makeMeasureSpec( (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
populate
那我們再來看一下 populate
吧
populate
顧名思義 填充的意思。
void populate() {
// 將當前選中position放入
populate(mCurItem);
}
void populate(int newCurrentItem) {
// 舊的選中的 ItemInfo
ItemInfo oldCurInfo = null;
if (mCurItem != newCurrentItem) {
//infoForPosition方法得到 舊 ItemInfo
oldCurInfo = infoForPosition(mCurItem);
mCurItem = newCurrentItem;
}
if (mAdapter == null) {
//排序繪制view的順序
sortChildDrawingOrder();
return;
}
// Bail now if we are waiting to populate. This is to hold off
// on creating views from the time the user releases their finger to
// fling to a new position until we have finished the scroll to
// that position, avoiding glitches from happening at that point.
// 如果是在等待填充 的時候,,解釋是說為了避免 在用戶釋放手指之前創建view
if (mPopulatePending) {
if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
sortChildDrawingOrder();
return;
}
// 不在window中時
if (getWindowToken() == null) {
return;
}
// 調用 startUpdate 方法
mAdapter.startUpdate(this);
//頁的限制
final int pageLimit = mOffscreenPageLimit;
//開始頁
final int startPos = Math.max(0, mCurItem - pageLimit);
//總數
final int N = mAdapter.getCount();
// 結束頁
final int endPos = Math.min(N - 1, mCurItem + pageLimit);
// 如果預期數量不一致
if (N != mExpectedAdapterCount) {
String resName;
try {
//得到資源名稱
resName = getResources().getResourceName(getId());
} catch (Resources.NotFoundException e) {
resName = Integer.toHexString(getId());
}
throw new IllegalStateException("The application's PagerAdapter changed the adapter's"
+ " contents without calling PagerAdapter#notifyDataSetChanged!"
+ " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N
+ " Pager id: " + resName
+ " Pager class: " + getClass()
+ " Problematic adapter: " + mAdapter.getClass());
}
// Locate the currently focused item or add it if needed.
int curIndex = -1;
ItemInfo curItem = null;
for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
//得到當前 ItemInfo
final ItemInfo ii = mItems.get(curIndex);
//如果 position 大于 mCurItem
if (ii.position >= mCurItem) {
// cutlItem=ii
if (ii.position == mCurItem) curItem = ii;
break;
}
}
// 如果等于空 添加
if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex);
}
// 如果 curItem 不為空
if (curItem != null) {
float extraWidthLeft = 0.f;
//-1
int itemIndex = curIndex - 1;
//ii 如果itemIndex 大于 0 取出,否則null
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
//得到可用寬度
final int clientWidth = getClientWidth();
// 計算左邊需要的寬度
final float leftWidthNeeded = clientWidth <= 0 ? 0 :
2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
// 從'選中的下標-1' --> 0 遍歷
for (int pos = mCurItem - 1; pos >= 0; pos--) {
//如果額外的 大于 需要的 和 pos 小于 startPos
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
if (ii == null) {
break;
}
// 移除
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, pos, ii.object);
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos
+ " view: " + ((View) ii.object));
}
itemIndex--;
curIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) { // 如果選中的上一個有 infoItem
// extraWidthLeft 加上 ii.widthFactor
extraWidthLeft += ii.widthFactor;
itemIndex--;
//拿上一個 infoItem
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
} else {
// 新增一個 INfoItem
ii = addNewItem(pos, itemIndex + 1);
// 將占用的寬度比例加進來
extraWidthLeft += ii.widthFactor;
curIndex++;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}
// 右邊消耗的比例
float extraWidthRight = curItem.widthFactor;
//右邊index
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++) {
// 如果 pos大于 endPos
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 {
// 否則 新建 infoItem
ii = addNewItem(pos, itemIndex);
itemIndex++;
extraWidthRight += ii.widthFactor;
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);
}
}
// 通知適配器 哪個個是主頁
mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
// 結束更新
mAdapter.finishUpdate(this);
// Check width measurement of current pages and drawing sort order.
// Update LayoutParams as needed.
final int childCount = getChildCount();
// 遍歷view
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
lp.childIndex = i;
if (!lp.isDecor && lp.widthFactor == 0.f) {
// 0 means requery the adapter for this, it doesn't have a valid width.
final ItemInfo ii = infoForChild(child);
if (ii != null) {
// 設置屬性
lp.widthFactor = ii.widthFactor;
lp.position = ii.position;
}
}
}
//排序
sortChildDrawingOrder();
if (hasFocus()) {
View currentFocused = findFocus();
ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
if (ii == null || ii.position != mCurItem) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
ii = infoForChild(child);
//如果拿到當前view
if (ii != null && ii.position == mCurItem) {
//設置聚焦
if (child.requestFocus(View.FOCUS_FORWARD)) {
break;
}
}
}
}
}
}
populate
的作用:
通過當前選中項, 移除兩邊 不需要的 view以及 infoItem;
通過選中項,創建需要view 以及 infoItem;
計算設置view的偏移量信息(計算位置放入 infoItem);
設置聚焦當前選中項child.requestFocus(View.FOCUS_FORWARD)
(焦點轉移處理);
2、布局 onLayout()
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();
// 裝飾view的數量
int decorCount = 0;
//遍歷
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
// 設置不為GONE
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;
//根據重力的方向 計算位置
switch (hgrav) {
default:
childLeft = paddingLeft;
break;
case Gravity.LEFT:
childLeft = paddingLeft;
paddingLeft += child.getMeasuredWidth();
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;
}
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 += scrollX;
//布局裝飾視圖
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
decorCount++;
}
}
}
//最終可用的寬度
final int childWidth = width - paddingLeft - paddingRight;
//遍歷
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
//如果不為空
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
ItemInfo ii;
//如果不為裝飾,以及 infoForChild方法不為空
if (!lp.isDecor && (ii = infoForChild(child)) != null) {
// 偏移
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);
}
if (DEBUG) {
Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
+ ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
+ "x" + child.getMeasuredHeight());
}
//布局
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
}
}
}
//上邊距位置
mTopPageBounds = paddingTop;
// 下臨界點位置
mBottomPageBounds = height - paddingBottom;
mDecorChildCount = decorCount;
if (mFirstLayout) {
// 滑動至當前頁面
scrollToItem(mCurItem, false, 0, false);
}
mFirstLayout = false;
}
布局做了以下這些事:
布局裝飾視圖,并消耗一定的空間;
在剩余空間中布局view; 而我們的view的布局位置 與 ItemInfo.offset
有關。
3、setAdapter
public void setAdapter(PagerAdapter adapter) {
//如果不為空
if (mAdapter != null) {
//移除監聽
mAdapter.setViewPagerObserver(null);
// 調用 startUpdate方法
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
removeNonDecorViews();
mCurItem = 0;
//滑動至 0,0
scrollTo(0, 0);
}
//賦值
final PagerAdapter oldAdapter = mAdapter;
mAdapter = adapter;
// 支持的數量置為零
mExpectedAdapterCount = 0;
if (mAdapter != null) {·
//初始化 觀察者
if (mObserver == null) {
mObserver = new PagerObserver();
}
//設置監聽
mAdapter.setViewPagerObserver(mObserver);
// 支持填充
mPopulatePending = false;
final boolean wasFirstLayout = mFirstLayout;
mFirstLayout = true;
//數量
mExpectedAdapterCount = mAdapter.getCount();
// mRestoredCurItem >=0
if (mRestoredCurItem >= 0) {
//調用適配器 回復狀態方法
mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
//設置當前item
setCurrentItemInternal(mRestoredCurItem, false, true);
mRestoredCurItem = -1;
mRestoredAdapterState = null;
mRestoredClassLoader = null;
//如果不是第一次布局
} else if (!wasFirstLayout) {
// 填充
populate();
} else {
//重新布局
requestLayout();
}
}
// 分發一些改變給 監聽者
if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) {
for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) {
mAdapterChangeListeners.get(i).onAdapterChanged(this, oldAdapter, adapter);
}
}
}
setAdapter
先是判斷 是否有舊適配器;移除舊適配器,view,監聽等等。
存放新的adapter,設置新的監聽;
如果非第一次布局 那么調用populate
4、手勢 Gesture , touch
手勢呢 是一個比較難得點。 我們想要搞懂的是:
- 它是如何根據手勢來滑動的;
- 在手勢過程中 加載 view的流程是怎么樣的;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//拿到手勢事件
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) {
// Release the drag.
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, otherwise the shortcut would have caught it. Check
* whether the user has moved far enough from his original down touch.
*/
/*
* Locally do absolute value. mLastMotionY is set to the y value
* of the down event.
*/
//手勢點
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
//得到該觸摸點的下標
final int pointerIndex = ev.findPointerIndex(activePointerId);
// 得到該觸摸點的 x
final float x = ev.getX(pointerIndex);
// 相對值
final float dx = x - mLastMotionX;
final float xDiff = Math.abs(dx);
final float y = ev.getY(pointerIndex);
final float yDiff = Math.abs(y - mInitialMotionY);
if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
//判斷子view是否有滑動動作
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;
// 通知父view不要攔截
requestParentDisallowInterceptTouchEvent(true);
// 設置滑動狀態
setScrollState(SCROLL_STATE_DRAGGING);
// 設置最后坐標
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) {
// Scroll to follow the motion event
// 執行滑動且 計算是否需要 刷新界面
if (performDrag(x)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
/*
* Remember location of down touch.
* ACTION_DOWN always refers to pointer index 0.
*/
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
// 設置觸摸標識id
mActivePointerId = ev.getPointerId(0);
mIsUnableToDrag = false;
mIsScrollStarted = true;
//
mScroller.computeScrollOffset();
// 如果狀態==SCROLL_STATE_SETTLING 和 預估的滑動絕對值大于mCloseEnough
if (mScrollState == SCROLL_STATE_SETTLING
&& Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
// 中止scroller的動畫
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);
/*
* The only time we want to intercept motion events is if we are in the
* drag mode.
*/
return mIsBeingDragged;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
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();
// Remember where the motion event started
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
mActivePointerId = ev.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE:
//如果還沒有開始拖拽
if (!mIsBeingDragged) {
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) {
// A child has consumed some touch events and put us into an inconsistent
// state.
needsInvalidate = resetTouch();
break;
}
final float x = ev.getX(pointerIndex);
final float xDiff = Math.abs(x - mLastMotionX);
final float y = ev.getY(pointerIndex);
final float yDiff = Math.abs(y - mLastMotionY);
if (DEBUG) {
Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
}
//如果滿足拖拽條件
if (xDiff > mTouchSlop && xDiff > yDiff) {
if (DEBUG) Log.v(TAG, "Starting drag!");
// 設置為true
mIsBeingDragged = true;
// 請求父布局不允許攔截
requestParentDisallowInterceptTouchEvent(true);
mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
mInitialMotionX - mTouchSlop;
mLastMotionY = y;
setScrollState(SCROLL_STATE_DRAGGING);
setScrollingCacheEnabled(true);
// Disallow Parent Intercept, just in case
ViewParent parent = getParent();
if (parent != null) {
// 請求父布局不允許攔截
parent.requestDisallowInterceptTouchEvent(true);
}
}
}
// Not else! Note that mIsBeingDragged can be set above.
if (mIsBeingDragged) {
// Scroll to follow the motion event
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(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();
// 得到當前滑動的 ItemInfo
final ItemInfo ii = infoForCurrentScrollPosition();
// margin偏移
final float marginOffset = (float) mPageMargin / width;
// 得到當前頁下標
final int currentPage = ii.position;
// 計算偏移量
final float pageOffset = (((float) scrollX / width) - ii.offset)
/ (ii.widthFactor + marginOffset);
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(activePointerIndex);
final int totalDelta = (int) (x - mInitialMotionX);
// 確定下一個目標oage
int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
totalDelta);
//滑動至下一個page 并且帶有速度的滑動
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 = ev.getX(index);
mLastMotionX = x;
mActivePointerId = ev.getPointerId(index);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
//抬起第二根手指
onSecondaryPointerUp(ev);
mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
break;
}
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
return true;
}
先還是捋一下這個流程:
onInterceptTouchEvent
1、如果是行為是ACTION_CANCEL
或者是ACTION_UP
那么就重置觸摸resetTouch()
并且不攔截事件;
2、如果行為不是ACTION_DOWN
判斷 如果mIsBeingDragged
已經開始拖拽了 那么返回true,如果mIsUnableToDrag
返回 false;
3、在行為是ACTION_MOVE
的時候:等到當前觸摸點的各種信息,計算偏移量,如果子布局自身也是當前可滑動的canScroll()返回true
那么不攔截事件;
4、在行為是ACTION_MOVE
的時候:通過偏移量計算滑動方向,如果是橫向滑動 將mIsBeingDragged
置為true
代表開始拖拽了,并且通過requestParentDisallowInterceptTouchEvent(true);
通知副布局不要攔截事件; 如果認為是縱向滑動,那么將mIsUnableToDrag = true;
;如果mIsUnableToDrag = true;
調用
performDrag(x) 如果返回true 調用ViewCompat.postInvalidateOnAnimation(this);
;
5、行為是ACTION_DOWN
時:各種數據 調用populate
, 刷新一次狀態
6、行為是ACTION_POINTER_UP
時 設置一些參數;
onTouchEvent
1、先也是一些類的判斷 ,什么adapter是否為空,是否點擊在邊界等等。
2、行為是ACTION_DOWN
先停止scroller, 在調用populate
,在記錄位置,活動點(手指點標識)
3、行為是ACTION_MOVE
如果 !mIsBeingDragged
通過觸摸點位置計算是否達到拖拽標準以及方向 將mIsBeingDragged=true; 如果mIsBeingDragged 調用perfrom(x)
執行拖拽。
4、行為ACTION_UP
時,如果 mIsBeingDragged ,那么調用determineTargetPage
方法 計算需要滑動至那一頁nextPage
。在調用setCurrentItemInternal
滑動至nextPage
頁,resetTouch。
5、行為為ACTION_CANCEL
時 如果是mIsBeingDragged
滑動至當前頁;
6、行為為ACTION_POINTER_DOWN
切換觸摸點(手指)
7、行為為ACTION_POINTER_UP
手指抬起 各種初始化。
以上有些比較重要的方法 可以拿出來遛一遛;
setScrollState(); 設置滑動狀態
setScrollingCacheEnabled() 滑動時緩存;
determineTargetPage; 計算下一個page頁是哪個;
setCurrentItemInternal 設置當前頁 以及是否滑動;
scrollToItem 滑動至某項;
onPageScrolled( ) ; 通知滑動的監聽,
哎呀 這些就一個個去看吧
5、notifyDataSetChanged
1、刷新開始
PageAdapter.class
public void notifyDataSetChanged() {
synchronized (this) {
if (mViewPagerObserver != null) {
mViewPagerObserver.onChanged();
}
}
mObservable.notifyChanged();
}
public void unregisterDataSetObserver(DataSetObserver observer) {
mObservable.unregisterObserver(observer);
}
void setViewPagerObserver(DataSetObserver observer) {
synchronized (this) {
mViewPagerObserver = observer;
}
}
ok,看到這就知道這是個觀察者模式,而且成員變量mViewPagerObserver
是在 setViewPagerObserver
方法中設置進來的, 那么現在 找到這個'mViewPagerObserver' 是個什么東西以及mViewPagerObserver.onChanged()
方法都做了些什么
2、PagerObserver 以及 dataSetChanged()
可以看到 在ViewPager
當中我們創建了 PagerObserver
并調用了 mAdapter.setViewPagerObserver(mObserver);
所以我們的 mViewPagerBoserver
是PagerObserver
; 它又調用了dataSetChanged()
方法
ViewPager.class
public void setAdapter(PagerAdapter adapter) {
....
if (mAdapter != null) {
if (mObserver == null) {
mObserver = new PagerObserver();
}
//監聽adapter
mAdapter.setViewPagerObserver(mObserver);
}
// viewPager內部類
private class PagerObserver extends DataSetObserver {
PagerObserver() {
}
@Override
public void onChanged() {
// 數據改變時調用這個方法
dataSetChanged();
}
@Override
public void onInvalidated() {
dataSetChanged();
}
}
3、dataSetChanged
void dataSetChanged() {
// This method only gets called if our observer is attached, so mAdapter is non-null. 只給observer調用
// 拿到數據數量
final int adapterCount = mAdapter.getCount();
mExpectedAdapterCount = adapterCount;
//判斷是否需要填充
boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1
&& mItems.size() < adapterCount;
//拿到當前選中的item下標
int newCurrItem = mCurItem;
boolean isUpdating = false;
//循環mitems
for (int i = 0; i < mItems.size(); i++) {
final ItemInfo ii = mItems.get(i);
final int newPos = mAdapter.getItemPosition(ii.object);
if (newPos == PagerAdapter.POSITION_UNCHANGED) {
continue;
}
if (newPos == PagerAdapter.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;
}
...
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;
}
}
//滑動至當前頁
setCurrentItemInternal(newCurrItem, false, true);
requestLayout();
}
}
可以看到 大致的意思是:循環遍歷視圖上所有的item, 并通過adapter.getgetItemPosition
方法 拿到一個newPos
,該值有兩種狀態 一種:PagerAdapter.POSITION_UNCHANGED
和PagerAdapter.POSITION_NONE
,如果為PagerAdapter.POSITION_UNCHANGED
狀態那么就不刷新itemView,否則 先銷毀現有的itemView再重新創建一個新的itemVIew; 最后滑動至指定頁,以及requestLayout。
好的 分析得差不多。 之后碰到問題就應該不會迷茫了。
希望我的文章不會誤導在觀看的你,如果有異議的地方歡迎討論和指正。
如果能給觀看的你帶來收獲,那就是最好不過了。