Android ScrollView实现滚动超过边界松手回弹

Android ScrollView实现滚动超过边界松手回弹

ScrollView滚动超过边界,松手回弹

Android原生的ScrollView滑动到边界之后,就不能再滑动了,感觉很生硬。不及再多滑动一段距离,松手后回弹这种效果顺滑一些。

先查看下滚动里面代码的处理

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 (!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();                     }                     ………………………………       } break;

先判断手指的移动距离,超过了移动的默认距离,认为是处于mIsBeingDragged状态,然后调用overScrollBy()函数,这个方法是实现滚动的关键。并且该方法有个参数传递的是mOverscrollDistance,通过名字可以知道是超过滚动距离,猜测这个是预留的实现超过滚动边界的变量。
进入该方法看一下

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;     }

ScrollView主要是竖直方向的滚动,主要看其Y轴方向的偏移。可以看到newScrollY的范围,top是-maxOverScrollY,bottom是maxOverScrollY + scrollRangeY,其中scrollRangeY是mScrollY的范围值,maxOverScrollY是超过边界的范围值。如果newScrollY的值小于top或者大于bottom,会对该值进行调整。
再进入onOverScrolled()方法看看,

@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,说明正在滚动动画中(包括fling和springBack)。如果没有滚动动画,则直接调用scrollTo到新的滑动到的mScrollY。再经过绘制之后,就能看到界面滚动。
再回看overScrollBy()方法中,如果偏移距离到-maxOverScrollY与0之间,则是滑动超过上面边界;如果偏移在scrollRangeY与maxOverScrollY + scrollRangeY之间,则是滑动超过下面边界。
通过上面的分析可知,maxOverScrollY参数是预留的超过边界的滑动距离,看一下传递过来的实参为成员变量mOverscrollDistance,改动一下该值应该就可以实现超过边界滑动了。但是发现成员变量为private,并且也没提供修改的方法,所以改变该变量的值可以通过反射修改。
下面为修改

class OverScrollDisScrollView(cont: Context, attrs: AttributeSet?): ScrollView(cont, attrs) {     val tag = "OverScrollDisScrollView"     private val overScrollDistance = 500     constructor(cont: Context): this(cont, null)     init {         val sClass = ScrollView::class.java         var field: Field? = null         try {             field = sClass.getDeclaredField("mOverscrollDistance")             field.isAccessible = true             field.set(this, overScrollDistance)         } catch (e: NoSuchFieldException) {             e.printStackTrace()         } catch (e: IllegalAccessException) {             e.printStackTrace()         }         overScrollMode = OVER_SCROLL_ALWAYS     } }

这样修改可以实现滑动超过边界,不过有个问题,就是有时候松手了不能弹回,卡在超过边界那了。需要看看手指抬起的代码处理,经过代码调试发现问题出在手指抬起的下列代码了

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;

假如现在手指向下滑动超过边界的时候,计算出来的速度initialVelocity是正数,取个负然后传到方法flingWithNestedDispatch()函数

private void flingWithNestedDispatch(int velocityY) {         final boolean canFling = (mScrollY > 0 || velocityY > 0) &&                 (mScrollY < getScrollRange() || velocityY < 0);         if (!dispatchNestedPreFling(0, velocityY)) {             dispatchNestedFling(0, velocityY, canFling);             if (canFling) {                 fling(velocityY);             }         }     }

这个时候mScrollY小于0,velocityY小于0,所以canFling为false,导致后续的操作都不做了。这个时候,在界面上表现得就是卡在那里不动了。
超过边界不弹回,这个问题怎么解决?经过调试,找到以下方法,见代码:

class OverScrollDisScrollView(cont: Context, attrs: AttributeSet?): ScrollView(cont, attrs) {     val tag = "OverScrollDisScrollView"     private val overScrollDistance = 500     constructor(cont: Context): this(cont, null)     init {         val sClass = ScrollView::class.java         var field: Field? = null         try {             field = sClass.getDeclaredField("mOverscrollDistance")             field.isAccessible = true             field.set(this, overScrollDistance)         } catch (e: NoSuchFieldException) {             e.printStackTrace()         } catch (e: IllegalAccessException) {             e.printStackTrace()         }         overScrollMode = OVER_SCROLL_ALWAYS     } //    override fun onOverScrolled(scrollX: Int, scrollY: Int, clampedX: Boolean, clampedY: Boolean) { //        super.onOverScrolled(scrollX, scrollY, clampedX, clampedY) //    }     override fun onTouchEvent(ev: MotionEvent?): Boolean {         super.onTouchEvent(ev)         if (ev != null) {             when(ev.action) {                 MotionEvent.ACTION_UP -> {                     val yDown = getYDownScrollRange()                     //解决超过边界松手不回弹得问题                     if (mScrollY < 0) {                         scrollTo(0, 0) //                        onOverScrolled(0, 0, false, false)                     } else if (mScrollY > yDown) {                         scrollTo(0, yDown) //                        onOverScrolled(0, yDown, false, false)                     }                 }             }         }         return true     }     private fun getYDownScrollRange(): Int {         var scrollRange = 0         if (childCount > 0) {             val child = getChildAt(0)             scrollRange = Math.max(                 0,                 child.height - (height - mPaddingBottom - mPaddingTop)             )         }         return scrollRange     } }

在onTouchEvent中最后,手指抬起的时候,加上一道判断,如果这个时候是超过边界的状态,弹回边界。这样基本上,可以解决问题。

推荐阅读