OverScroller与Scroller类类似,都是实现弹性滑动、惯性滑动的辅助类,只不过OverScroller出现的比较晚,功能更全面一些,可以实现边界回弹等。
之所以说它是一个辅助类,是因为它本身并不能移动View,真正移动View的实现需要我们在回调方法computeScroll()
中自己实现。
基础方法
getStartX()和getStartY()
getStartX()
滑动的起点x坐标 ,对应startX的值
getStartY()
滑动的起点y坐标 ,对应startY的值
getFinalX()和getFinalY()
getFinalX()
滑动的终点x坐标,通过 startX + dx
计算得来,对应源码mFinal = start + distance
getFinalY()
滑动的终点y坐标,通过 startY + dy
计算得来
getCurrX()和getCurrY()
getCurrX()
滑动时,该方法返回当前x轴坐标,随着时间的推移最终等于终点坐标
getCurrY()
当前在y轴坐标,随着时间的推移最终等于终点坐标
getCurrVelocity()
getCurrVelocity()
当前滑动的速度,根据X和Y轴速度计算得来
computeScrollOffset()
computeScrollOffset()
返回值类型为boolean,true说明滚动尚未完成,false说明滚动已经完成。这是一个很重要的方法,通常放在View.computeScroll()中,用来判断滚动是否结束。
以上get系列方法都有对应的set方法,如setFinalX(int newX)
(设置最终停留的水平坐标,没有动画效果,直接跳到目标位置) 等。不过这些set方法一般都用不到或者要避免去使用。如果要实现移动,使用下面的核心方法。
需要再强调一遍,无论是set系列方法还是下面的核心方法,本身并不能移动控件,而是要通过重写computeScroll()
方法自己去实现。
核心方法
startScroll
/**
* Start scrolling by providing a starting point and the distance to travel.
* The scroll will use the default value of 250 milliseconds for the
* duration.
* 通过提供一个起点和要移动的距离开始滚动,默认耗时250ms
* @param startX x轴起点坐标 ,这里包括下面的不一定是绝对坐标点,事实
* 上大多情况下都不是。要根据具体实现来区分,如实现滑动使用的是setTranslationY
* 方法,那么startX可能就是getTranslationY
* @param startY y轴起点坐标
*
* @param dx x轴滑动距离 正数内容左移
*
* @param dy Y轴滑动距离 正数内容上移
*
*/
public void startScroll(int startX, int startY, int dx, int dy) {
startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
}
根据这个方法的参数,可以计算终点坐标,如Y轴终点坐标为startY + dy
。
使用示例:
private float mLastY;
@Override
public boolean onTouchEvent(MotionEvent event) {
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
float dy = y - mLastY;
if (Math.abs(dy) > 5){
mScroller.startScroll(0, mScroller.getFinalY(), 0, (int) dy);
invalidate();
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
setTranslationY(mScroller.getCurrY());
postInvalidate();
}
}
fling
/**
* 基于一个投掷手势开始滚动。所走的距离将取决于抛掷的初速度。
*
* @param startX X轴滚动起点坐标,这里包括下面的不一定是绝对坐标点,事实
* 上大多情况下都不是。要根据具体实现来区分,如实现滑动使用的是scrollTo方
* 法,那么startX可能就是getScrollX
*
* @param startY Y轴滚动起点坐标
*
* @param velocityX X轴初速度
*
* @param velocityY Y轴初速度
*
* @param minX 首先要明确的是这个值不是绝对坐标,而是一个距离或者说是一个范围,等于
* startX减去view向左滚动最终停留的位置坐标FinalX,即StartX - FinalX。
* 如果速度足够大且overX > 0,view在滚动的时候,滚动范围会超出这个值,
* 但随后会自动回弹到minX处,总的来说这个值控制着左边界
*
* @param maxX 和minX类似,只不过,这个值控制的是右边界
*
* @param minY 和minX类似,控制上边界
*
* @param maxY 和minX类似,控制下边界
*
* @param overX 滚动时允许滑出左右边界的范围,如果是0,滚动到边界会立即停止,如
* 果大于0,会有一个回弹效果
*
* @param overY 滚动时允许滑出上下边界的范围
*/
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY, int overX, int overY) {
// Continue a scroll or fling in progress
if (mFlywheel && !isFinished()) {
float oldVelocityX = mScrollerX.mCurrVelocity;
float oldVelocityY = mScrollerY.mCurrVelocity;
if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
Math.signum(velocityY) == Math.signum(oldVelocityY)) {
velocityX += oldVelocityX;
velocityY += oldVelocityY;
}
}
mMode = FLING_MODE;
mScrollerX.fling(startX, velocityX, minX, maxX, overX);
mScrollerY.fling(startY, velocityY, minY, maxY, overY);
}
springBack
/**
* Call this when you want to 'spring back' into a valid coordinate range.
* 当你在滑动view松手后,想实现一个“回弹”的效果时,调用这个方法
* @param startX Starting X coordinate
* @param startY Starting Y coordinate
* @param minX 这里同样是一个范围,需要计算,详见下面的例子
* @param maxX Maximum valid X value
* @param minY Minimum valid Y value
* @param maxY Minimum valid Y value
* @return true if a springback was initiated, false if startX and startY were
* already within the valid range.
*/
public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) {
mMode = FLING_MODE;
// Make sure both methods are called.
final boolean spingbackX = mScrollerX.springback(startX, minX, maxX);
final boolean spingbackY = mScrollerY.springback(startY, minY, maxY);
return spingbackX || spingbackY;
}
private int top;//上边界坐标
private int bottom;//下边界坐标
private float mLastX;
private float mLastY;
@Override
public boolean onTouchEvent(MotionEvent event) {
float y = event.getY();
float x = event.getX();
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
float dx = x - mLastX;
float dy = y - mLastY;
mScroller.startScroll(0, mScroller.getFinalY(), 0, (int) dy);
invalidate();
break;
case MotionEvent.ACTION_UP:
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) mVelocityTracker.getYVelocity();
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
fling(initialVelocity);
} else if (mScroller.springBack(0, (int) getTranslationY(), 0, 0, top - getTop(),
bottom - getBottom())) {//minY = top - getTop() 即上边界坐标 - 控件上坐标
postInvalidateOnAnimation();
}
recycleVelocityTracker();
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.onTouchEvent(event);
}
public void fling(int velocityY) {
int minY = top - getTop();//实际上滑允许滚动的范围,等于minY加上overY
int maxY = bottom - getBottom();//实际下滑允许滚动的范围,等于maxY加上overY
mScroller.fling(0, (int) getTranslationY(), 0, velocityY, 0, 0, minY, maxY, 0, 50);
postInvalidateOnAnimation();
}
private void recycleVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
setTranslationY(mScroller.getCurrY());
postInvalidate();
}
}
demo
通过移动自定义TextView,测试验证OverScroller的相关方法
通过自定义流式布局,滑动内容验证OverScroller的相关方法