掌握滑动的方法是实现绚丽的自定义控件的基础。实现View的三种滑动方式:
1.通过View自身提供的scrollTo/scrollBy方法。
2.通过动画给View施加水平移动效果。
3.通过改变View的LayouParams使得View重新布局从而实现滑动。
使用scrollTo/scrollBy
Set the scrolled position of your view. This will cause a call to onScrollChanged(int, int, int, int) and the view will be invalidated.
Parameters:x the x position to scroll toy the y position to scroll to
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
Move the scrolled position of your view. This will cause a call to onScrollChanged(int, int, int, int) and the view will be invalidated.
Parameters:x the amount of pixels to scroll by horizontallyy the amount of pixels to scroll by vertically
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
可以看出,scrollBy实际上也是调用了scrollTo方法来实现。不同的是,scrollBy实现了基于当前位置的相对滑动,而scrollTo则实现了基于所传递参数的绝对滑动。这样说两者的区别显得非常的抽象,那到底它们的区别是什么呢?在这之前,先了解一下View内部的两个属性:mScrollX和mScrollY
The offset, in pixels, by which the content of this view is scrolled horizontally.
@ViewDebug.ExportedProperty(category = "scrolling")
protected int mScrollX;
The offset, in pixels, by which the content of this view is scrolled vertically.
@ViewDebug.ExportedProperty(category = "scrolling")
protected int mScrollY;
mScrollX和mScrollY分别表示的是View内容在X轴和Y轴方向的偏移量,单位是像素。注意这里都是指的View内容而不是View本身的移动。那这个偏移量怎么计算呢?假设有一个View的起始位置是(0,0),这里的(0,0)指的是View的左上角的坐标,现在需要向右偏移100个像素即需要使View的坐标为(100,0),那么偏移量就是(0,0) - (100,0) = -100。这里也可以看出:View从坐往右滑动,mScrollX为负值,反之为正值。同理,View从上往下滑动,mScrollY为负值,反之为正值。
绝对滑动与相对滑动:可以看到,源码scrollTo方法中的参数x和y在代码中直接赋值给了mScrollX和mScrolly,偏移量的值就是以传入的为准。看如下代码:
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mButton.scrollTo(-100,-100);
mButton.scrollTo(-30,-30);
Log.i(TAG,"偏移量: " + mButton.getScrollX());
}
});
mButton执行了两次scrollTo方法,那么获取的最后的偏移量是多少呢?没错,就是-30。也就是说,scrollTo方法的参照点始终都是(0,0),而不管之前是否已经滑动过,并且它的滑动结果也是和最后一次调用scrollTo的一致。
再看如下代码:
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mButton.scrollTo(-100,-100);
mButton.scrollBy(-30,-30);
Log.i(TAG,"偏移量: " + mButton.getScrollX());
}
});
mButton在执行了一次scrollTo方法后再去执行scrollBy方法,那么获取的偏移量是多少呢。没错就是-130。可以看到再scrollBy方法的源码中,传入的x和y是和之前的偏移量进行了加运算,也就是说,scrollBy的相对滑动参照点是前一次滑动后的点。
那么如果先调用scrollBy方法然后调用scrollTo方法的结果是如何呢?经试验证明,结果还是以最后一次的scrollTo的滑动为准。那么是否能有这样的一个结论:关于View的scrollBy和scrollTo的滑动,如果最后一次滑动动作是通过scrollTo完成的,而不管之前的滑动如何,都是以最后一次scrollTo的结果为最后的滑动结果的呢?
回顾上面分析,需要注意的一点就是这里所说的滑动都是指View的内容的滑动,而View本身是不会移动的。
使用动画
涉及View的滑动的话,其实就是动画中的平移。而动画又有View动画和属性动画,现在都是推荐使用属性动画,两者的区别简单的说就是View动画并不是在真正的移动View而是产生View的影像而已。属性动画需要在3.0以上才支持,不过现在的用户基本上都是3.0以上了,如果需要兼容3.0以下的就需要使用nineoldandroids开源动画库。
属性动画的使用:
// 100ms内向右平移100像素
ObjectAnimator.ofFloat(mButton,"translationX",0,100).setDuration(100).start();
改变布局参数
改变布局参数就是改变LayouParams,例如我们需要将一个Button像右平移100个像素,就只需要将其marginLeft的值增加100,或者先在Button的左边放置一个宽度为0的View,在Button需要平移的时候改变View的宽度即可。
改变marginLeft的方式:
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mButton.getLayoutParams();
params.leftMargin += 100;
mButton.requestLayout(); // 或者mButton.setLayoutParams(params);
三种方式对比
1. scrollto/scrollBy:操作简单,适合对View内容的滑动;
2. 动画:操作简单,主要使用于没有交互的View和实现复杂的动画效果;
3. 改变布局参数:操作稍微复杂,适用于有交互的View。
接下来实现一个跟随着手滑动的View,我们只需要获取该View在屏幕中的位置,在滑动之后获取与之前坐标的差即是平移的距离。每次滑动之后改变View的位置。
public class MyView extends TextView {
// 记录上次View的位置
private int mLastX = 0;
private int mLastY = 0;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context) {
super(context);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
// 获取两次滑动之间的位移
int delaX = rawX - mLastX;
int delaY = rawY - mLastY;
int translationX = (int)ViewHelper.getTranslationX(this) + delaX;
int translationY = (int)ViewHelper.getTranslationY(this) + delaY;
ViewHelper.setTranslationX(this,translationX);
ViewHelper.setTranslationY(this,translationY);
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = rawX;
mLastY = rawY;
return true;
}
}