scrollview作为一个android的基础的控件,用途十分的广泛,下面来分析它他的原理
关于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中是调用父view的onMeasure的逻辑进行测量的,onMeasure的目的就是让子view测量,然后获取到宽高后,然后在给自身的宽高赋值,而在onMeasure中,要测量子view必定会调用measureChildWithMargins方法,此方法很明显是根据父view的measureSpec减去自身的padding和子view的margin,然后根据子view的xml的宽高,计算出子view的宽高的,很明显,scrollview打破了这种规则,来看下其重写的方法
@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的模式下的高度是根据子view的高度再加上其子view也就是textview的margin和自己的padding决定的,所以measure的步骤分析完了,简单来说就是根据子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
关于滑动的分析
在分析前,如果对事件分发不熟悉的话可以看下事件分发结论
简单来说就是在viewRootImp中先注册了一个inputChannel对象,然后底层会回调
private void dispatchInputEvent(int seq, InputEvent event, int displayId) {
mSeqMap.put(event.getSequenceNumber(), seq);
onInputEvent(event, displayId);
}
继而间接的调用view的dispatchPointerEvent方法
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
继而调用decorview的dispatchTouEvent方法,在activity中此decorview进行了重写,会调用activity的dispatchTouEvent方法,而我们在popwindow中,此方法如下
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
return true;
}
return super.dispatchTouchEvent(ev);
}
可以看到直接调用了viewGroup的dispatchTouchEvent方法,此方法在down的时候会层层向下调用,一直调用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的坐标
2.如果此时在滑动的情况下,点下去了,让其停止滑动,也就是调用overScroller的computeScrollOffset方法
3.进行了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,那他调用父view的onStartNestedScroll方法,如果为true,那它就调用其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会一直循环向上调用
由于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不能响应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事件,并且由于设置了requestDisallowInterceptTouchEvent,所以scrollview的parent也不会抢走他的事件了,总而言之就是move和up都会确保调用其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;
可以看到由于有了嵌套滑动的逻辑,代码变得很长,首先会调用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数组的Y赋值50)那这个move就不会滑动
而如果走到了下面的mIsBeingDragged的话,表示可滑动的,继而会调用overScrollBy方法,这个方法也就是核心方法,内部实现是裁剪了点距离,算出了边界,并没有真正的滑动,此方法会间接的调用重载方法
@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();
}
这里会调用重绘,继而调用computeScroll,里面会一直的循环调用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;
}
}
}
在调用完overScrollBy以后,如果有嵌套滑动的逻辑的话,还会调用dispatchNestedScroll,啥意思呢
简单来说就是当scrollview一直滑动滑到顶部或滑到最下面,然后继续滑动,这时候如果要父view处理的话,就用此逻辑
当然后面的canOVerscroll是关于滑动光晕的代码。
在up的时候基本上和move差不多,只不过有fling的逻辑,在fling到头后,也会采用嵌套滑动,让父view处理,这里就不多说了
总结:
scrollview作为经典的基础控件,可学习地方非常多包括但不限于:
1.滑动机制的处理(看了代码才知道,其实scrollview是有嵌套滑动的代码的,只不过没有nestedScrollview支持到5.0以下)
2.事件分发的处理,很经典,很全面的处理
3.对于overScroller的运用,囊括了滑动和fling的处理,也是滑动的三种实现之一(scroller,layout,translate)当然scrollerview也支持像ios那样的下拉回弹的效果(只不过默认被clamped),关键代码在其重写的scroll to上,
4.多点触控以及速度跟踪器的运用,这里没过多的分析,自己写的话照着写就可以了
网友评论