OverView
- A view group that allows the view hierarchy placed within it to be scrolled.
- Scroll view may have only one direct child placed within it.
- To add multiple views within the scroll view, make the direct child you add a view group, for example {@link LinearLayout}, and place additional views within that LinearLayout.
從上面官網(wǎng)的描述中可以看到,ScrollView是一個只能有1個直接子View的可滑動的ViewGroup
ScrollView只能縱向滑動被辑,如果需要橫向滑動則使用HorizontalScrollView,兩者實現(xiàn)原理一致
其繼承層次為:
public class ScrollView extends FrameLayout
可以看到ScrollView繼承自FrameLayout
構造函數(shù)
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();
if (context.getResources().getConfiguration().uiMode == Configuration.UI_MODE_TYPE_WATCH) {
setRevealOnFocusHint(false);
}
}
構造函數(shù)中只對1個屬性:fillViewport進行了提取淑玫,這個屬性的作用是屑埋,如果被設置成true,ScrollView的子View將充滿視圖
繪制相關
- 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) {
final View child = getChildAt(0);
final int widthPadding;
final int heightPadding;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
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 desiredHeight = getMeasuredHeight() - heightPadding;
if (child.getMeasuredHeight() < desiredHeight) {
final int childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec, widthPadding, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
desiredHeight, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
ScrollView的onMeasure()函數(shù)贞盯,先調用了父類的super.onMeasure()镀赌,隨后如果mFillViewport屬性為false氯哮,實際上函數(shù)就結束,也就是說在mFillViewport為false的情況下商佛,ScrollView在測量截斷是沒有額外工作的蛙粘;如果mFillViewport為true,就會調用一次child的measure()方法來重新測量大小以達到充滿視圖的效果
- 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;
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);
}
ScrollView的onLayout()過程也是先調用了父類的onLayout()威彰,隨后會判斷是否需要滑動到子View的位置;最后一行scrollTo(mScrollX, mScrollY)將滑動到對應的位置穴肘。中間的isLaidOut()是View里面的方法歇盼,用于判斷該View是否已經(jīng)被添加到window上。
- draw()
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (mEdgeGlowTop != null) {
final int scrollY = mScrollY;
final boolean clipToPadding = getClipToPadding();
if (!mEdgeGlowTop.isFinished()) {
final int restoreCount = canvas.save();
final int width;
final int height;
final float translateX;
final float translateY;
if (clipToPadding) {
width = getWidth() - mPaddingLeft - mPaddingRight;
height = getHeight() - mPaddingTop - mPaddingBottom;
translateX = mPaddingLeft;
translateY = mPaddingTop;
} else {
width = getWidth();
height = getHeight();
translateX = 0;
translateY = 0;
}
canvas.translate(translateX, Math.min(0, scrollY) + translateY);
mEdgeGlowTop.setSize(width, height);
if (mEdgeGlowTop.draw(canvas)) {
postInvalidateOnAnimation();
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeGlowBottom.isFinished()) {
final int restoreCount = canvas.save();
final int width;
final int height;
final float translateX;
final float translateY;
if (clipToPadding) {
width = getWidth() - mPaddingLeft - mPaddingRight;
height = getHeight() - mPaddingTop - mPaddingBottom;
translateX = mPaddingLeft;
translateY = mPaddingTop;
} else {
width = getWidth();
height = getHeight();
translateX = 0;
translateY = 0;
}
canvas.translate(-width + translateX,
Math.max(getScrollRange(), scrollY) + height + translateY);
canvas.rotate(180, width, 0);
mEdgeGlowBottom.setSize(width, height);
if (mEdgeGlowBottom.draw(canvas)) {
postInvalidateOnAnimation();
}
canvas.restoreToCount(restoreCount);
}
}
}
在draw()階段涉及到的變量主要是mEdgeGlowTop和mEdgeGlowBottom评抚,這兩個變量的類型是EdgeEffect豹缀,用于控制邊緣的陰影效果伯复,ScrollView的draw()階段主要工作是繪制了上下陰影
事件相關
作為一個ViewGroup的子類,ScrollView與事件相關的有dispatchTouchEvent()/onInterceptTouchEvent()/onTouchEvent()這3個處理函數(shù)邢笙,其中ScrollView沒有重寫dispatchTouchEvent()啸如,因此主要來關注下onInterceptTouchEvent()和onTouchEvent()這兩個函數(shù)做了什么
- onInterceptTouchEvent()
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onMotionEvent will be called and we do the actual
* scrolling there.
*/
// 移動事件并且處于拖拽過程,這種狀況肯定攔截
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
if (super.onInterceptTouchEvent(ev)) {
return true;
}
// 如果沒有滑動空間氮惯,則肯定不攔截
if (getScrollY() == 0 && !canScrollVertically(1)) {
return false;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
// 走到這里叮雳,證明是移動事件,但還不是拖拽過程妇汗,需要檢測用戶是否移動了足夠遠
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
}
// 下面幾行是用這一次事件的y和上次的y來進行差值帘不,看是否大于最小滑動距離,是的話就需要標記拖拽狀態(tài)為true并且進行一系列的
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
mIsBeingDragged = true;
mLastMotionY = y;
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內杨箭,將拖拽狀態(tài)置為false
if (!inChild((int) ev.getX(), (int) y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
// 記錄下當前按下的位置
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
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.
*/
mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();
if (mIsBeingDragged && mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
startNestedScroll(SCROLL_AXIS_VERTICAL);
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// 抬起動作寞焙,重置拖拽狀態(tài)為false
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
stopNestedScroll();
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
return mIsBeingDragged;
}
由官方注釋可以看出,onInterceptTouchEvent()只是決定是否攔截事件互婿,而并不處理觸摸邏輯捣郊。
- onTouchEvent()
ScrollView的onTouchEvent()源碼結構是這樣的:
@Override
public boolean onTouchEvent(MotionEvent ev) {
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: {
// ……
}
case MotionEvent.ACTION_MOVE:
// ……
case MotionEvent.ACTION_UP:
// ……
case MotionEvent.ACTION_CANCEL:
// ……
case MotionEvent.ACTION_POINTER_DOWN: {
// ……
case MotionEvent.ACTION_POINTER_UP:
// ……
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
首先會初始化VelocityTracker,這個類的作用是追蹤觸摸事件的加速度慈参;最后會調用其addMovement()函數(shù)呛牲,中間部分是對各個動作的具體處理,下面看下各個動作都會怎樣處理:
- ACTION_DOWN
case MotionEvent.ACTION_DOWN: {
if (getChildCount() == 0) {
return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
/*
* mScroller.isFinished()如果為false懂牧,證明處于拖拽或者fling狀態(tài)
* 這時候需要把之前的滑動停下來
*/
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;
}
- ACTION_MOVE
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);
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
// 如果還沒有處在拖拽狀態(tài)并且滑動距離大于最小滑動距離
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;
}
}
// 如果已經(jīng)處于拖拽狀態(tài)
if (mIsBeingDragged) {
// Scroll to follow the motion event
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) {
// 這里面的邏輯是和邊緣陰影的處理有關
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事件中需要關注的是,會調用View的overScrollBy()方法來真正處理觸摸事件僧凤,overScrollBy()方法實現(xiàn)如下:
protected boolean overScrollBy(int deltaX, int deltaY,
int scrollX, int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY,
boolean isTouchEvent) {
final int overScrollMode = mOverScrollMode;
final boolean canScrollHorizontal =
computeHorizontalScrollRange() > computeHorizontalScrollExtent();
final boolean canScrollVertical =
computeVerticalScrollRange() > computeVerticalScrollExtent();
final boolean overScrollHorizontal = overScrollMode == OVER_SCROLL_ALWAYS ||
(overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
final boolean overScrollVertical = overScrollMode == OVER_SCROLL_ALWAYS ||
(overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);
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;
}
主要的流程就是根據(jù)傳入的各個滑動參數(shù)畜侦,以及View本身與滑動相關的屬性,計算出newScrollX/newScrollY等參數(shù)躯保,然后調用onOverScrolled()方法來進行實際的滑動處理
onOverScrolled()方法在View中是空實現(xiàn)旋膳,因此具體的實現(xiàn)在ScrollView中:
@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();
}
有兩條分支,mScroller.isFinished()為false證明上次的滑動還沒有結束途事,這時需要計算出新的滑動位置進行滑動验懊;如果此時不在滑動,則調用View的scrollTo()方法滑動到對應位置尸变;最后一行awakenScrollBars()作用是讓ScrollBar顯示出來
- ACTION_UP
case MotionEvent.ACTION_UP:
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;
endDrag();
}
break;
當手指抬起時义图,ScrollView的操作很簡單,先判斷是否處于滑動狀態(tài)召烂,如果不是就什么都不用干碱工;如果是的話,需要讓滑動繼續(xù)慣性向前滑一段,這里借助了VelocityTracker和Scroller來實現(xiàn)怕篷。