之前一直在QQ或者很多腾讯的游戏上看到过一个滚动的容器,滚到低或者滚到顶部后还可以往下拉一小短距离(一种粘性的效果),松开手后会自动弹回去,最近终于有这样的需求了,其实网上也有很多实现方案,直接copy过来用也是没有问题的,但是总得自己搞清楚原理,这样就算出了bug自己也能解决掉,下面分享下自己在实现过程中学到的一些东西!
一、scrollTo(),scrollBy(), getScrollX() 和 getScrollY()
在开始写ScrollView之前,我们先了解View的这几个方法,大家可别小看这几个东西,如果之前没有了解过的话,第一次理解起来还是蛮绕的,感觉有点反人类思维,我们先看个小例子。
1、我们先看ScrollTo(),首先我们先正常去显示一张图片,然后再去调用ScrollTo()方法,看看前后的效果
初始化效果:
调用scrollTo(100,0)后:
大家应该用过canvas的平移的方法,那么大家有没有想过,既然是平移,偏移量自然要有,有了偏移量那么自然就有参考点,所以说,canvas本身就拥有自己的坐标系,而这坐标系的原点刚好就是跟View初始化视图后的坐标原点是重合的。而scrollTo(100,0)的意义就是指将canvas上坐标为(100,0)的点显示到View的坐标原点上来,这就是为什么反方向移动了!
2、那么getScrollX()跟scrollTo()又有什么关系呢?
看scrollTo(),getScrollX的源码:
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();
}
}
}
public final int getScrollX() {
return mScrollX;
}
我们看到,x,y两个值会被保存到mScrollX、mScrollY中,而getScrollX()返回的就是他们。。关系不言而喻
3、那么接下来我们看下scrollBy()
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
这就清楚了吧,内部还是调用了scrollTo(),只不过每次都加上上一次保存的值,也就是说每掉一次都会在上一次的基础上进行移动
二、好了,我们为什么要说scrollTo()?因为这就是ScrollView的滚动原理,后面对临界值的判断需要用到,下面我们分析下实现思路
三、下面是完整的代码,注释都有了
**
* Created by server on 18-5-10.
*/
public class LazyScrollView extends ScrollView {
/**
* 子View
*/
private View mChild;
/**
* 触摸点在Y轴方向上的坐标
*/
private float mTouchY;
/**
* 用于保存子View开始调用layout重新布局前的位置信息
* 确保松手后能恢复原来的位置
*/
private Rect mOldRect = new Rect();
/**
* 用于标识当前动画是否正在执行
*/
private boolean mAnimation = false;
/**
* 动画执行时间
*/
private static final int DURATION = 200;
private int mOffest;
public LazyScrollView(Context context) {
this(context, null);
}
public LazyScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LazyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() > 0) {
mChild = getChildAt(0);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mOffest = getMeasuredHeight()/2;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mChild == null) {
return super.onTouchEvent(ev);
} else {
if (mAnimation) {
return super.onTouchEvent(ev);
}
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mTouchY = ev.getY();
break;
case MotionEvent.ACTION_UP:
mTouchY = 0;
if (!mOldRect.isEmpty()) {
runAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
//此处为了防止子View还没完全复原,又快速触摸屏幕时出现跳屏现象
final float preY = mTouchY == 0 ? ev.getY() : mTouchY;
//获取滑动后的位置
float moveY = ev.getY();
//计算当前位置与上一个位置的差值
int diffY = (int) (preY - moveY + 0.5f);
//更新位置信息
mTouchY = moveY;
/**
* 这里获取ScrollView的底部与子View底部的差值
* 如果这个差值等与getScrollY()的是那就证明已经滚动到了底部临界点
*/
int offset = mChild.getMeasuredHeight() - getHeight();
/**
* 该值等于0,证明滚动到了顶部
*/
int scrollY = getScrollY();
if (scrollY == 0 || scrollY == offset) {
if (mOldRect.isEmpty()) {
/**
* 保存原有的位置信息
*/
mOldRect.set(mChild.getLeft(), mChild.getTop(), mChild.getRight(), mChild.getBottom());
}
//随着移动的增加,逐渐增加粘性效果
int df = 2;
if (Math.abs(mChild.getTop())>mOffest/3){
df = 3;
}else if (Math.abs(mChild.getTop())>mOffest/2){
df = 4;
}else if (Math.abs(mChild.getTop())>mOffest){
df = 5;
}
/**
* 重新进行布局操作
*/
mChild.layout(mChild.getLeft(), mChild.getTop() - diffY/df, mChild.getRight(), mChild.getBottom() - diffY/df);
} else {
super.onTouchEvent(ev);
}
break;
default:
break;
}
}
return super.onTouchEvent(ev);
}
/**
* 恢复View的位置时的动画实现
*/
public void runAnimation() {
TranslateAnimation ta = new TranslateAnimation(0, 0, 0, mOldRect.top - mChild.getTop());
ta.setDuration(DURATION);
ta.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
mAnimation = true;
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
mChild.clearAnimation();
mChild.layout(mOldRect.left, mOldRect.top, mOldRect.right, mOldRect.bottom);
mOldRect.setEmpty();//清除位置信息
mAnimation = false;
}
});
mChild.startAnimation(ta);
}
}
ok下面看下效果图