效果:
主要的几个知识点有 Scroller 、VelocityTracker
主要操作View里面的几个方法有 onMeasure、onTouchEvent 、computeScroll 、scrollTo() 、scrollBy()
Scroller
是一个专门用于处理滚动效果的工具类,直接调用ScrollTo()或者ScrollBy()的方式来移动的话是瞬间完成,用户体验感觉不是很好,然后使用Scroller就可以有个一个种平滑的效果
使用的时候也可传入自定义的插值器,常用的插值器有
AccelerateDecelerateInterpolator 在动画开始与介绍的地方速率改变比较慢,在中间的时候加速
AccelerateInterpolator 在动画开始的地方速率改变比较慢,然后开始加速
AnticipateInterpolator 开始的时候向后然后向前甩
AnticipateOvershootInterpolator 开始的时候向后然后向前甩一定值后返回最后的值
BounceInterpolator 动画结束的时候弹起
CycleInterpolator 动画循环播放特定的次数,速率改变沿着正弦曲线
DecelerateInterpolator 在动画开始的地方快然后慢
LinearInterpolator 以常量速率改变
OvershootInterpolator 向前甩一定值后再回到原来位置(快速完成动画,超出再回到结束样式)
系统默认是 ViscousFluidInterpolator
用法:
Scroller mScroller = new Scroller(context, new OvershootInterpolator());
相应方法解释:
mScroller.getCurrX() //获取mScroller当前水平滚动的位置
mScroller.getCurrY() //获取mScroller当前竖直滚动的位置
mScroller.forceFinished(true); //停止一切滑动
mScroller.computeScrollOffset()//判断是否还在滑动中, true滑动中 ,false滑动完成
mScroller.startScroll //设置滑动 ,执行这个方法,之后执行invalidate()才会触发View里面的computeScroll方法的回调的
/**
*开始滚动只要提供一个开始位置和结束位置,滚动将使用默认值为250毫秒的持续时间。
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
*/
public void startScroll(int startX, int startY, int dx, int dy) {
}
感觉有点不好理解,要先知道,滑动的开始都是左上角(0,0),最开始时的状态也就是刚刚说的(0,0),向下滑动getScrollY()的值是负数,向上滑动是正数
mScroller.fling() //惯性滑动的,需要配合VelocityTracker.getYVelocity();来获取初速度
/**
* fling 方法参数注解
*
* startX 滚动起始点X坐标
* startY 滚动起始点Y坐标
* velocityX 当滑动屏幕时X方向初速度,以每秒像素数计算
* velocityY 当滑动屏幕时Y方向初速度,以每秒像素数计算
* minX X方向的最小值,scroller不会滚过此点。
* maxX X方向的最大值,scroller不会滚过此点。
* minY Y方向的最小值,scroller不会滚过此点。
* maxY Y方向的最大值,scroller不会滚过此点。
*/
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {
}
用法:
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
float yVelocity = mVelocityTracker.getYVelocity(pointerId);
mScroller.fling(0, getScrollY(), 0, (int) -yVelocity * 2, 0, 0, 0, measuredHeight - height);
VelocityTracker
追踪触摸事件速率,实现flinging和其他手势的帮助类
- 当开始追踪的时候,使用obtain来获取VelocityTracker类的实例
- 把接收到的MotionEvent放入到addMovement(android.view.MotionEvent)中
- 当要确定速度时调用computeCurrentVelocity(int),使用getXVelocity(int)和getYVelocity(int)来检测每个触摸点id的速率
onMeasure
主要用于测量控件的大小,刚开始直接用getMeasuredHeight()获取到的高度,其实这个是当前内容可见区域高度,全部内容的滚动长度需要计算的,如下,计算好所有子控件的高度之后需要调用setMeasuredDimension更改高度
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//测量子控件的大小
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams layoutParams = (MarginLayoutParams) childView.getLayoutParams();
measuredHeight += (childView.getMeasuredHeight() + layoutParams.bottomMargin + layoutParams.topMargin);
}
//调用此方法 重新更改高度
setMeasuredDimension(getMeasuredWidth(), measuredHeight);
computeScroll
用scollTo/scollBy/startScroll方法来进行滑动时,都需要执行invalidate()才会触发其的回调,从而才会看到效果
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
下面是全部代码
public class ScrollerViewLayout extends LinearLayout {
private int measuredHeight;//全部item高度
private int height; //可见内容高度
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
private float mMaxVelocity;
public ScrollerViewLayout(Context context) {
this(context, null, 0);
}
public ScrollerViewLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScrollerViewLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOrientation(VERTICAL);
mScroller = new Scroller(context, new OvershootInterpolator());
ViewConfiguration vc = ViewConfiguration.get(getContext());
mMaxVelocity = vc.getScaledMaximumFlingVelocity();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measuredHeight = 0;
//得到控件原始显示高度
height = getMeasuredHeight();
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//测量子控件的大小
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams layoutParams = (MarginLayoutParams) childView.getLayoutParams();
measuredHeight += (childView.getMeasuredHeight() + layoutParams.bottomMargin + layoutParams.topMargin);
}
//调用此方法 重新更改高度
setMeasuredDimension(getMeasuredWidth(), measuredHeight);
}
private float downY;
private int pointerId;
private boolean isSilde = false;
@Override
public boolean onTouchEvent(MotionEvent event) {
handlerScroll(event);
return true;
}
/**
* 处理滚动事件
* @param event
*/
private void handlerScroll(MotionEvent event) {
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
pointerId = event.getPointerId(0);
//停止一切滚动
mScroller.forceFinished(true);
mVelocityTracker.clear();
mVelocityTracker.addMovement(event);
downY = y;
break;
case MotionEvent.ACTION_MOVE:
mVelocityTracker.addMovement(event);
int move = (int) (downY - y);
if (isSilde || move == 0) {
return;
}
//向下滑动
if (move < 0) {
scrollBy(0, move);
downY = y;
}
//向上滑动
else if (move > 0) {
scrollBy(0, move);
downY = y;
}
Log.e("kawa", ">>>>move:" + move
+ ">>>downY:" + downY
+ ">>>y:" + y
+ ">>>height:" + height
+ ">>>measuredHeight:" + measuredHeight
+ ">>>getScrollY:" + getScrollY());
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if ((measuredHeight - height) < getScrollY() || getScrollY() < 0) {
scrollReset();
} else {
scrollFling();
}
break;
}
}
@Override //每次执行draw都会执行,获取当前的滚动位置进行重绘制
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
isSilde = true;
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
} else {
isSilde = false;
}
}
/**
* 超出顶部/底部的进行复位
*/
private void scrollReset() {
int scrollY = getScrollY();
if (scrollY < 0) {
int startY = scrollY;
int endY = -scrollY;
mScroller.startScroll(0, startY, 0, endY);
invalidate();
} else {
//向上滑动超出底部界限时才进行复位
if ((measuredHeight - height) < getScrollY()) {
int startY = scrollY;
int endY = -(scrollY - (measuredHeight - height));
mScroller.startScroll(0, startY, 0, endY);
invalidate();
}
}
}
/**
* 惯性滚动
*/
private void scrollFling() {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
float yVelocity = mVelocityTracker.getYVelocity(pointerId);
/**
* fling 方法参数注解
*
* startX 滚动起始点X坐标
* startY 滚动起始点Y坐标
* velocityX 当滑动屏幕时X方向初速度,以每秒像素数计算
* velocityY 当滑动屏幕时Y方向初速度,以每秒像素数计算
* minX X方向的最小值,scroller不会滚过此点。
* maxX X方向的最大值,scroller不会滚过此点。
* minY Y方向的最小值,scroller不会滚过此点。
* maxY Y方向的最大值,scroller不会滚过此点。
*/
mScroller.fling(0, getScrollY(), 0, (int) -yVelocity * 2, 0, 0, 0, measuredHeight - height);
invalidate();
}
}
这里有张图可以更好的理解使用Scroller (图来源网络)