做android開發也有很長一段時間類,一直沒有仔細想過ScrollView是怎么實現的,如何實現滾動的,所以就去研究類一下其源碼,順便做一下筆記,望日后好查閱。俗話說好記性不如爛筆頭嘛。小弟不才,哪里理解錯了還望大神指教,再此先謝過。
理論上弄清楚源碼是怎么做的,我們按照這個邏輯也可以寫出一個的ScrollView的,所有我也寫了一個ScrollView,留作參考。這個ScrollView對于滑動到邊界的處理,只做了回彈的處理。所以支持邊界阻尼回彈的ScrollView。
原理請參考:實現一個ScrollView
項目地址:https://github.com/cyuanyang/ScrollView.git
FillViewport
眾所周知ScrollView有一個FillViewport屬性,而他的實現也很簡單,下面是源碼,注釋是依照我的理解自己加上去的。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//1.如果是false 按照父視圖的測量方式測量ScrollView的子View的寬高
// 即使你的子View設置math_parant 也只當者wrap_content處理
if (!mFillViewport) {
return;
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
return;
}
//2.如果設置mFillViewport=true 則會走這里開始測量子View的寬高
if (getChildCount() > 0) {
//3.因為ScrollView有且只有一個子View所以直接取第一個
final View child = getChildAt(0);
final int widthPadding;
final int heightPadding;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
//4.拿到布局參數
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
//5.計算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;
}
//6. desiredHeight 是scrollView的高度減去上下margin剩下的高度 如果child的高度小于這個才去測量
// 如果大于的話已經充滿里沒必要再折騰一次 源碼的水平還是很有質量的
final int desiredHeight = getMeasuredHeight() - heightPadding;
if (child.getMeasuredHeight() < desiredHeight) {
//7. 計算寬高 調用child的measure 完成
final int childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec, widthPadding, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
desiredHeight, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
這里細心的人可能會有疑問,scrollView是FrameLayout的子類,而mFillViewport=false,會調用 super.onMeasure()測量子View的寬高,這樣我們也會得到一個正確的值的。事實并不是這么簡單的,再FrameLayout的測量View的方法中,測量child是有一個額外條件
if (mMeasureAllChildren || child.getVisibility() != GONE)
mMeasureAllChildren再mFillViewport為false的時候就是false
onInterceptTouchEvent
這個方法對于ScrollView是很關鍵的。如果想要滑動,肯定得返回true的,但是又不能全部返回true要不子View就接受不到事件了。這個方法就是處理何時該攔截事件。還是拿關鍵的源碼說話。如果不懂mScroller或者VelocityTracker請參考實現一個ScrollView
case MotionEvent.ACTION_DOWN: {
// 1. 如果按下的位置在不在 子View上
final int y = (int) ev.getY();
if (!inChild((int) ev.getX(), (int) y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
/*
* 2. 記住down事件 取第一個手指
* Remember location of down touch.
* ACTION_DOWN always refers to pointer index 0.
*/
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
//3. 這個是計算速率的 主要用來計算手指離開后的fling的速率
mVelocityTracker.addMovement(ev);
/*
* If being flinged and user touches the screen, initiate drag;
* otherwise don't. mScroller.isFinished should be false when
* being flinged. We need to call computeScrollOffset() first so that
* isFinished() is correct.
*
*/
//4. 下面是如何區分是點擊子View還是拖動ScrollView 原因上面源碼注釋也很清楚
//如果mScroller再滾動 即認為是拖動 直接賦值true
mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();
if (mIsBeingDragged && mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
startNestedScroll(SCROLL_AXIS_VERTICAL);
break;
}
這個是move事件的處理
//6.如果先點擊沒有滑動,攔截事件中為false,ScrollView中的button也能接受到事件,這是再根據滑動的距離來決定是不是需要攔截事件
//mTouchSlop(這個值是一個系統值,判斷滑動的一個閾值)
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
mIsBeingDragged = true;
//7. 賦值down后的y的位置
mLastMotionY = y;
//8. 初始化速率軌跡計算 主要用來計算手指離開后的fling的速率
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
if (mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
onTouchEvent
這個是ScrollView最關鍵,最關鍵,最關鍵的地方,重要的話說三遍。理解這個地方后自己就可以寫出一個ScrollView了。還是拿代碼說話吧
//代碼不必要每一步都懂 只需要理解關鍵的地方即可,畢竟android是一個系統,考慮的很多很多,我們沒有必要理解每一句代碼的含義
//所以這里列舉一下關鍵的地方
public boolean onTouchEvent(MotionEvent ev) {
//1. 如果沒有初始化速率軌跡 初始化它,這個還是用于手指離開后計算fling的
initVelocityTrackerIfNotExists();
MotionEvent vtev = MotionEvent.obtain(ev);
final int actionMasked = ev.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
vtev.offsetLocation(0, mNestedYOffset);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
if (getChildCount() == 0) {
return false;
}
//2.請求父視圖不要攔截
if ((mIsBeingDragged = !mScroller.isFinished())) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
//3. 如果當前在fling 就是mScroller還沒有完成就觸摸了
//立刻放棄當前的滾動
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
if (mFlingStrictSpan != null) {
mFlingStrictSpan.finish();
mFlingStrictSpan = null;
}
}
// Remember where the motion event started
//4. 記住觸摸的位置 mLastMotionY 這個值在move的時候用來計算手指移動的變化量,然后用來計算需要滾動的距離
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
//5. 這個是處理內部滾動 可以先不用管這個
//涉及到Nested的都可以先不用管它 這個好像是為了支持v4包內的某個功能做的處理
startNestedScroll(SCROLL_AXIS_VERTICAL);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
final int y = (int) ev.getY(activePointerIndex);
//6. deltaY 計算手指移動的距離 在4中記錄的 同時下面還會更新這個值 8中會用到這個值來計算需要滾動的距離
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
//7. 如果先點擊沒有滑動,攔截事件中為false,ScrollView中的button也能接受到事件,這是再根據滑動的距離來決定是不是需要攔截事件
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) {
// Scroll to follow the motion event
//更新mLastMotionY 這個很關鍵 否則根本滑不懂
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);
// Calling overScrollBy will call onOverScrolled, which
// calls onScrollChanged if applicable.
//8. 調用overScrollBy方法計算滾動 這個方法就是計算一下滾動的距離然后回調給onOverScrolled()在這里調用scrollTo方法
// 到這里的時候 ScrollView還不會滾動,滾動的代碼在onOverScrolled()中,緊接著下面會出現
// 這里返回true表示滑動超出了內容區域 像滑倒頂部會有阻尼的那種效果就可以用這個實現
// 這個是最關鍵的地方 關鍵的源碼都有注釋 厲害了word
if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
&& !hasNestedScrollingParent()) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
//9.下面就沒必要仔細去研究了 這里處理一下滑到邊界出的效果
final int scrolledDeltaY = mScrollY - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
} else if (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;
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
//10. 速率軌跡終于要大顯神威了
// up后 8中的計算滾動就會停止,但是實際上ScrollView還會滾動一段距離
// 這里根據 VelocityTracker 得到手指離開這一瞬間的Velocity
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
//11. 速錄很大 則會認為是一個fling 動作
// flingWithNestedDispatch()方法內部就是執行了mScroller.fling()方法
//else if 含義:速錄很小,例如我們滑動最后停下來,然后手指離開屏幕,這時的速率可能為0,就不需要fling
//但是若滑動到頂部就需要回彈動畫 ,直接動用 mScroller.springBack()即可
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
flingWithNestedDispatch(-initialVelocity);
} else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
getScrollRange())) {
postInvalidateOnAnimation();
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
//12. 取消事件的處理 類似于up事件 理解上面的下面的多個觸摸點的處理就很簡單了
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
case MotionEvent.ACTION_POINTER_DOWN: {
final int index = ev.getActionIndex();
mLastMotionY = (int) ev.getY(index);
mActivePointerId = ev.getPointerId(index);
break;
}
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
break;
}
剛剛在8中調用overScrollBy用來計算滾動的距離然后回調給onOverScrolled來處理是否需要滾動,這里就是處理邏輯
@Override
protected void onOverScrolled(int scrollX, int scrollY,
boolean clampedX, boolean clampedY) {
// Treat animating scrolls differently; see #computeScroll() for why.
//這個if是用來區別mScroller滾動調用的還是手指拖動滾動的
//mScroller.isFinished()為true 就是手指拖動引起的滾動 直接調用super.scrollTo,這樣就完成了滾動 完美
//if代碼塊其實就是一個和scrollTo的代碼差不多,這里并沒有直接調用我也不知道為什么,看注解也沒太明白,哪位大神知道麻煩告訴我一下,謝謝。
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);
}
//顯示滾動條 滾動條是View的方法,其實每個View都有滾動的功能的。
awakenScrollBars();
}
到此,ScrollView就能滾動了。
總結
瀏覽源碼不是為了去寫一個ScrollView,而是在看完之后我們學到了啥。就像小時候學校組織看電影一樣,學校單純的只想讓你看完電影就算了,一般都會讓我們寫一篇讀后感。haha。。。
OverScroller
如果你要是想做一個滾動的View,這個一定能幫助你實現夢想。 自帶強大的滾動技能。一般配合VelocityTracker來計算fling滾動。
如何優雅的區分是點擊還是滑動操作
當我們做一個滑動的容器組件的時候,當我們快速的滑動的時候,并不想讓down事件傳遞下去,但同時又不影響點擊容器內的View。我們可以這么做。這是在onInterceptTouchEvent中哦!
case MotionEvent.ACTION_DOWN:
.....
mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();
......
break;
return mIsBeingDragged;
可能會坑猿的地方
ScrollView會自動滾動到獲取焦點的View上面。例如我們在ScrollView中放一個WebView,就會發現總是會滾動到WebView那里。筆者有一次用WebView來加載MathJax來渲染數學符號的時候就遇到這個坑。解決辦法有很多。主要思路就是移除不必要的焦點。
scrollBy參數是Int 會丟失小數部分