一.实现的功能
1.ScrollView的滚动效果
2.边界处理及回弹
3.事件处理,防止子view消费。
二.基本知识点
1.Scroller的方法
/**填写初始的坐标和要滚动的距离,dx为正,则向左,dx为负,那么则向右移动。最后一个参数是动画执行的时间默认是250毫秒。
当在computeScroll()中调用computeScrollOffset时候,其实是将在startScroll()中传递的dx或是dy按照调用的时间
差按照250毫秒进行平分。然后通过scrollTo()进行平移。其中getCurrX()和getCurrY()就是获取平分的距离。
*/
scroller.startScroll(int startX, int startY, int dx, int dy, int duration) ;
scroller.computeScrollOffset()//如果返回true,那么证明动画还没执行完
/** velocityX 当滑动屏幕时X方向初速度,以每秒像素数计算
velocityY 当滑动屏幕时Y方向初速度,以每秒像素数计算
**/
scroller.fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY);
scroller.forceFinished(boolean finished);强制停止
scroller.getCurrVelocity();// 获取当前的速度
scroller.getCurrX();//这是获得移动距离除以默认时间后的平均距离,因此使用Scroller时候有一个平滑的滚动效果
scroller.getCurrY();//这是获得移动距离除以默认时间后的平均距离,因此使用Scroller时候有一个平滑的滚动效果
scroller.extendDuration();//设置滚动的时间
使用的时候不要忘记重写方法,基本写法是固定的,就是获取x或y方向的移动距离的平均值,然后进行移动,由于不停的调用这个方法,因此产生平滑滚动的效果。
@Override
public void computeScroll() {
super.computeScroll();
//如果scroller还没停止,那么就还进行不停的绘制
if (mScroller.computeScrollOffset()) {
//注意这里的getCurrY()的源码获取的是进行微移后的当前的坐标,不是相对距离
scrollTo(0, mScroller.getCurrY());
postInvalidate();
}
}
2.Scroller的使用场景
①滑动到边界的回弹效果
②快速滑动的fling效果 (但是我实现的效果感觉没有真正的scrollView流畅)
3.event.getX() event.getY() getScrollX() getScrollY() 的区别
event.getX() event.getY()是获取的手指的点击的位置,它的作用仅仅是计算手指一动的距离,或是计算手指滑动速度。
getScrollX() getScrollY()是又ScrollTo()进行直接的赋值(或是scrollBy() ,本质也是调用了scrollTo()),因此获取的是当前子view移动的距离,注意方向,getScrollY()为负值,就是向下移动。
4.速度识别器的使用
public class MyScrollView extends LinearLayout {
private Scroller mScroller;
//整个的ScrollView的高度
private int scrollHeight = 0;
private float lastX = 0;
private float lastY = 0;
/**
* 在自定义view中经常使用的是重新绘制
* 在viewGroup中经常使用scrollBy等进行移动
*/
private int windowHeight;
//这是进行记录横向或者竖向是否进行滑动了
private float moveX = 0;
private float moveY = 0;
private VelocityTracker mVelocityTracker;
public MyScrollView(Context context) {
this(context, null);
}
public MyScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScroller = new Scroller(context);
init();
}
public void init() {
this.setOrientation(LinearLayout.VERTICAL);
}
// 如果是继承了LinearLayout 那么就没必要进行测量高度和摆放布局,仅仅进行计算滑动的距离就可以了。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
scrollHeight = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//添加上所有view的margin,获取整个子view的高度
scrollHeight += child.getMeasuredHeight() + lp.bottomMargin + lp.topMargin;
}
//获取ScrollView的高度
windowHeight = getMeasuredHeight();
Log.e("TAG", "MyScrollView---scrollHeight---" + scrollHeight + " windowHeight----" + windowHeight);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// Log.e("TAG", "MyScrollView---dispatchTouchEvent---" + ev.getAction());
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = moveX = ev.getX();
lastY = moveY = ev.getY();
mScroller.forceFinished(true);
if(mVelocityTracker==null){
mVelocityTracker=VelocityTracker.obtain();
}else{
mVelocityTracker.clear();
}
mVelocityTracker.addMovement(ev);
break;
case MotionEvent.ACTION_MOVE:
Log.e("TAG", "MyScrollView---getScrollY====" + getScrollY());
mVelocityTracker.addMovement(ev);
float curX = ev.getX();
float curY = ev.getY();
//垂直滑动的时候进行移动
if (Math.abs(curY - lastY) > Math.abs(curX - lastX)) {
scrollBy(0, -(int) (curY - lastY));
// 或者使用下面的方法,因为设置了平滑过渡时间为0
// mScroller.startScroll(0,getScrollY(),0,-(int)(curY-lastY),0);
invalidate();
}
lastX = curX;
lastY = curY;
break;
case MotionEvent.ACTION_UP:
//这里做了一个回弹的效果,在第二个参数中设置了滚动了多少距离,然后dy为它的负值,立马就回弹回去
//这里肯定不能传递ev.getX(),因为getX()是获取的手指点击的位置,因此一定要使用getScrollY(),这是获取的滚动后的距离。
//这里getScrollY()是在scrollTo()或scrollBy()中进行赋值。因此要调用这个方法,一定要先调用这两个方法。
//startScroll()方法不适合在action_move中调用,因为这个方法默认的时间就是250毫秒,频繁的使用postInvalidate()进行刷新,就会导致移动动作的
//覆盖,反而出现很难移动的效果。因为action_move的回调很快,每个十几像素就回调了。如果将startScroll()的第五个参数设置为0,也就是间隔时间设置为0
//mScroller.startScroll(0,(int)getScrollY(),0,-(int)(curY-lastY),0); 那么就出现了和scrollBy()相似的效果
mScroller.abortAnimation();
if (getScrollY() < 0) {//证明达到上边界了,这时候要进行回弹处理
mScroller.startScroll(0, getScrollY(), 0, -getScrollY());
postInvalidate();// 重绘执行computeScroll()
} else if (windowHeight + getScrollY() > scrollHeight) {//达到最底部
mScroller.startScroll(0, getScrollY(), 0, -(getScrollY() - (scrollHeight - windowHeight)));
postInvalidate();// 重绘执行computeScroll()
} else {
mVelocityTracker.computeCurrentVelocity(1000);
float yVelocity = mVelocityTracker.getYVelocity();
// Log.e("TAG", "MyScrollView---yVelocity====" + yVelocity);
/**
* fling 方法参数注解
* startX 滚动起始点X坐标
* startY 滚动起始点Y坐标
* velocityX 当滑动屏幕时X方向初速度,以每秒像素数计算
* velocityY 当滑动屏幕时Y方向初速度,以每秒像素数计算
* minX X方向的最小值,scroller不会滚过此点。
* maxX X方向的最大值,scroller不会滚过此点。
* minY Y方向的最小值,scroller不会滚过此点。
* maxY Y方向的最大值,scroller不会滚过此点。
*/
if (Math.abs(yVelocity) > 50) {
// mScroller.extendDuration(2000);
mScroller.fling(0, getScrollY(), 0, -(int) yVelocity,
0, 0,
0, (scrollHeight - windowHeight));
postInvalidate();
}
}
//进行计算移动的距离
moveX = Math.abs(ev.getX() - moveX);
moveY = Math.abs(ev.getY() - moveY);
//如果横向或者竖向已经移动了一段距离,那么就不能响应子控件的点击事件
if (moveY > 10) {
return true;
}
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// Log.e("TAG", "MyScrollView---onInterceptTouchEvent---" + ev.getAction());
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.e("TAG", "MyScrollView---onTouchEvent---" + ev.getAction());
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
//如果scroller还没停止,那么就还进行不停的绘制
if (mScroller.computeScrollOffset()) {
Log.e("TAG", "MyScrollView---computeScroll---");
//注意这里的getCurrY()的源码获取的是进行微移后的当前的坐标,不是相对距离
scrollTo(0, mScroller.getCurrY());
postInvalidate();
}
}}
具体效果请看demo:
https://github.com/yunzheyue/myScrollView