欣赏
线索
- 主要的成员标记
- 核心功能与方法
1. 成员
mScroller: 用来计算内部内容滚动的辅助类OverScroller,但是主要的滚动并没有使用到他哦
mEdgeGlowTop: 当滚动到最顶部的时候的上边界阴影效果
mEdgeGlowBottom: 当滚动到最底部时候的下边界阴影效果
mChildToScrollTo: 记录拥有焦点的view, 在layout的时候会将位置滚动到他的地方
mIsBeingDragged: true表示scrollview在滚动;注意在嵌套滚动的时候,不属于scrollView的滚动,他为false;
mFillViewport: true, ScrollView模式不是MeasureSpec.UNSPECIFIED, 会用scrollView的高度来当自己的高度,重新在给子view测量一遍;一般他都不会是这个模式, 什么情况下ScrollView是UNSPECIFIED模式呢,从scollView的测量可以看出,他给子view测量的规格就是UNSPECIFIED,因此scrollView嵌套scrollView的时候, 第二个scrollView如果设置了mFillViewport,依然是不会重新测量他的子view的哦。
mActivePointerId: 当前激活的手指; 这个是scrollView处理多手指滑动的原则。他的基本规则是这样的,当第二个手指落下滑动的时候,那么前面第一个手指滑动就失效了,因为将激活的手指给到了第二个手指,当第二个手指抬起的时候,又会把激活的点给到第一个手指。
其他的就不一一列出来了......
2. 功能与方法
普通的LinearLayout, RelativeLayout, FrameLayout他们的页面内容都是有限的,限于一屏之内,而ScrollView却不是哦,这就是他主要功能。
- ScrollView的构造与初始化:
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);
//读取是否设置了viewport属性,让子view填充scrollView高度
setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));
a.recycle();
}
private void initScrollView() {
mScroller = new OverScroller(getContext());
setFocusable(true);
//优先让子view获得焦点
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
setWillNotDraw(false);
final ViewConfiguration configuration = ViewConfiguration.get(mContext);
//滚动识别距离,
mTouchSlop = configuration.getScaledTouchSlop();
//fling滑行的最小速度
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
//fling滑行的最大速度
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
mOverscrollDistance = configuration.getScaledOverscrollDistance();
mOverflingDistance = configuration.getScaledOverflingDistance();
}
总结一下,其实构造和初始化比较简单,就是读取scrollView的一个_fillViewport属性,如果他为true, 他的子child一般会经过两侧测量,最终的高度和scollView是一样的; 然后就是读取系统的默认配置,比如移动多少算是滚动,多大速度可以在松手时候认为是要去滑翔。
-
ScrollView只能有一个child,否则会抛异常, 看这里:
public void addView(View child) { if (getChildCount() > 0) { throw new IllegalStateException("ScrollView can host only one direct child"); } super.addView(child); }
-
setFillViewport,动态设定ScrollView的填充属性
public void setFillViewport(boolean fillViewport) { if (fillViewport != mFillViewport) { mFillViewport = fillViewport; //因为要改变子child的高度,自然要重新测量,布局一次啦。 requestLayout(); } }
-
ScrollView的测量入口, onMeasure:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //主要的测量还是调用父类FrameLayout的测量, super.onMeasure(widthMeasureSpec, heightMeasureSpec); //mFillViewport为true, 才需要重新测量一次 if (!mFillViewport) { return; } final int heightMode = MeasureSpec.getMode(heightMeasureSpec); //scrollView本身的模式一般不会是这个,除非是scrollView的父容器也是scrollView if (heightMode == MeasureSpec.UNSPECIFIED) { return; } if (getChildCount() > 0) { final View child = getChildAt(0); int height = getMeasuredHeight(); //如果child的高度小于scrollView的高度啦, if (child.getMeasuredHeight() < height) { final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); height -= mPaddingTop; height -= mPaddingBottom; int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); //重新测量子child child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } }
- 总结一下, 可以看出ScrollView的本身测量主要是调用父类FrameLayout的测量方法,简单说说FrameLayout的测量,他其实是先遍历所有的子view, 找到一个高度最大的view作为预备的高度,然后通过FrameLayout本身的测量规格(包含规格和父容器的剩余高度),来决定他的高度。比如FrameLayout的onMeasure传递过来的规格是EXACTLY,那么最终高度就是measureSpec解析出来的高度,比如传递过来的是AT_MOST,并且我们的预备高度小于measureSpec解析的高度,那么最终高度就是我们前面计算的预备高度啦, 也就是我们的scrollView的最终高度!
-
ScrollView的测量之二,子Child的测量measureChildWithMargins:
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); //获取child的宽度规格, final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); //构建高度的测量规格,可以看到给到size只是一个上下margin, mode是无限制的。根据标准的测量原则 //可以知道,子child想要多高就给多高啦。 final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); //测量。 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
- 总结一下,在测量scrollview的时候,其实是要在FrameLayout中先测量他的子view的宽高的,然后根据子view的高度来设定Scrollview的高度。scrollview重写的measureChildWithMargins方法,他在测量子child时候给child的规格是MeasureSpec.UNSPECIFIED, 这个是很少见的,但原来也是很有用的,他可以让子child得到他想要的任意高度,ScrollView不限制他的子view高度, 在滚动容器中正好是需要的啦!
-
ScrollView的布局
protected void onLayout(boolean changed, int l, int t, int r, int b) { //依然还是调用frameLayout去布局他的child啦; super.onLayout(changed, l, t, r, b); // mIsLayoutDirty = false; // 如果记录了焦点的child,那么就滚到到该child的位置,注意可能不是ScrollView的直接子child哦 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)); // 范围纠正 if (mScrollY > scrollRange) { mScrollY = scrollRange; } else if (mScrollY < 0) { mScrollY = 0; } } // 重新布局的时候不会丢失原来的滚动位置 scrollTo(mScrollX, mScrollY); }
- 总结一下,可以看出,布局的主要手段还是借助了FrameLayout,这个其实也很简单,就一个child. ScrollView不本身做了焦点child的位置滚动,以及还原以前的scroll位置.
-
ScrollView的绘制,draw方法
public void draw(Canvas canvas) { //调用fm.draw绘制自己的内容 super.draw(canvas); if (mEdgeGlowTop != null) { final int scrollY = mScrollY; if (!mEdgeGlowTop.isFinished()) {//拖到了边界处没有松手 final int restoreCount = canvas.save(); final int width = getWidth() - mPaddingLeft - mPaddingRight; canvas.translate(mPaddingLeft, Math.min(0, scrollY)); mEdgeGlowTop.setSize(width, getHeight()); //绘制他的上边界 if (mEdgeGlowTop.draw(canvas)) { postInvalidateOnAnimation(); } canvas.restoreToCount(restoreCount); } if (!mEdgeGlowBottom.isFinished()) {//拖到了边界处没有松手 final int restoreCount = canvas.save(); final int width = getWidth() - mPaddingLeft - mPaddingRight; final int height = getHeight(); canvas.translate(-width + mPaddingLeft, Math.max(getScrollRange(), scrollY) + height); canvas.rotate(180, width, 0); mEdgeGlowBottom.setSize(width, height); //绘制他的下边界 if (mEdgeGlowBottom.draw(canvas)) { postInvalidateOnAnimation(); } canvas.restoreToCount(restoreCount); } } }
- 总结一下,除了借助Fm去绘制child和本身外,这里主要的内容就是当滚动了边界处的时候,绘制上边界的阴影和下边界的阴影效果。
-
ScollView滚动之拦截,onInterceptTouchEvent, 如果拦截了触摸,那么子view就不能顺利地使用触摸事件啦,比如ScrollView下面的Recyclerview等,看看代码吧:
public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); //如果之前拦截了,档次又是move,那么就拦下来,不去后面的计算啦 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { return true; } //没得去滑了,而且还要没滑动过不拦截,奇怪,为什么要getScrollY==? if (getScrollY() == 0 && !canScrollVertically(1)) { return false; } switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: { 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); //如果move达到了滚动,并且不是嵌套滚动,立即拦截。 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; } ........ //如果在fling状态,立即拦截。 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: //取消拦截 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; } //拦截与否,主要还是i看这个标记呢, return mIsBeingDragged; }
- 总结一下,ScrollView有几个地方会去拦截:当在down事件时候,如果当前的ScrollView在滑行状态,会拦截下来不给子view使用,这个时候down事件都并不会下发下去。当move的时候,如果不是嵌套滚动,一般ScrollView也将它拦截下来自己使用, 还有就是之前拦截了,这次又是move事件也会立刻拦截下来,整体上ScrollView就是这么处理拦截的啦,不过拦截了也不是说子view就不能用,毕竟子child可以用requestDisallowInterceptTouchEvent来禁止他的拦截生效。
-
ScollView滚动之触摸,这个算是滚动的最重要的地方之一啦, onTouchEvent, 根据手指的事件一个个地看吧:
case MotionEvent.ACTION_DOWN: { //没有child,不消耗啦,没搞头啊 if (getChildCount() == 0) { return false; } //如果当前在滚动状态,说明之前自己消耗的,这次也要自己来,请求父容器不要消耗. if ((mIsBeingDragged = !mScroller.isFinished())) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } //这里有意思,当上一次的滚动还在fling状态的时候,手指一放下,那么就会停止滑动啦,不信试试 if (!mScroller.isFinished()) { //停止滑动。 mScroller.abortAnimation(); if (mFlingStrictSpan != null) { mFlingStrictSpan.finish(); mFlingStrictSpan = null; } } // Remember where the motion event started mLastMotionY = (int) ev.getY(); mActivePointerId = ev.getPointerId(0); //触发嵌套滑动,ScrollView的嵌套滑动太鸡肋,这里觉得没有分析的必要 startNestedScroll(SCROLL_AXIS_VERTICAL); break; } ........ return true;
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); //向上滑动,大于0 int deltaY = mLastMotionY - y; //嵌套滚动相关的,在ScrollView中的实现是很鸡肋的,没什么好说的 if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { deltaY -= mScrollConsumed[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } //如果当前没有被认定为scrollview的滚动,会根据情况让scollView去滚动,这也是scrollView实现 //内容滚动的核心地方。 if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { final ViewParent parent = getParent(); //这里的意思是说,当move传递到了scrollView了,并且达到了滚动距离,就认为该让scrollview去 //消耗事件了,请求不要拦截后续的事件就交给scrollview处理了。但是这种实现有点诡异,因为如果ScollView //的父容器拦截了move,根本就不会走到这里来了,这里的请求不要拦截也不会生效,只有当父容器在move中拦截的条件还没有生效,在这里设定才会起到作用。 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; //子child.height - scrollview的height,得出可以滚动的范围 final int range = getScrollRange(); final int overscrollMode = getOverScrollMode(); //如果是OVER_SCROLL_IF_CONTENT_SCROLLS,那么必须child的内容高度大于scollview的高度才可 //认为绘制过度滚动视觉阴影 boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); //overScollBy进行了主要的内容哦你那个滚动啦; if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true) && !hasNestedScrollingParent()) { // Break our velocity if we hit a scroll barrier. mVelocityTracker.clear(); } //计算scrolledDeltaY滚动过的距离 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) {//如果累计的滚动已经小于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;
- 总结一下,在onTouchEvent中的move中,处理scrollView的内容滚动的关键地方,同时还根据滑动的位置,去计算将要绘制的上下阴影的图形效果呢。这里要有一个地方注意的是,在scrollView的move中会根据实际滑动的距离来请求他的父容器不要拦截,但是这里我认为这样的设计有太多的风险了,因为他要求父容器的拦截计算要很精确,如果在scrollview的请求不要拦截之前已经预先拦截了,那么后续的move根本不会走到这个请求的逻辑,就凉凉了。所以嵌套scrollview写拦截逻辑,是要小心一点的.......
- 对了, ScrollView中关键滚动内容的地方在overScrollBy这里,还没有说明呢:
----view.java protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { //获取scrollView的模式:三种模式啦,if_content, none, alaways; final int overScrollMode = mOverScrollMode; ....... //computeVerticalScrollRange, 高度呢,说白了child的实际高度; //computeVerticalScrollExtent---就是scrollView的高度 final boolean canScrollVertical = computeVerticalScrollRange() > computeVerticalScrollExtent(); final boolean overScrollVertical = overScrollMode == OVER_SCROLL_ALWAYS || (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); ....... //新的滚动距离,等于老的+这次move移动的距离 int newScrollY = scrollY + deltaY; if (!overScrollVertical) { maxOverScrollY = 0; } // maxOverScrollY一般都是0,这里没看头,掠过 final int left = -maxOverScrollX; final int right = maxOverScrollX + scrollRangeX; final int top = -maxOverScrollY; final int bottom = maxOverScrollY + scrollRangeY; ......... //纠正,让我们的新的滚动距离不要超过可滚动的界限 boolean clampedY = false; if (newScrollY > bottom) { newScrollY = bottom; clampedY = true; } else if (newScrollY < top) { newScrollY = top; clampedY = true; } //这里计算新的滚动距离后就调用scrollView的滚动方法,他重写了这个方法, onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); //表示滚动到了边界啦...... return clampedX || clampedY; }
- 总结一下, 这个方法之view中的方法,主要思路是根据scollView当次move的距离,进行一次距离纠正让他不会划出我们子child提供的最大范围, 没什么东西了.
- 接着再看上面的onOverScrolled方法:
protected void onOverScrolled(int scrollX, int scrollY,
boolean clampedX, boolean clampedY) {
//.
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);
}
//唤醒scrollbar
awakenScrollBars();
}
- 从上面可以看到,如果当次没滚动完,是不会滚动的,要等到下次再滚动哦.如果可以滚动就调用View.scrollTo去滑动内容。
- 好了,继续把onTouchEvent中的事件阅读完:
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;
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;
- 总结一下,当主手指抬起来的时候,判断一下要不要去滑行一下。除此之外,这里有多手指操作的判断,他的逻辑是,当第二个手指放下的时候,会将前面记录主手指的第一个手指换成第二个,然后后面的滑动第一个手指就没有效果,滑动第二个才有效果,因为计算只是跟踪主手指。现在第一个手指是副手指,第二个手指是主手指了。还有抬起的时候,如果抬起的是第二个手指即主手指抬起, 那么还要将前面的第一个手指又还原成主手指,后面滑动又跟踪他了,如果抬起的是原来的第一个手指即非主手指,那么就不做什么了....这种多指操作,也是android系统的标准行为。
- 看看onSecondaryPointerUp的实现吧....
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
//如果当前抬起的是主手指,就要进行主手指的重新定位,如果不是那么就啥都不做.....
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
// TODO: Make this decision more intelligent.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionY = (int) ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
}
}
3. 结束
- 好了, 原来ScollView的滚动还是利用View标准的scrollTo去滚动的呀,对ScrollView的理解就到这里了吧, 说实话感觉代码有点恶心,代码不算太多,但是功能不是特别明确,对应用开发可造成的隐性的问题不少。