前言

 View的滑动对于View交互性及效果有很大影响,我们可以通过以下四种方式来实现View的滑动,准确地说是View位置的改变。要改变View的位置,首先我们需要了解Android的坐标系,因为View的是通过坐标来定位的。

绝对坐标系

 Android系统中,屏幕的最左上角为坐标原点,如下图所示。

Android模拟滑动 android实现界面滑动_Android模拟滑动

屏幕最左上角的点为坐标原点,向右向下分别为x轴和y轴

视图坐标系

 视图坐标系是在View的层级体系中使用到的,View的父布局最左上角为坐标原点,向右向下为x轴和y轴,如下图所示:

Android模拟滑动 android实现界面滑动_x_02

几个容易混淆的方法:

  1. getX():视图坐标系点的X坐标
  2. getRawX():绝对坐标系点的X坐标
  3. getLeft():视图坐标系View左边框距离ViewGroup左边框距离
  4. getTranslationX():View的偏移量,初始为0,当View发生平移时,其值会变,向右为正,向左为负。

其中view.getX() = view.getLeft() + view.getTranslationX(),而get*Y同理。

1. 通过改变View的布局位置

 View的layout方法用来将View放到布局的合适位置,我们可以通过这个方法改变它的left,top,right,bottom参数的值来改变它在布局中的位置。在此基础上,如果我们在用户手指移动的过程中不断地改变View的位置就可以让View跟随手指移动。要实现View跟随用户手指滑动,我们可以监听用户手指的动作(按下,移动,。。。)计算偏移量,通过layout改变View的位置即可。

如下代码则通过layout实现View跟随手指滑动(重写View的onTouchEvent方法)

private int lastX;
private int lastY;

@Override
public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            Log.d(TAG,"onTouchEvent() down");
            lastX = x;
            lastY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            Log.d(TAG,"onTouchEvent() move");
            int offX = x - lastX;
            int offY = y -lastY;
            layout(getLeft()+offX,getTop()+offY,getRight()+offX,getBottom()+offY);
            break;
    }
    return true;
}

这里通过相对坐标计算偏移量完成View的滑动,还可以通过绝对坐标计算偏移量,代码如下:

private int lastRawX;
private int lastRawY;

@Override
public boolean onTouchEvent(MotionEvent event) {
    int rawX = (int) event.getRawX();
    int rawY = (int) event.getRawY();
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            Log.d(TAG,"onTouchEvent() down");
            lastRawX = rawX;
            lastRawY = rawY;
            break;
        case MotionEvent.ACTION_MOVE:
            Log.d(TAG,"onTouchEvent() move");
            int offX = rawX - lastRawX;
            int offY = rawY -lastRawY;
            layout(getLeft()+offX,getTop()+offY,getRight()+offX,getBottom()+offY);
            lastRawX = rawX; // 重置坐标
            lastRawY = rawY; // 重置坐标
            break;
    }
    return true;
}

与相对坐标不同的是,使用绝对坐标需要在move事件结束后重置上一次手指的坐标值,这样才能准确地计算出偏移量。

为什么绝对坐标要重置?那是如果不重置的话每次移动都是拿新的坐标与最开始的坐标比较得到偏移量,而最开始的坐标是View的初始位置手指按下的坐标,View每次移动的偏移量应该是新位置的坐标减去上一次的坐标,所以每次移动后需要更新上一次的坐标。

除了使用View的layout方法重新布局View外,还可以使用offsetLeftAndRight和offsetTopAndBottom方法重新布局View,同样可以实现View的滑动效果。代码如下:

private int lastRawX;
private int lastRawY;

@Override
public boolean onTouchEvent(MotionEvent event) {
    int rawX = (int) event.getRawX();
    int rawY = (int) event.getRawY();
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            Log.d(TAG,"onTouchEvent() down");
            lastRawX = rawX;
            lastRawY = rawY;
            break;
        case MotionEvent.ACTION_MOVE:
            Log.d(TAG,"onTouchEvent() move");
            int offX = rawX - lastRawX;
            int offY = rawY -lastRawY;
            offsetLeftAndRight(offX);
            offsetTopAndBottom(offY);
//                layout(getLeft()+offX,getTop()+offY,getRight()+offX,getBottom()+offY);
            lastRawX = rawX;
            lastRawY = rawY;
            break;
    }
    return true;
}

还可以通过View的改变布局参数LayoutParams的leftMargin和topMargin属性值改变View的位置,实现View的滑动,代码如下:

private int lastRawX;
private int lastRawY;

@Override
public boolean onTouchEvent(MotionEvent event) {
    int rawX = (int) event.getRawX();
    int rawY = (int) event.getRawY();
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            Log.d(TAG,"onTouchEvent() down");
            lastRawX = rawX;
            lastRawY = rawY;
            break;
        case MotionEvent.ACTION_MOVE:
            Log.d(TAG,"onTouchEvent() move");
            int offX = rawX - lastRawX;
            int offY = rawY -lastRawY;
//                offsetLeftAndRight(offX);
//                offsetTopAndBottom(offY);
//                layout(getLeft()+offX,getTop()+offY,getRight()+offX,getBottom()+offY);
            ViewGroup.LayoutParams layoutParams = getLayoutParams();
            ((ViewGroup.MarginLayoutParams)layoutParams).leftMargin += offX;
            ((ViewGroup.MarginLayoutParams)layoutParams).topMargin += offY;
            setLayoutParams(layoutParams);
            lastRawX = rawX;
            lastRawY = rawY;
            break;
    }
    return true;
}

改变View的布局参数需要注意的是这个View(或ViewGroup)必须有一个父布局,同时还需要注意布局参数的类型(如LinearLayout.LayoutParams,FrameLayout.LayoutParams),不过可以使用万能的MarginLayoutParams,这样就可以不用考虑布局参数类型了。

2. 使用scrollTo和scrollBy

 View类有scrollTo和scollBy方法,它们可以改变View内容的位置,scrollTo表示移动到某个坐标点,scrollBy表示移动多少偏移量。我们可以通过scrollBy实现View跟随手指的滑动,代码如下:

private int lastRawX;
private int lastRawY;

@Override
public boolean onTouchEvent(MotionEvent event) {
    int rawX = (int) event.getRawX();
    int rawY = (int) event.getRawY();
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            Log.d(TAG,"onTouchEvent() down");
            lastRawX = rawX;
            lastRawY = rawY;
            break;
        case MotionEvent.ACTION_MOVE:
            Log.d(TAG,"onTouchEvent() move");
            int offX = rawX - lastRawX;
            int offY = rawY -lastRawY;
//                offsetLeftAndRight(offX);
//                offsetTopAndBottom(offY);
//                layout(getLeft()+offX,getTop()+offY,getRight()+offX,getBottom()+offY);
//                ViewGroup.LayoutParams layoutParams = getLayoutParams();
//                ((ViewGroup.MarginLayoutParams)layoutParams).leftMargin += offX;
//                ((ViewGroup.MarginLayoutParams)layoutParams).topMargin += offY;
//                setLayoutParams(layoutParams);
            ((View)getParent()).scrollBy(-offX,-offY);
            lastRawX = rawX;
            lastRawY = rawY;
            break;
    }
    return true;
}

这里你可能会有所迷惑,为什么调用scrollBy的是View的父View(ViewGroup)?为什么偏移量是负的?

首先scrollBy移动的是View的内容content,而不是View本身,如TextView的content为文本,ImageView的content为drawable,而ViewGroup的content是View或是ViewGroup,所以要移动当前View本身,我们就需要通过它的ViewGroup改变自己的内容从而改变View本身的位置。其次,我们真正操作的是View的父控件ViewGroup,要让View往左(上/右/下)移,应该要让ViewGroup往相反方向移动,也就是右(下/左/上),所以偏移量就是相反的(负的)。下面贴上一张图,感受一下。

Android模拟滑动 android实现界面滑动_滑动_03

3. 使用Scroller类实现View平滑移动

 Android为View的滑动提供了Scroller辅助类,它本身并不能导致View滑动,需要借助computeScroll和ScrollTo方法完成View的滑动。使用Scroller类完成View的平滑,需要通过以下三个步骤:

(1)创建Scroller类

通常在自定义View的构造方法中完成Scroller类的初始化

mScroller = new Scroller(context);

(2)重写computeScroll方法

@Override
 public void computeScroll() {
     super.computeScroll();
     // 判断Scroller滑动是否执行完毕
     if(mScroller.computeScrollOffset()){
         ((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
         // 通过重绘让系统调用onDraw,onDraw中又会调用computeScroll,如此不断循环,直到Scroller执行完毕
         invalidate();
     }
 }

这里需要注意的是computeScroll方法在onDraw中会被调用,因此需要调用invalidate方法通知View调用onDraw重绘,然后再调用computeScroll完成View的滑动,过程为invalidate->onDraw->computeScroll->invalidate->…,无限循环直到mScroller的computeScrollOffset返回false,也就是滑动完成。

(3)调用Scroller类的startScroll方法开启滚动过程

public void smoothScrollBy(int dx,int dy){
    mScroller.startScroll(mScroller.getFinalX(),mScroller.getFinalY(),dx,dy,2000);
    invalidate(); // 必须调用改方法通知View重绘以便computeScroll方法被调用。
}

接下来就开始模拟滑动过程了,重写onTouchEvent方法,代码如下:

@Override
public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            lastX = x;
            lastY = y;
            break;
        case MotionEvent.ACTION_UP:
            int offX = x - lastX;
            int offY = y -lastY;
            smoothScrollBy(-offX,-offY);
            break;
    }
    return true;
}

计算偏移量的方法和上面一样,这里实现的效果是手指离开时,View会在2秒内平滑到手指离开时的位置。

4. 使用属性动画实现View的滑动

 属性动画可以改变View的属性,那么我们可以通过属性动画改变View的x和y属性从而改变View的位置实现View的滑动,代码如下:

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void animationScroll(float dx, float dy){
    Path path = new Path();
    path.moveTo(dx,dy);
    ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(this, "x", "y", path);
    objectAnimator.start();
}

通过执行ObjectAnimator改变x和y属性,我们需要新的x和y属性值,可以通过重写onTouchEvent方法得到新的x和y属性值,代码如下:

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            lastX = x;
            lastY = y;
            break;
        case MotionEvent.ACTION_UP:
            int offX = x - lastX;
            int offY = y -lastY;
            animationScroll(getX()+offX,getY()+offY);
            break;
    }
    return true;
}