做android開發(fā)也有很長(zhǎng)一段時(shí)間類批什,一直沒有仔細(xì)想過ScrollView是怎么實(shí)現(xiàn)的哆窿,如何實(shí)現(xiàn)滾動(dòng)的链烈,所以就去研究類一下其源碼,順便做一下筆記挚躯,望日后好查閱强衡。俗話說好記性不如爛筆頭嘛。小弟不才码荔,哪里理解錯(cuò)了還望大神指教漩勤,再此先謝過。
理論上弄清楚源碼是怎么做的缩搅,我們按照這個(gè)邏輯也可以寫出一個(gè)的ScrollView的越败,所有我也寫了一個(gè)ScrollView,留作參考硼瓣。這個(gè)ScrollView對(duì)于滑動(dòng)到邊界的處理究飞,只做了回彈的處理。所以支持邊界阻尼回彈的ScrollView堂鲤。
原理請(qǐng)參考:實(shí)現(xiàn)一個(gè)ScrollView
項(xiàng)目地址:https://github.com/cyuanyang/ScrollView.git
FillViewport
眾所周知ScrollView有一個(gè)FillViewport屬性亿傅,而他的實(shí)現(xiàn)也很簡(jiǎn)單,下面是源碼瘟栖,注釋是依照我的理解自己加上去的葵擎。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//1.如果是false 按照父視圖的測(cè)量方式測(cè)量ScrollView的子View的寬高
// 即使你的子View設(shè)置math_parant 也只當(dāng)者wrap_content處理
if (!mFillViewport) {
return;
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
return;
}
//2.如果設(shè)置mFillViewport=true 則會(huì)走這里開始測(cè)量子View的寬高
if (getChildCount() > 0) {
//3.因?yàn)镾crollView有且只有一個(gè)子View所以直接取第一個(gè)
final View child = getChildAt(0);
final int widthPadding;
final int heightPadding;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
//4.拿到布局參數(shù)
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
//5.計(jì)算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的高度小于這個(gè)才去測(cè)量
// 如果大于的話已經(jīng)充滿里沒必要再折騰一次 源碼的水平還是很有質(zhì)量的
final int desiredHeight = getMeasuredHeight() - heightPadding;
if (child.getMeasuredHeight() < desiredHeight) {
//7. 計(jì)算寬高 調(diào)用child的measure 完成
final int childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec, widthPadding, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
desiredHeight, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
這里細(xì)心的人可能會(huì)有疑問,scrollView是FrameLayout的子類半哟,而mFillViewport=false酬滤,會(huì)調(diào)用 super.onMeasure()測(cè)量子View的寬高,這樣我們也會(huì)得到一個(gè)正確的值的镜沽。事實(shí)并不是這么簡(jiǎn)單的敏晤,再FrameLayout的測(cè)量View的方法中,測(cè)量child是有一個(gè)額外條件
if (mMeasureAllChildren || child.getVisibility() != GONE)
mMeasureAllChildren再mFillViewport為false的時(shí)候就是false
onInterceptTouchEvent
這個(gè)方法對(duì)于ScrollView是很關(guān)鍵的缅茉。如果想要滑動(dòng)嘴脾,肯定得返回true的,但是又不能全部返回true要不子View就接受不到事件了。這個(gè)方法就是處理何時(shí)該攔截事件译打。還是拿關(guān)鍵的源碼說話耗拓。如果不懂mScroller或者VelocityTracker請(qǐng)參考實(shí)現(xiàn)一個(gè)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事件 取第一個(gè)手指
* Remember location of down touch.
* ACTION_DOWN always refers to pointer index 0.
*/
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
//3. 這個(gè)是計(jì)算速率的 主要用來計(jì)算手指離開后的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. 下面是如何區(qū)分是點(diǎn)擊子View還是拖動(dòng)ScrollView 原因上面源碼注釋也很清楚
//如果mScroller再滾動(dòng) 即認(rèn)為是拖動(dòng) 直接賦值true
mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();
if (mIsBeingDragged && mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
startNestedScroll(SCROLL_AXIS_VERTICAL);
break;
}
這個(gè)是move事件的處理
//6.如果先點(diǎn)擊沒有滑動(dòng),攔截事件中為false奏司,ScrollView中的button也能接受到事件乔询,這是再根據(jù)滑動(dòng)的距離來決定是不是需要攔截事件
//mTouchSlop(這個(gè)值是一個(gè)系統(tǒng)值,判斷滑動(dòng)的一個(gè)閾值)
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. 初始化速率軌跡計(jì)算 主要用來計(jì)算手指離開后的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
這個(gè)是ScrollView最關(guān)鍵韵洋,最關(guān)鍵竿刁,最關(guān)鍵的地方,重要的話說三遍搪缨。理解這個(gè)地方后自己就可以寫出一個(gè)ScrollView了食拜。還是拿代碼說話吧
//代碼不必要每一步都懂 只需要理解關(guān)鍵的地方即可,畢竟android是一個(gè)系統(tǒng)副编,考慮的很多很多负甸,我們沒有必要理解每一句代碼的含義
//所以這里列舉一下關(guān)鍵的地方
public boolean onTouchEvent(MotionEvent ev) {
//1. 如果沒有初始化速率軌跡 初始化它,這個(gè)還是用于手指離開后計(jì)算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.請(qǐng)求父視圖不要攔截
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. 如果當(dāng)前在fling 就是mScroller還沒有完成就觸摸了
//立刻放棄當(dāng)前的滾動(dòng)
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
if (mFlingStrictSpan != null) {
mFlingStrictSpan.finish();
mFlingStrictSpan = null;
}
}
// Remember where the motion event started
//4. 記住觸摸的位置 mLastMotionY 這個(gè)值在move的時(shí)候用來計(jì)算手指移動(dòng)的變化量痹届,然后用來計(jì)算需要滾動(dòng)的距離
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
//5. 這個(gè)是處理內(nèi)部滾動(dòng) 可以先不用管這個(gè)
//涉及到Nested的都可以先不用管它 這個(gè)好像是為了支持v4包內(nèi)的某個(gè)功能做的處理
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 計(jì)算手指移動(dòng)的距離 在4中記錄的 同時(shí)下面還會(huì)更新這個(gè)值 8中會(huì)用到這個(gè)值來計(jì)算需要滾動(dòng)的距離
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
//7. 如果先點(diǎn)擊沒有滑動(dòng)呻待,攔截事件中為false,ScrollView中的button也能接受到事件队腐,這是再根據(jù)滑動(dòng)的距離來決定是不是需要攔截事件
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 這個(gè)很關(guān)鍵 否則根本滑不懂
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. 調(diào)用overScrollBy方法計(jì)算滾動(dòng) 這個(gè)方法就是計(jì)算一下滾動(dòng)的距離然后回調(diào)給onOverScrolled()在這里調(diào)用scrollTo方法
// 到這里的時(shí)候 ScrollView還不會(huì)滾動(dòng)蚕捉,滾動(dòng)的代碼在onOverScrolled()中,緊接著下面會(huì)出現(xiàn)
// 這里返回true表示滑動(dòng)超出了內(nèi)容區(qū)域 像滑倒頂部會(huì)有阻尼的那種效果就可以用這個(gè)實(shí)現(xiàn)
// 這個(gè)是最關(guān)鍵的地方 關(guān)鍵的源碼都有注釋 厲害了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.下面就沒必要仔細(xì)去研究了 這里處理一下滑到邊界出的效果
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中的計(jì)算滾動(dòng)就會(huì)停止柴淘,但是實(shí)際上ScrollView還會(huì)滾動(dòng)一段距離
// 這里根據(jù) VelocityTracker 得到手指離開這一瞬間的Velocity
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
//11. 速錄很大 則會(huì)認(rèn)為是一個(gè)fling 動(dòng)作
// flingWithNestedDispatch()方法內(nèi)部就是執(zhí)行了mScroller.fling()方法
//else if 含義:速錄很小鱼冀,例如我們滑動(dòng)最后停下來,然后手指離開屏幕悠就,這時(shí)的速率可能為0,就不需要fling
//但是若滑動(dòng)到頂部就需要回彈動(dòng)畫 充易,直接動(dòng)用 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事件 理解上面的下面的多個(gè)觸摸點(diǎn)的處理就很簡(jiǎn)單了
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中調(diào)用overScrollBy用來計(jì)算滾動(dòng)的距離然后回調(diào)給onOverScrolled來處理是否需要滾動(dòng)梗脾,這里就是處理邏輯
@Override
protected void onOverScrolled(int scrollX, int scrollY,
boolean clampedX, boolean clampedY) {
// Treat animating scrolls differently; see #computeScroll() for why.
//這個(gè)if是用來區(qū)別mScroller滾動(dòng)調(diào)用的還是手指拖動(dòng)滾動(dòng)的
//mScroller.isFinished()為true 就是手指拖動(dòng)引起的滾動(dòng) 直接調(diào)用super.scrollTo,這樣就完成了滾動(dòng) 完美
//if代碼塊其實(shí)就是一個(gè)和scrollTo的代碼差不多盹靴,這里并沒有直接調(diào)用我也不知道為什么炸茧,看注解也沒太明白,哪位大神知道麻煩告訴我一下稿静,謝謝梭冠。
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);
}
//顯示滾動(dòng)條 滾動(dòng)條是View的方法,其實(shí)每個(gè)View都有滾動(dòng)的功能的改备。
awakenScrollBars();
}
到此控漠,ScrollView就能滾動(dòng)了。
總結(jié)
瀏覽源碼不是為了去寫一個(gè)ScrollView,而是在看完之后我們學(xué)到了啥盐捷。就像小時(shí)候?qū)W校組織看電影一樣偶翅,學(xué)校單純的只想讓你看完電影就算了,一般都會(huì)讓我們寫一篇讀后感碉渡。haha聚谁。。滞诺。
OverScroller
如果你要是想做一個(gè)滾動(dòng)的View形导,這個(gè)一定能幫助你實(shí)現(xiàn)夢(mèng)想。 自帶強(qiáng)大的滾動(dòng)技能习霹。一般配合VelocityTracker來計(jì)算fling滾動(dòng)朵耕。
如何優(yōu)雅的區(qū)分是點(diǎn)擊還是滑動(dòng)操作
當(dāng)我們做一個(gè)滑動(dòng)的容器組件的時(shí)候,當(dāng)我們快速的滑動(dòng)的時(shí)候序愚,并不想讓down事件傳遞下去憔披,但同時(shí)又不影響點(diǎn)擊容器內(nèi)的View。我們可以這么做爸吮。這是在onInterceptTouchEvent中哦芬膝!
case MotionEvent.ACTION_DOWN:
.....
mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();
......
break;
return mIsBeingDragged;
可能會(huì)坑猿的地方
ScrollView會(huì)自動(dòng)滾動(dòng)到獲取焦點(diǎn)的View上面。例如我們?cè)赟crollView中放一個(gè)WebView形娇,就會(huì)發(fā)現(xiàn)總是會(huì)滾動(dòng)到WebView那里锰霜。筆者有一次用WebView來加載MathJax來渲染數(shù)學(xué)符號(hào)的時(shí)候就遇到這個(gè)坑。解決辦法有很多桐早。主要思路就是移除不必要的焦點(diǎn)癣缅。
scrollBy參數(shù)是Int 會(huì)丟失小數(shù)部分