滑动原理

实现View的滑动如下图所示:
图上通过滑动从将圆形从黄色地方移到灰色地方,从图上很明显看出,要实现这种移动只需要计算出滑动的偏移量,然后通过原始坐标就可以计算出最终坐标的位置,因此我们需要获得如下信息:

  • 第一次相对于View的坐标
  • 再次按下的时候相对于View的坐标
  • 根据上面两个坐标即可计算出两次的偏移量,有了偏移量就可以移动View了,不断重复上述步骤就实现了滑动过程。

大体的结构如下,接下来我们的重点将放在如何实现processTouchEvent这个方法来实现滑动。

private float originX = -1;
private float originY = -1;
private void processTouchEvent(float offsetX,float offsetY) {

}
@Override
public boolean onTouchEvent(MotionEvent event) {
float destX = event.getX();
float destY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
originX = destX;
originY = destY;
break;
case MotionEvent.ACTION_MOVE:
float offsetX = destX - originX;
float offsetY = destY - originY;
processTouchEvent(offsetX,offsetY);
break;
default:
break;
}
return true;
}

滑动的实现

使用layout方法

在绘制View的时候,会调用layout方法并传入left,top,right,bottom来移动View。

private void processTouchEvent(Context context,float offsetX,float offsetY) {
layout(getLeft()+offsetX,getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY);
}

使用LayoutParams

这种适用于要移动的View位于一个Layout中的情形,因为移动一个View通常是通过修改它在Parent View 中的margin来实现的。所以可以通过这种方式来实现:

private void processTouchEvent(float offsetX,float offsetY) {
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = (int) (getLeft() + offsetX);
layoutParams.topMargin = (int) (getTop() + offsetY);
setLayoutParams(layoutParams);
}

使用ScrollTo和ScrollBy

在使用这两个方法移动视图的时候,移动的不是View或者ViewGroup本身,而是View 或者ViewGroup的包含的内容,比如TextView 使用这两种方式移动的时候移动的将是TextView的文本,
而一个Layout移动的时候移动的将是里面的子View,那么我们怎样移动一个View本身,而不是仅仅其内容呢?很简单换位思考下,站在要移动View的父类的角度,它不就是父类的内容吗?
有了这个思路就好办了。

private void processTouchEvent(float offsetX,float offsetY) {
((View)getParent()).scrollBy((int)offsetX,(int)offsetY);
}

但是这里我们需要时时刻刻让自己记住,这里移动的是父类的位移,而子View的移动偏移实际上是和父View移动位移的相反数。因此要达到我们的移动目标必须将上面的偏移量取相反数。

private void processTouchEvent(float offsetX,float offsetY) {
((View)getParent()).scrollBy(-(int)offsetX,-(int)offsetY);
}

使用Scroller

Scroller存在的意义就是为了避免ScrollTo和ScrollBy带来的瞬间移动的效果,它的移动效果会更加平滑。

  • 创建Scroller
    mScroller = new Scroller(context);
  • 覆写computeScroll方法
    computeScroll 方法是什么时候执行的呢?我们知道在调用invalidate会调用onDraw来重绘当前的页面,在onDraw方法中会调用computeScroll
    下面是一个典型的实现,首先调用computeScrollOffset来判断时候完成整个滑动,如果返回true,则表示滑动过程还没结束。我们需要继续滑动
    @Override
    public void computeScroll() {
    super.computeScroll();
    //If it returns true, the animation is not yet finished.
    if(mScroller.computeScrollOffset()) {
    ((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
    invalidate();
    }
    }
  • 调用startScroll启动滑动过程:
    case MotionEvent.ACTION_UP:
    mScroller.startScroll(xpos,ypos,((View)getParent()).getScrollX(),((View)getParent()).getScrollY());
    invalidate();

由于这种方法十分重要所以在这里将对Scroller常用的方法做个总结:

mScroller.getCurrX() //获取mScroller当前水平滚动的位置
mScroller.getCurrY() //获取mScroller当前竖直滚动的位置
mScroller.getFinalX() //获取mScroller最终停止的水平位置
mScroller.getFinalY() //获取mScroller最终停止的竖直位置
mScroller.setFinalX(int newX) //设置mScroller最终停留的水平位置,没有动画效果,直接跳到目标位置
mScroller.setFinalY(int newY) //设置mScroller最终停留的竖直位置,没有动画效果,直接跳到目标位置
//开启滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量, duration为完成滚动的时间
mScroller.startScroll(int startX, int startY, int dx, int dy) //使用默认完成时间250ms
mScroller.startScroll(int startX, int startY, int dx, int dy, int duration)
第一个参数是起始移动的x坐标值, 正值表明滚动将向左滚动
第二个是起始移动的y坐标值, 正值表明滚动将向上滚动
dx 水平方向滑动的距离,正值会使滚动向左滚动
dy 垂直方向滑动的距离,正值会使滚动向上滚动
mScroller.computeScrollOffset() //返回值为boolean,true说明滚动尚未完成,false说明滚动已经完成。这是一个很重要的方法,通常放在View.computeScroll()中,用来判断是否滚动是否结束。
Contents
  1. 1. 滑动原理
  2. 2. 滑动的实现
    1. 2.1. 使用layout方法
    2. 2.2. 使用LayoutParams
  3. 3. 使用ScrollTo和ScrollBy
  4. 4. 使用Scroller