时间:2022-12-10 12:09:26 | 栏目:Android代码 | 点击:次
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中最后,手指抬起的时候,加上一道判断,如果这个时候是超过边界的状态,弹回边界。这样基本上,可以解决问题。