本文分析版本: Android API 23
1.簡介
ScrollView
是我們在開發中經常使用的控件。當我們需要展示的內容比較多但并不是重復的item
時,我們就會使用ScrollView
使內容可以在垂直方向滾動顯示防止顯示不全。ScrollView
使用起來非常簡單,大多數情況下你甚至都不用寫一行Java
代碼就能使用ScrollView
了。但是要注意的是ScrollView
中只能添加一個子View
。今天我們就來看看ScrollView
到底是如何實現的。以及最后會教大家一行代碼實現類似IOS
上的彈性ScrollView
。_
2.源碼分析
2.1 繼承關系
2.2 主要輔助類
//用來計算滑動位置
private OverScroller mScroller;
//用來繪制邊緣陰影
private EdgeEffect mEdgeGlowTop;
private EdgeEffect mEdgeGlowBottom;
//用于計算滑動時的加速度
private VelocityTracker mVelocityTracker;
2.3 構造方法
ScrollView
的構造方法如下:
public ScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initScrollView();
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.ScrollView, defStyleAttr, defStyleRes);
setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));
a.recycle();
}
在構造方法中分別調用了initScrollView()
與setFillViewport()
方法,代碼如下:
private void initScrollView() {
//初始化OverScroller
mScroller = new OverScroller(getContext());
setFocusable(true);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
setWillNotDraw(false);
final ViewConfiguration configuration = ViewConfiguration.get(mContext);
//被認為是滑動操作的最小距離
mTouchSlop = configuration.getScaledTouchSlop();
//最小加速度
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
//最大加速度
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
//用手指拖動超過邊緣的最大距離
mOverscrollDistance = configuration.getScaledOverscrollDistance();
//滑動超過邊緣的最大距離
mOverflingDistance = configuration.getScaledOverflingDistance();
}
可以看到是初始化了一些類與參數,繼續看看setFillViewport()
:
public void setFillViewport(boolean fillViewport) {
if (fillViewport != mFillViewport) {
mFillViewport = fillViewport;
requestLayout();
}
}
只是根據布局文件中的fillViewport
屬性來給mFillViewport
賦值并調用requestLayout()
方法。mFillViewport
如果為true
則表示:將子View
的高度延伸到和視圖高度一致,即充滿整個視圖。初始化結束之后,會進入到繪制流程。下面我們按照Measure
-> Layout
-> Draw
的繪制流程來分析ScrollView
中的實現。
2.4 Measure、Layout與Draw
2.4.1 onMeasure方法的實現
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!mFillViewport) {
return;
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
return;
}
if (getChildCount() > 0) {
// 獲取子View
final View child = getChildAt(0);
// 獲取ScrollView的高度
final int height = getMeasuredHeight();
if (child.getMeasuredHeight() < height) {
final int widthPadding;
final int heightPadding;
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
// 獲取ScrollView的padding
if (targetSdkVersion >= VERSION_CODES.M) {
widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
} else {
widthPadding = mPaddingLeft + mPaddingRight;
heightPadding = mPaddingTop + mPaddingBottom;
}
final int childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec, widthPadding, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
height - heightPadding, MeasureSpec.EXACTLY);
//根據新的高度重新measure子View
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
從代碼中可以看到首先調用了super.onMeasure(widthMeasureSpec, heightMeasureSpec);
即父類FrameLayout
的onMeasure()
方法。如果我們將mFillViewport
設置為false
的話將會直接return
。當為true
時才會繼續執行,會根據子View
的高度和ScrollView
本身的高度決定是否重新measure
子View
使其充滿ScrollView
。ScrollView
的onMeasure()
其實就是處理了mFillViewport
。
2.4.1 onLayout方法的實現
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mIsLayoutDirty = false;
// Give a child focus if it needs it
if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
scrollToChild(mChildToScrollTo);
}
mChildToScrollTo = null;
//是否還未添加過window中去
if (!isLaidOut()) {
if (mSavedState != null) {
mScrollY = mSavedState.scrollPosition;
mSavedState = null;
} // mScrollY default value is "0"
final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0;
final int scrollRange = Math.max(0,
childHeight - (b - t - mPaddingBottom - mPaddingTop));
// Don't forget to clamp
if (mScrollY > scrollRange) {
mScrollY = scrollRange;
} else if (mScrollY < 0) {
mScrollY = 0;
}
}
// Calling this with the present values causes it to re-claim them
scrollTo(mScrollX, mScrollY);
}
首先也是調用了父類的onLayout
方法。接下來處理了是否有需要滾動到的View
,以及根據保存的滾動狀態來決定是否需要滾動。如果需要則調用scrollTo()
方法。
2.4.1 draw方法的實現
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (mEdgeGlowTop != null) {
final int scrollY = mScrollY;
final boolean clipToPadding = getClipToPadding();
if (!mEdgeGlowTop.isFinished()) {
......
if (mEdgeGlowTop.draw(canvas)) {
postInvalidateOnAnimation();
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeGlowBottom.isFinished()) {
......
if (mEdgeGlowBottom.draw(canvas)) {
postInvalidateOnAnimation();
}
canvas.restoreToCount(restoreCount);
}
}
}
依然是調用了父類的draw
方法。之后則是根據是否需要繪制邊緣陰影來繪制陰影。ScrollView
的邊緣陰影就是在這里繪制的。值得一提的是包括ListView
以及RecycleView
的邊緣陰影都是用這種方法來繪制的。以上就是ScrollView
的整個繪制流程。可以看出都是調用了父類的對應方法。自身只處理了一些與ScrollView
相關的屬性。分析完繪制流程我們就來看看ScrollView
中的觸摸事件處理機制,來看看ScrollView
中的滑動滾動到底是如何做到的:
2.5 觸摸事件處理
說到觸摸事件的分發與消費機制這算是一個比較基礎的知識。但是要是完全掌握也并不是那么容易的,這里推薦一篇文章Android:View的事件分發與消費機制。對事件處理機制還不了解的同學可以先看看這邊文章。ScrollView
因為是繼承自ViewGroup
的,所以觸摸事件會依次調用dispatchTouchEvent()
-> onInterceptTouchEvent()
若返回true
-> onTouchEvent()
處理觸摸事件。ScrollView
并沒有重寫dispatchTouchEvent()
方法,所以我們從onInterceptTouchEvent()
方法來看。
2.5.1 onInterceptTouchEvent方法的實現
//這個方法只決定我們是否攔截這個手勢,如果返回true,則onMotionEvent會被調用,并處理滑動事件。
//此方法并不處理事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//如果是移動手勢并在處于拖拽階段,直接返回true
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
//如果并不能滑動則返回false
if (getScrollY() == 0 && !canScrollVertically(1)) {
return false;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
//檢測用戶是否移動了足夠遠的距離。
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);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
}
//得到當前觸摸的y左邊
final int y = (int) ev.getY(pointerIndex);
//計算移動的插值
final int yDiff = Math.abs(y - mLastMotionY);
//如果yDiff大于最小滑動距離,并且是垂直滑動則認為觸發了滑動手勢。
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
//標記拖動狀態為true
mIsBeingDragged = true;
//賦值mLastMotionY
mLastMotionY = y;
//初始化mVelocityTracker并添加
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
if (mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
final ViewParent parent = getParent();
if (parent != null) {
//通知父布局不再攔截觸摸事件
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
//觸摸點不在子View內
if (!inChild((int) ev.getX(), (int) y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
//記錄當前位置
mLastMotionY = y;
//記錄pointer的ID,ACTION_DOWN總會在index 0
mActivePointerId = ev.getPointerId(0);
//初始化mVelocityTracker
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
//如果在滑動過程中則mIsBeingDragged = true
mIsBeingDragged = !mScroller.isFinished();
if (mIsBeingDragged && mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
//回調NestedScroll相關接口
startNestedScroll(SCROLL_AXIS_VERTICAL);
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
//清除Drag狀態
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
//回調NestedScroll相關接口
stopNestedScroll();
break;
case MotionEvent.ACTION_POINTER_UP:
//當多個手指觸摸中有一個手指抬起時,判斷是不是當前active的點,如果是則尋找新的
//mActivePointerId
onSecondaryPointerUp(ev);
break;
}
//最終根據是否開始拖拽的狀態返回
return mIsBeingDragged;
}
以上就是onInterceptTouchEvent()
的整體實現。onInterceptTouchEvent()
只決定是否攔截觸摸事件并交給onTouchEvent()
處理。內部并不處理觸摸邏輯。ScrollView
中根據mIsBeingDragged
來決定是否攔截事件。當手指按下發生MotionEvent.ACTION_DOWN
時,會記錄當前位置并檢測是否在快速滾動過程中如果是則返回true
。當手指移動發生MotionEvent.ACTION_MOVE
時,會判斷是否是垂直方向上的滑動事件,如果是則返回true
。當手指抬起發生MotionEvent.ACTION_UP
時,則清除狀態并返回false
。在返回true
的情況中,onTouchEvent()
方法就會被調用來處理觸摸事件。我們繼續來看onTouchEvent()
方法的實現。
2.5.2 onTouchEvent方法的實現
在看onTouchEvent()
的實現之前,我們知道在ScrollView
中手指無論怎么移動,只會有垂直方向上的滑動發生。而觸摸事件的大致流程是:
ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP
我們根據事件的類型分別來分析:
- ACTION_DOWN:
ACTION_DOWN
代表手指按下時第一個發生的事件,在onTouchEvent()
中實現如下:
@Override
public boolean onTouchEvent(MotionEvent ev) {
//初始化VelocityTracker
initVelocityTrackerIfNotExists();
//復制當前的MotionEvent賦值給vtev
MotionEvent vtev = MotionEvent.obtain(ev);
final int actionMasked = ev.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
//調整vtev的偏移量
vtev.offsetLocation(0, mNestedYOffset);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
if (getChildCount() == 0) {
return false;
}
// 將!mScroller.isFinished()賦值給mIsBeingDragged
if ((mIsBeingDragged = !mScroller.isFinished())) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
//如果正在fling狀態并且用戶觸摸。則停止fling。
//當處于fling過程中isFinished為false。
//fling :即快速滑動。
if (!mScroller.isFinished()) {
//停止
mScroller.abortAnimation();
if (mFlingStrictSpan != null) {
mFlingStrictSpan.finish();
mFlingStrictSpan = null;
}
}
//記錄觸摸事件的初始值
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
startNestedScroll(SCROLL_AXIS_VERTICAL);
break;
}
......
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
在處理各種事件之前,首先初始化了VelocityTracker
。并且復制一個新的MotionEvent
對象用于計算加速度。接著開始處理ACTION_DOWN
:首先是給mIsBeingDragged
賦值,接著檢查是否在fling
動畫執行過程中,如果正在執行則停止,這也是為什么我們在ScrollView
滑動過程中手指觸摸時會終止ScrollView
的滑動。最后記錄了mLastMotionY
與mActivePointerId
。
- ACTION_MOVE:
當手指移動時,會產生ACTION_MOVE
事件:
@Override
public boolean onTouchEvent(MotionEvent ev) {
......
switch (actionMasked) {
......
//如果為ACTION_MOVE事件時
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
//得到當前y值
final int y = (int) ev.getY(activePointerIndex);
//計算偏移量deltaY
int deltaY = mLastMotionY - y;
//如果dispatchNestedPreScroll返回true,即有NestedScroll存在
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
//如果還未處于drag狀態,并且deltaY大于最小滑動距離,
//則賦值mIsBeingDragged為true
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
//如果在拖拽狀態
if (mIsBeingDragged) {
//記錄當前的y值
mLastMotionY = y - mScrollOffset[1];
final int oldY = mScrollY;
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
// 調用overScrollBy()方法處理滑動事件。
if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
&& !hasNestedScrollingParent()) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
final int scrolledDeltaY = mScrollY - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
//處理NestedScroll
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
} else if (canOverscroll) {
//如果canOverscroll,即可以越過邊緣滑動。
final int pulledToY = oldY + deltaY;
//初始化邊緣陰影
if (pulledToY < 0) {
mEdgeGlowTop.onPull((float) deltaY / getHeight(),
ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {
mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
1.f - ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if (mEdgeGlowTop != null
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
postInvalidateOnAnimation();
}
}
}
break;
}
......
}
ACTION_MOVE
事件中,首先計算當前的垂直偏移量deltaY
。然后判斷是否大于最小滑動距離,并且給mIsBeingDragged
賦值。接著如果mIsBeingDragged
為true
。就取得處理滑動需要的各種參數,并調用overScrollBy()
方法來處理觸摸事件,overScrollBy()
是在View
里實現的方法,大致實現如下:
protected boolean overScrollBy(int deltaX, int deltaY,
int scrollX, int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY,
boolean isTouchEvent) {
.....
int newScrollX = scrollX + deltaX;
if (!overScrollHorizontal) {
maxOverScrollX = 0;
}
int newScrollY = scrollY + deltaY;
if (!overScrollVertical) {
maxOverScrollY = 0;
}
// Clamp values if at the limits and record
final int left = -maxOverScrollX;
final int right = maxOverScrollX + scrollRangeX;
final int top = -maxOverScrollY;
final int bottom = maxOverScrollY + scrollRangeY;
boolean clampedX = false;
if (newScrollX > right) {
newScrollX = right;
clampedX = true;
} else if (newScrollX < left) {
newScrollX = left;
clampedX = true;
}
boolean clampedY = false;
if (newScrollY > bottom) {
newScrollY = bottom;
clampedY = true;
} else if (newScrollY < top) {
newScrollY = top;
clampedY = true;
}
onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);
return clampedX || clampedY;
}
方法的參數很清楚了應該不難理解,可以看到在overScrollBy()
方法中根據我們傳入的參數以及View
本身是否可以滑動的設定,等等來最終決定了新的newScrollX
與newScrollY
。接著調用了onOverScrolled()
方法來處理滑動,onOverScrolled()
方法在View
中是空實現,所以再回到ScrollView
中可以看到重寫了onOverScrolled()
方法:
@Override
protected void onOverScrolled(int scrollX, int scrollY,
boolean clampedX, boolean clampedY) {
// Treat animating scrolls differently; see #computeScroll() for why.
if (!mScroller.isFinished()) {
final int oldX = mScrollX;
final int oldY = mScrollY;
mScrollX = scrollX;
mScrollY = scrollY;
invalidateParentIfNeeded();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (clampedY) {
mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());
}
} else {
super.scrollTo(scrollX, scrollY);
}
awakenScrollBars();
}
首先是有一句注釋是說:不同的對待滑動動畫,查看computeScroll()
方法看原因。說到滑動動畫一定是和Scroller
相關了,目前我們還沒涉及到,下面我們再談。回到這里看到根據!mScroller.isFinished()
來判斷,根據前面的判斷得知,要么是滑動動畫并不存在,要么就已經被終止,所以在這里!mScroller.isFinished()
為false
。所以會調用super.scrollTo(scrollX, scrollY);
最終產生滑動。到這里手指觸摸產生的滑動就分析完了。
- ACTION_UP:
ACTION_UP
是當我們手指離開時產生的事件,在ScrollView
中當我們手指離開時,會根據當前的加速度再滑動一段距離。具體的實現我們來看看是如何實現的:
@Override
public boolean onTouchEvent(MotionEvent ev) {
......
switch (actionMasked) {
......
case MotionEvent.ACTION_UP:
//如果實在drag狀態中
if (mIsBeingDragged) {
//計算加速度
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
//如果有有效的加速度
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
//處理帶有加速度的滑動事件
flingWithNestedDispatch(-initialVelocity);
} else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
getScrollRange())) {
postInvalidateOnAnimation();
}
mActivePointerId = INVALID_POINTER;
//清除drag狀態
endDrag();
}
break;
}
......
}
可以看到代碼并不復雜,在計算了加速度后,調用了flingWithNestedDispatch(-initialVelocity);
:
private void flingWithNestedDispatch(int velocityY) {
final boolean canFling = (mScrollY > 0 || velocityY > 0) &&
(mScrollY < getScrollRange() || velocityY < 0);
if (!dispatchNestedPreFling(0, velocityY)) {
dispatchNestedFling(0, velocityY, canFling);
if (canFling) {
fling(velocityY);
}
}
}
代碼如上,我們這里不考慮NestedFling
的方式,所以dispatchNestedPreFling(0, velocityY)
默認會返回false
,所以最終會執行fling(velocityY);
:
public void fling(int velocityY) {
if (getChildCount() > 0) {
int height = getHeight() - mPaddingBottom - mPaddingTop;
int bottom = getChildAt(0).getHeight();
mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
Math.max(0, bottom - height), 0, height/2);
if (mFlingStrictSpan == null) {
mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");
}
postInvalidateOnAnimation();
}
}
可以看到是調用了mScroller
的fling
方法,在上一篇Scroller源碼分析中,我們已經詳細解釋了Scroller
的原理,ScrollView
中雖然使用的是OverScroller
但是使用方法也是類似的。所以在調用了mScroller
的fling
方法后。我們需要在computeScroll()
處理mScroller
計算出的值。ScrollView
中的computeScroll()
方法實現如下:
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
int oldX = mScrollX;
int oldY = mScrollY;
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
if (oldX != x || oldY != y) {
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
0, mOverflingDistance, false);
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (canOverscroll) {
if (y < 0 && oldY >= 0) {
mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
} else if (y > range && oldY <= range) {
mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
}
}
}
if (!awakenScrollBars()) {
// Keep on drawing until the animation has finished.
postInvalidateOnAnimation();
}
} else {
if (mFlingStrictSpan != null) {
mFlingStrictSpan.finish();
mFlingStrictSpan = null;
}
}
}
我省略了一些注釋,意思是說:computeScroll()
會在繪制的過程中調用,為了不重復的顯示滾動條。這里重復做了scrollTo()
方法中的代碼。但并沒有調用scrollTo()
,因為scrollTo()
中也有滾動條相關的處理。所以computeScroll()
中也調用了overScrollBy()
方法處理滑動。所以最終仍然會調用onOverScrolled()
方法:
@Override
protected void onOverScrolled(int scrollX, int scrollY,
boolean clampedX, boolean clampedY) {
// Treat animating scrolls differently; see #computeScroll() for why.
if (!mScroller.isFinished()) {
final int oldX = mScrollX;
final int oldY = mScrollY;
mScrollX = scrollX;
mScrollY = scrollY;
invalidateParentIfNeeded();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (clampedY) {
mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());
}
} else {
super.scrollTo(scrollX, scrollY);
}
awakenScrollBars();
}
這次就會進入到第一個if
語句里,可以看到是給mScrollX
與mScrollY
賦值后調用了invalidateParentIfNeeded();
方法來完成最終的滑動處理。
- ACTION_POINTER_DOWN:
@Override
public boolean onTouchEvent(MotionEvent ev) {
......
switch (actionMasked) {
......
case MotionEvent.ACTION_POINTER_DOWN: {
//更新狀態,即新的觸摸手勢決定是否滑動。
final int index = ev.getActionIndex();
mLastMotionY = (int) ev.getY(index);
mActivePointerId = ev.getPointerId(index);
break;
}
}
......
}
ACTION_POINTER_DOWN
是指有另外一個手指發生了觸摸。這里的處理是將mActivePointerId
賦值給新的點了。所以在ScrollView
中當有一個手指按下,我們再按下另一個手指時,第二個按下的手指能決定ScrollView
的滑動。
- ACTION_POINTER_UP:
@Override
public boolean onTouchEvent(MotionEvent ev) {
......
switch (actionMasked) {
......
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
break;
}
......
}
ACTION_POINTER_UP
是指多個手指中的一個手指離開屏幕。所以這里會檢測是否是當前active
的手指離開了,并做相應的處理,具體邏輯在onSecondaryPointerUp(ev);
方法中,我們就不多解釋了。至此整個ScrollView
我們應該有了一個清晰完整的理解了。最后再分享一個小trick
。一行代碼實現仿ios
的彈性ScrollView
。
3.一行代碼實現彈性ScrollView
我們都知道ios
上的彈性滑動做的相當順滑。我們Android
系統一直都沒有。看完ScrollView
的代碼發現,其實通過變量mOverflingDistance
就能決定彈性滑動的最大值。但是ScrollView
并沒有暴露出方法給我們設置。但是我們只需要通過反射來設定mOverflingDistance
的值即可。。JOOR對反射做了封裝,可以使我們非常簡潔的來寫反射,所以這里我們只需要一行代碼即可:
Reflect.on(scrollView).set("mOverflingDistance", 100);
這樣就實現了彈性ScrollView
。以上demo
的代碼在SkyScrollViewDemo。
我每周會寫一篇源代碼分析的文章,以后也可能會有其他主題.
如果你喜歡我寫的文章的話,歡迎關注我的新浪微博@達達達達sky
地址: http://weibo.com/u/2030683111
每周我會第一時間在微博分享我寫的文章,也會積極轉發更多有用的知識給大家.