scrollview作為一個android的基礎(chǔ)的控件,用途十分的廣泛麻惶,下面來分析它他的原理
關(guān)于onMeasure,onLayout
scrollview的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);
}
}
}
可以看到,在onMeasure中是調(diào)用父view的onMeasure的邏輯進(jìn)行測量的猛计,onMeasure的目的就是讓子view測量洲脂,然后獲取到寬高后,然后在給自身的寬高賦值裹虫,而在onMeasure中,要測量子view必定會調(diào)用measureChildWithMargins方法融击,此方法很明顯是根據(jù)父view的measureSpec減去自身的padding和子view的margin筑公,然后根據(jù)子view的xml的寬高,計算出子view的寬高的尊浪,很明顯匣屡,scrollview打破了這種規(guī)則封救,來看下其重寫的方法
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
heightUsed;
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
可以看到在寬度的計算上沒啥問題,而在高度上捣作,無論子view的高度是那種模式誉结,scrollview都給子view賦值為MeasureSpec.UNSPECIFIED模式,高度就是自身的高度券躁,所以這也是為啥scrollview嵌套listview時惩坑,高度顯示不全的原因了,因為listview沒有對UNSPECIFIED模式做處理也拜,而此時子view是linearLayout,linearLayout在UNSPECIFIED的模式下的高度是根據(jù)子view的高度再加上其子view也就是textview的margin和自己的padding決定的以舒,所以measure的步驟分析完了,簡單來說就是根據(jù)子view的高度決定自己的高度(LinearLayout的高度比scrollview要高)
而onLayout用的也是FrameLayout的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);
}
可以看到又是熟悉的super
關(guān)于滑動的分析
在分析前慢哈,如果對事件分發(fā)不熟悉的話可以看下事件分發(fā)結(jié)論
簡單來說就是在viewRootImp中先注冊了一個inputChannel對象蔓钟,然后底層會回調(diào)
private void dispatchInputEvent(int seq, InputEvent event, int displayId) {
mSeqMap.put(event.getSequenceNumber(), seq);
onInputEvent(event, displayId);
}
繼而間接的調(diào)用view的dispatchPointerEvent方法
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
繼而調(diào)用decorview的dispatchTouEvent方法,在activity中此decorview進(jìn)行了重寫卵贱,會調(diào)用activity的dispatchTouEvent方法滥沫,而我們在popwindow中,此方法如下
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
return true;
}
return super.dispatchTouchEvent(ev);
}
可以看到直接調(diào)用了viewGroup的dispatchTouchEvent方法键俱,此方法在down的時候會層層向下調(diào)用兰绣,一直調(diào)用scrollview的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.
*/
/*
* Shortcut the most recurring case: the user is in the dragging
* state and he is moving his finger. We want to intercept this
* motion.
*/
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
if (super.onInterceptTouchEvent(ev)) {
return true;
}
/*
* Don't try to intercept touch if we can't scroll anyway.
*/
if (getScrollY() == 0 && !canScrollVertically(1)) {
return false;
}
switch (action & MotionEvent.ACTION_MASK) {
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);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
}
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();
if (!inChild((int) ev.getX(), (int) y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
/*
* Remember location of down touch.
* ACTION_DOWN always refers to pointer index 0.
*/
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:
/* Release the drag */
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;
}
/*
* The only time we want to intercept motion events is if we are in the
* drag mode.
*/
return mIsBeingDragged;
}
其實代碼在down的時候主要做了這幾件事
1.確定了down在scrollview的坐標(biāo)
2.如果此時在滑動的情況下方妖,點下去了狭魂,讓其停止滑動,也就是調(diào)用overScroller的computeScrollOffset方法
3.進(jìn)行了startNestedScroll也就是嵌套滑動党觅,其實看代碼scrollview在5.0以上是完全兼容嵌套滑動的代碼如下
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = getParent();
View child = this;
while (p != null) {
try {
if (p.onStartNestedScroll(child, this, axes)) {
mNestedScrollingParent = p;
p.onNestedScrollAccepted(child, this, axes);
return true;
}
} catch (AbstractMethodError e) {
Log.e(VIEW_LOG_TAG, "ViewParent " + p + " does not implement interface " +
"method onStartNestedScroll", e);
// Allow the search upward to continue
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
可以看到如果支持嵌套滑動的話雌澄,也就是isNestedScrollingEnabled為true,那他調(diào)用父view的onStartNestedScroll方法杯瞻,如果為true镐牺,那它就調(diào)用其onNestedScrollAccepted方法,我們就以scrollview舉例魁莉,看其這兩個方法是如何寫的
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
super.onNestedScrollAccepted(child, target, axes);
startNestedScroll(SCROLL_AXIS_VERTICAL);
}
可以看到scrollview的parent的onNestedScrollAccepted會一直循環(huán)向上調(diào)用
由于scrollview的Intercept方法在down的時候返回的false,所以回去找能消耗down事件的view睬涧,也就是mFirstTargart的賦值
如果mFirstTargart找到了,就記錄下mFirstTargart的值
如果沒找到就讓scrollview處理down事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
...
主要是viewGroup的這個方法判斷的
在來看下move的情況
也差不多分2種情況
要看viewGroup的mFirstTouch字段是否為null
1.如果是null的話旗唁,說明子view不能響應(yīng)down的事件畦浓,此時將不會走scrollview的Intercept方法,所有的事件都會到scrollview上
2.如果不為null检疫,由于Intercept方法中move的代碼如下
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);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
}
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;
}
也就是說如果滑動的絕對值大于最小移動值讶请,就其返回true,此時他的子view會收到cancel事件屎媳,并且由于設(shè)置了requestDisallowInterceptTouchEvent夺溢,所以scrollview的parent也不會搶走他的事件了论巍,總而言之就是move和up都會確保調(diào)用其onTouchEvent的方法,那重點看下其onTouchEvent方法
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];
}
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 = 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.
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;
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;
可以看到由于有了嵌套滑動的邏輯风响,代碼變得很長嘉汰,首先會調(diào)用dispatchNestedPreScroll方法
public boolean dispatchNestedPreScroll(int dx, int dy,
@Nullable @Size(2) int[] consumed, @Nullable @Size(2) int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[2];
}
consumed = mTempNestedScrollConsumed;
}
consumed[0] = 0;
consumed[1] = 0;
mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed);
if (offsetInWindow != null) {
getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
返回值由父view的onNestedPreScroll返回值決定,也就是消耗的意思
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
舉個例子状勤,在scrollview上滑動了50像素鞋怀,但是父view消耗了50
像素,(就是拿consumed數(shù)組的Y賦值50)那這個move就不會滑動
而如果走到了下面的mIsBeingDragged的話持搜,表示可滑動的接箫,繼而會調(diào)用overScrollBy方法,這個方法也就是核心方法朵诫,內(nèi)部實現(xiàn)是裁剪了點距離,算出了邊界薄扁,并沒有真正的滑動剪返,此方法會間接的調(diào)用重載方法
@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();
}
眾所周知,scroller的滑動依靠的是不斷的invidate邓梅,然后改變畫布的位置脱盲,其實所有view本身的位置并沒有變化,那重繪的代碼呢日缨?其實在awakenScrollBars這里面
protected boolean awakenScrollBars(int startDelay, boolean invalidate) {
final ScrollabilityCache scrollCache = mScrollCache;
if (scrollCache == null || !scrollCache.fadeScrollBars) {
return false;
}
if (scrollCache.scrollBar == null) {
scrollCache.scrollBar = new ScrollBarDrawable();
scrollCache.scrollBar.setState(getDrawableState());
scrollCache.scrollBar.setCallback(this);
}
if (isHorizontalScrollBarEnabled() || isVerticalScrollBarEnabled()) {
if (invalidate) {
// Invalidate to show the scrollbars
postInvalidateOnAnimation();
}
這里會調(diào)用重繪钱反,繼而調(diào)用computeScroll,里面會一直的循環(huán)調(diào)用scrollerby
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
// This is called at drawing time by ViewGroup. We don't want to
// re-show the scrollbars at this point, which scrollTo will do,
// so we replicate most of scrollTo here.
//
// It's a little odd to call onScrollChanged from inside the drawing.
//
// It is, except when you remember that computeScroll() is used to
// animate scrolling. So unless we want to defer the onScrollChanged()
// until the end of the animated scrolling, we don't really have a
// choice here.
//
// I agree. The alternative, which I think would be worse, is to post
// something and tell the subclasses later. This is bad because there
// will be a window where mScrollX/Y is different from what the app
// thinks it is.
//
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;
}
}
}
在調(diào)用完overScrollBy以后匣距,如果有嵌套滑動的邏輯的話面哥,還會調(diào)用dispatchNestedScroll,啥意思呢
簡單來說就是當(dāng)scrollview一直滑動滑到頂部或滑到最下面毅待,然后繼續(xù)滑動尚卫,這時候如果要父view處理的話,就用此邏輯
當(dāng)然后面的canOVerscroll是關(guān)于滑動光暈的代碼尸红。
在up的時候基本上和move差不多吱涉,只不過有fling的邏輯,在fling到頭后外里,也會采用嵌套滑動怎爵,讓父view處理,這里就不多說了
總結(jié):
scrollview作為經(jīng)典的基礎(chǔ)控件盅蝗,可學(xué)習(xí)地方非常多包括但不限于:
1.滑動機制的處理(看了代碼才知道鳖链,其實scrollview是有嵌套滑動的代碼的,只不過沒有nestedScrollview支持到5.0以下)
2.事件分發(fā)的處理风科,很經(jīng)典撒轮,很全面的處理
3.對于overScroller的運用乞旦,囊括了滑動和fling的處理,也是滑動的三種實現(xiàn)之一(scroller,layout,translate)當(dāng)然scrollerview也支持像ios那樣的下拉回彈的效果(只不過默認(rèn)被clamped),關(guān)鍵代碼在其重寫的scroll to上题山,
4.多點觸控以及速度跟蹤器的運用兰粉,這里沒過多的分析,自己寫的話照著寫就可以了