在之前的文章中,已经介绍过View的坐标系,滑动等相关内容。今天结合一个具体的例子来演示一下。
首先介绍一下Scroller。
Scroller
Scroller是一个滑动的辅助类,它主要包含以下内容
startScroll(int startX, int startY, int dx, int dy, int duration)
startX: 滑动开始的X位置
dx: 将要滑动的距离
duration:在多长时间内完成滑动getScrollX(),getScrollY()
获取滑动距离
需要注意的是,在Scroll相关的方法中,包括scrollTo,scrollBy,getScrollX(),startScroll,他们的参数是有方向的,与View的坐标系不同,向右,向下为负值,反之为正值。
举个例子:
首先通过scrollTo(100,50)将View的内容滑动,View的位置会向左,上移动 100,50 个像素。此时,通过getScrollX/Y()获取到的值,就是 100 , 50
然后通过scrollBy(-20, -10)再移动一下View,此时View会向右,下移动 20,10 个像素,然后通过getScrollX/Y()获取到的值为100-20=80,50-10=40,View的内容位于初始位置的左上方。
简单的总结一下:
getScrollX/Y()得到的是当前View的内容与View的初始位置之间的距离,右下为负,反之为正。
startScroll(int startX, int startY, int dx, int dy, int duration):
startX/Y表示滑动开始的位置(通常由getScrollX/Y()得到)
dX/Y表示将要滑动的距离,右下为负,反之为正。
稍后会在具体的例子中演示。
invalidate();
invalidate();是View中的方法,会调用draw,进行View重绘,跟在startScroll方法之后。startScroll实际上只是告诉View怎样滑动,invalidate()之后才会开始滑动。重写computeScroll()
这个方法才是实现弹性滑动的关键。弹性滑动,可以理解为平滑的移动,我们通过scrollTo/By,实现的滑动是一下就完成的,弹性滑动是缓慢平滑的移动。computeScroll()是怎样实现弹性滑动的呢?
看一下代码就明白了:
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
computeScroll是在draw方法中调用的,startScroll之后的invalidate会调用draw方法,draw又调用了computeScroll,if里的条件mScroller.computeScrollOffset()是判断滑动是否完成,如果没有完成,就会调用scrollTo,将view滑动到当前位置,然后再postInvalidate继续调用draw方法,最终将View一点一点移动到目标位置。
实例演示
我们要实现的是一个很简单的例子,自定义一个圆形,当手指按下的时候,圆形移动到手指的位置,手指移动的时候,圆形会跟随手指移动,抬起手指的时候,圆形慢慢的回复到初始位置。
代码如下:
public class FollowFingerView extends View{
//绘制圆形
private Paint mPaint;
//绘制背景
private Paint mBackGroundPaint;
//View的宽和高
private int mWidth;
private int mHeight;
//定义Scroller
private Scroller mScroller;
//存储上次View的位置参数
private int mLastX;
private int mLastY;
public FollowFingerView(Context context) {
this(context,null);
// TODO Auto-generated constructor stub
}
public FollowFingerView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
// TODO Auto-generated constructor stub
}
private void init(){
mPaint = new Paint();
//设置圆形颜色
mPaint.setColor(0x22ff0000);
mBackGroundPaint=new Paint();
//设置背景颜色
mBackGroundPaint.setColor(0xfff8efe0);
mScroller = new Scroller(getContext());
//设置默认宽高,wrap_content时使用
mWidth = 400;
mHeight = 400;
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
//绘制背景
canvas.drawPaint(mBackGroundPaint);
//绘制半径为30像素的圆形
int radius = 30;
canvas.drawCircle(30, 30, radius, mPaint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
//处理wrap_content失效
setMeasuredDimension(measureWidth(widthMode,width), measureHeight(heightMode,height));
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
//获取触发事件时,手指的触碰位置
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//手指按下时,移动View到触碰位置
scrollTo(-x, -y);
break;
case MotionEvent.ACTION_MOVE:
//事件发生时手指的位置与上一个事件结束时手指的位置,之间的距离
//注意方向,可能这样写更容易理解:
//int dx = -(x - mLastX);
//int dy = -(y - mLastY);
int dy = mLastY - y;
int dx = mLastX - x;
int dy = mLastY - y;
//跟随手指移动
scrollBy(dx, dy);
break;
case MotionEvent.ACTION_UP:
//手指抬起的时候,开始返回初始位置
mScroller.startScroll(getScrollX(), getScrollY(), -getScrollX(), -getScrollY(), 500);
Log.d("Follow", "getScrollX is " + getScrollX());
invalidate();
break;
default:
break;
}
//记录事件结束时手指的位置
mLastX = x;
mLastY = y;
//该View不是clickable的,返回值默认为false,应该手动改为true,否则不能消费事件,只能执行down
//return super.onTouchEvent(event);
return true;
}
@Override
public void computeScroll() {
// TODO Auto-generated method stub
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
private int measureWidth(int widthMode,int widthSize){
switch (widthMode) {
case MeasureSpec.EXACTLY:
mWidth=widthSize;
//Log.d("ViewConstructor", "mode is exactly");
break;
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST:
break;
default:
break;
}
return mWidth;
}
private int measureHeight(int heightMode,int heightSize){
switch (heightMode) {
case MeasureSpec.EXACTLY:
mHeight=heightSize;
break;
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST:
break;
default:
break;
}
return mHeight;
}
}