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

*对坐标系

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

%title插图%num

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

视图坐标系

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

%title插图%num

几个容易混淆的方法:

getX():视图坐标系点的X坐标
getRawX():*对坐标系点的X坐标
getLeft():视图坐标系View左边框距离ViewGroup左边框距离
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往相反方向移动,也就是右(下/左/上),所以偏移量就是相反的(负的)。下面贴上一张图,感受一下。

%title插图%num

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

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

(1)创建Scroller类

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

mScroller = new Scroller(context);
1
(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;
}
————————————————