目录
View的滑动简介
View的位置参数
view的坐标系
- 上图圆点是手指触摸点,蓝色的是MotionEvent的方法,点击事件走到onTouchEvent,获得点击事件的各种坐标:getX、getY是相对view;getRawX、getRawY是相对屏幕。
- 绿色的是View获取坐标的方法。ViewGroup是View的父布局。getLeft、getRight、getTop、getBottom相对父布局。
width = getRight()- getLeft()
height = getTop()- getBottom()
VelocityTracker、GestureDetector
1.VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向上的速度。过程很简单,在View的onTouchEvent()方法,追踪当前的单击事件的速度。
例如相册的图片,手指快速左右滑动会切换图片,慢则不会切换。获取速度前,要先调用computeCurrentVelocity计算速度,如下代码。效果是手指滑的快时,就会弹Toast。
private void init() {
//获取速度追踪器
mVelocityTracker = VelocityTracker.obtain();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
//获取触摸点坐标
int x = (int) event.getX();
int y = (int) event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - mLastX;
int offsetY = y - mLastY;
//这句是为了随手指滑动,下面会讲
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
//速度器添加事件
mVelocityTracker.addMovement(event);
break;
case MotionEvent.ACTION_UP:
//计算手指滑动速度:1000ms内滑过的像素,(终点-起点)/时间段,所以从右向左滑 为负值。
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
Log.i(TAG, "onTouchEvent: xVelocity = " + xVelocity);
if (Math.abs(xVelocity) > 100) {
Toast.makeText(getContext(), "滑的有点快!", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
2.GestureDetector
手势检测。通常监听双击才使用GestureDetector,其他的滑动就在onTouchEvent中实现(DOWN、MOVE、UP)就可以了。
private void init() {
//要设置两个监听OnGestureListener、OnDoubleTapListener
mGestureDetector = new GestureDetector(getContext(),this);
mGestureDetector.setOnDoubleTapListener(this);
}
@Override
public boolean onDown(MotionEvent e) {
//手指触摸的一瞬间,由1个DOWN触发
Log.i(TAG, "onDown: ");
return false;
}
@Override
public void onShowPress(MotionEvent e) {
//手指触摸的状态,由1个DOWN触发,强调的是没有拖动的状态,就是按着没动。
Log.i(TAG, "onShowPress: ");
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
//单击,UP触发
Log.i(TAG, "onSingleTapUp: ");
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//滚动,1个DOWN,多个MOVE触发
Log.i(TAG, "onScroll: ");
return false;
}
@Override
public void onLongPress(MotionEvent e) {
//长按
Log.i(TAG, "onLongPress: ");
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//快速move后up
Log.i(TAG, "onFling: ");
return false;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
//确认的单击,不是双击中的某一击
Log.i(TAG, "onSingleTapConfirmed: ");
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
//双击,两个单击组成。和onSingleTapConfirmed不能共存
Log.i(TAG, "onDoubleTap: ");
return false;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
//发生了双击行为,双击期间DOWN、MOVE、UP都会触发此回调
Log.i(TAG, "onDoubleTapEvent: ");
return false;
}
View的滑动
1.视图坐标方法(offsetLeftAndRight())
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录触摸点坐标
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
//当前left、top、bottom的基础上加上偏移量
//将计算出来的偏移量递给layout(),完成view的移动
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
break;
}
return true;
}
2. 绝对坐标方式(layout())
view进行绘制的时候会调用onLayout()方法来设置显示的位置,因此我们同样也可以通过修改View的left、top、right、bottom这四种属性来控制View的坐标。
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX=(int) event.getRawX();
int rawY=(int) event.getRawY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
//记录触摸点坐标
lastx=rawX;
lasty=rawY;
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量
int offsetX=rawX-lastx;
int offsetY=rawY-lasty;
//在当前的left、top、right、bottom的基础上加上偏移量
layout(getLeft()+offsetX,
getTop()+offsetY,
getRight()+offsetX,
getBottom()+offsetY);
//重新设置初始坐标
lastx=rawX;
lasty=rawY;
break;
}
return true;
}
3. LayoutParams()方法
改变布局参数,比如我们想把一个Button向右平移100px,我们只需要将这个Button的LayoutParams里的marginLeft参数的值增加100px即可。
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录触点坐标
lastX = (int) event.getX();
lastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
}
return true;
}
4.scrollTo/scrollBy方法
View提供了专门的方法来实现滑动,即scrollTo()和scrollBy()
@Override
public boolean onTouchEvent(MotionEvent event) {
int x=(int) event.getX();
int y=(int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
//记录触点坐标
lastX=(int )event.getX();
lastY=(int)event.getY();
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量
int offsetX=x-lastX;
int offsetY=y-lastY;
((View)getParent()).scrollTo(-offsetX,-offsetY);
break;
}
return true;
}
5. Scroller()
我们用scollTo/scollBy方法来进行滑动时,这个过程是瞬间完成的,所以用户体验不大好。这里我们可以使用Scroller来实现有过度效果的滑动,这个过程不是瞬间完成的,而是在一定的时间间隔完成的。Scroller本身是不能实现View的滑动的,它需要配合View的computeScroll()方法才能弹性滑动的效果。
- 初始化Scroller
private void ininView(Context context) {
setBackgroundColor(Color.BLUE);
// 初始化Scroller
mscroller = new Scroller(context);
}
- 重写computeScroll()方法,系统会在绘制View的时候在draw()方法中调用该方法,这个方法中我们调用父类的scrollTo()方法并通过Scroller来不断获取当前的滚动值,每滑动一小段距离我们就调用invalidate()方法不断的进行重绘,重绘就会调用computeScroll()方法,这样我们就通过不断的移动一个小的距离并连贯起来就实现了平滑移动的效果。
@Override
public void computeScroll() {
super.computeScroll();
//判断Scroller是否执行完毕
if (mscroller.computeScrollOffset()) {
((View) getParent()).scrollTo(
mscroller.getCurrX(),
mscroller.getCurrY());
//通过重绘不断来调用computeScroll
invalidate();
}
}
- 调用Scroller.startScroll()方法。
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = (int) event.getX();
lastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
((View) getParent()).scrollTo(-offsetX, -offsetY);
break;
case MotionEvent.ACTION_UP:
//手指离开时,执行滑动过程
View viewgroup = (View) getParent();
mscroller.startScroll(viewgroup.getScrollX(),
viewgroup.getScrollY(),
-viewgroup.getScrollX(),
-viewgroup.getScrollY());
invalidate();
break;
}
return true;
}
- 源码分析
Scroller使用原理分析
分析:如上的主activity中的调用:我们知道Scroller的主要入口功能在其对象的startScroll()方法。所以我们便从这开始看源码
A. StartScroller源码
首先进入startscroller,看到这里只是进行的包装
public void startScroll(int startX, int startY, int dx, int dy) {
startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
}
继续进去startscroll源码
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
看到这里我们知道Scroller内部其实啥也没干,只是保存了我们传递来的参数(把我们传递来的参数用新的值接收)
- startX、startY表示滑动的起点。
- dx、dy表示滑动的距离。
- duration表示滑动事件,整个滑动过程所需时间。
我们看出仅仅调用startScroll方法是无法让view的内用滑动的,startScroll内部根本没有做滑动相关的事情,那么Scroller如何让view的内容弹性滑动呢?原因就是startScroll下面==view的invalidate()==方法。
invalidate方法会导致view重绘走view的draw方法,view的draw方法中会调用computeScroll而这个computeScroll是个空实现需要我们自己实现所以会调用我们重写的computeScroll,我们在重写的computeScroll中使用scrollTo,scrollTo的参数就是Scroller对象中保存的滚动的距离。
接着==postInvalidate();==调用进行二次重绘,继续调用computeScroll继续滑动。
接着:mScroller.computeScrollOffset()
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
........
该方法会流逝的时间计算出当前滑动X,Y值。计算的方法是按照当前所用时间占时间间隔的百分比*滑动距离。该方法返回一个boolean类型变量,true表示当前滑动还未结束,返回false则表示当前滑动已经结束。如果返回true则调用scrollTo(int x, int y)让View中的内容滑动即可。