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