一、scrollTo()和scrollBy()
调用View的scrollTo()和scrollBy()是用于滑动View中的内容,而不是把某个View的位置进行改变。
public class View {
....
protected int mScrollX; //该视图内容相当于视图起始坐标X的偏移量
protected int mScrollY; //该视图内容相当于视图起始坐标Y的偏移量
//返回值
public final int getScrollX() {
return mScrollX;
}
public final int getScrollY() {
return mScrollY;
}
public void scrollTo(int x, int y) {
//偏移位置发生了改变
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x; //赋新值,保存当前偏移量
mScrollY = y;
//回调onScrollChanged方法
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
invalidate(); //一般都引起重绘
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
//...
}
例如:
如果一个Button或者TextView调用scrollTo()和scrollBy()方法,它移动的是Button或者TextView里面的text文本,而不是Button或者TextView本身
如果一个LinearLayout调用scrollTo()和scrollBy()方法,它移动的是LinearLayout里的子View,而不是LinearLayout本身
所以如果想改变某个View本身在屏幕中的位置,可以使用如下的方法。
public void offsetLeftAndRight(int offset)用于左右移动方法
public void offsetTopAndBottom(int offset)用于上下移动
二、Scroller类
public class Scroller {
private int mStartX; //起始坐标点 , X轴方向
private int mStartY; //起始坐标点 , Y轴方向
private int mCurrX; //当前坐标点 X轴, 即调用startScroll函数后,经过一定时间所达到的值
private int mCurrY; //当前坐标点 Y轴, 即调用startScroll函数后,经过一定时间所达到的值
private float mDeltaX; //应该继续滑动的距离, X轴方向
private float mDeltaY; //应该继续滑动的距离, Y轴方向
private boolean mFinished; //是否已经完成本次滑动操作, 如果完成则为 true
//构造函数
public Scroller(Context context) {
this(context, null);
}
public final boolean isFinished() {
return mFinished;
}
//强制结束本次滑屏操作
public final void forceFinished(boolean finished) {
mFinished = finished;
}
public final int getCurrX() {
return mCurrX;
}
/* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished. loc will be altered to provide the
* new location. */
//根据当前已经消逝的时间计算当前的坐标点,保存在mCurrX和mCurrY值中
public boolean computeScrollOffset() {
if (mFinished) { //已经完成了本次动画控制,直接返回为false
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
float x = (float)timePassed * mDurationReciprocal;
...
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
...
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
//开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,即到达坐标为(startX+dx , startY+dy)出
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX; mStartY = startY;
mFinalX = startX + dx; mFinalY = startY + dy;
mDeltaX = dx; mDeltaY = dy;
...
}
}
Android框架提供了 computeScroll()方法去控制这个流程。在绘制View时,会在draw()过程调用该方法。因此, 再配合使用Scroller实例,我们就可以获得当前应该的偏移坐标,手动使View/ViewGroup偏移至该处。computeScroll()方法原型如下,该方法位于ViewGroup.java类中
@Override
protected void dispatchDraw(Canvas canvas){
...
for (int i = 0; i < count; i++) {
final View child = children[getChildDrawingOrder(count, i)];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
...
child.computeScroll();
...
}
在ViewGroup没有实现computeScroll这个方法,我们再来看看View里面,发现这个方法是一个空方法,也就行它需要我们自己来实现,即我们自己来控制移动效果。这个也很好理解,具体的移动效果肯定应该我们自己来实现和控制了。
public void computeScroll() {
}
下面我们来总结一下,Scroller只是一个位置计算类,它会根据起点和终点的位置以及我们给定的位移时间计算出接下来每个时刻的位置坐标。我们要做的就是不断的获取这个坐标进行移动,这样就实现了移动。
在Scroller中有以下方法需要强调一下:
startScroll(int startX, int startY, int dx, int dy, int duration)
它的含义跟函数名是不同的,并不是开始滚动,它只是根据起点、终点以及给定的时间间隔计算出每单位时间的位移量。
所以在调用startScroll函数之后,我们需要调用invalidate方法来进行刷新,因为调用invalidate方法之后就会进行重绘。
在dispatchDraw中我们看到它会调用computeScroll方法,这个时候我们就可以实现这个方法控制自己的移动了。
下面我们来看一段代码,代码来自参考文献2
public class ScrollerLayout extends ViewGroup {
private int leftBorder;
private int rightBorder;
private float mXMove;
private float mXLastMove;
private Scroller mScroller;
private int mTouchSlop;
private float mXDown;
public ScrollerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
ViewConfiguration configuration = ViewConfiguration.get(context);
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
leftBorder = getChildAt(0).getLeft();
rightBorder = getChildAt(getChildCount() - 1).getRight();
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mXDown = ev.getRawX();
mXLastMove = mXDown;
break;
case MotionEvent.ACTION_MOVE:
mXMove = ev.getRawX();
float diff = Math.abs(mXMove - mXDown);
mXLastMove = mXMove;
if (diff > mTouchSlop) {
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
mXMove = event.getRawX();
int scrolledX = (int) (mXLastMove - mXMove);
if (getScrollX() + scrolledX < leftBorder) {
// 看到这句现在应该就明白了
// 它移动的是ViewGroup里面的内容,即子View,而不是ViewGroup本身
scrollTo(leftBorder, 0);
return true;
} else if (getScrollX() + getWidth() + scrolledX > rightBorder) {
// 它移动的是ViewGroup里面的内容,即子View,而不是ViewGroup本身
scrollTo(rightBorder - getWidth(), 0);
return true;
}
scrollBy(scrolledX, 0);
mXLastMove = mXMove;
break;
case MotionEvent.ACTION_UP:
int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
int dx = targetIndex * getWidth() - getScrollX();
// 松开触摸事件的时候,为了自动平滑的滑动
// 调用startScroll通过滑动的距离以及时间,计算每单位时间滑动的距离
mScroller.startScroll(getScrollX(), 0, dx, 0);
// 触发重绘,这个时候才真正触发滑动,它会调用computeScroll方法
invalidate();
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
// 计算子View当前应该所处的位置
if (mScroller.computeScrollOffset()) {
// 将ViewGroup的内容进行滑动,即对ViewGroup的子View进行滑动
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
// 继续重绘,这样就会重复调用computeScroll方法了,就实现了连续平滑滑动
invalidate();
}
super.computeScroll();
}
}
参考文献
1、图解Android View的scrollTo(),scrollBy(),getScrollX(), getScrollY()
2、 Android Scroller完全解析,关于Scroller你所需知道的一切
3、Android中滑屏实现—-手把手教你如何实现触摸滑屏以及Scroller类详解
4、 Android中滑屏初探 —- scrollTo 以及 scrollBy方法使用说明
5、 Android 带你从源码的角度解析Scroller的滚动实现原理