一. View基础触摸、手势、滑动事件
View是所有界面层的空间的一种抽象。
Andorid屏幕坐标系原点在左上角, 而View的left,right,top,bottom属性,都是相对于其父View的坐标。
1. MotionEvent 和 TouchSlop
MotionEvent
手指触摸屏幕后,典型的事件类型:
ACTION_DOWN
:手指刚接触屏幕;ACTION_MOVE
:手指在屏幕上移动;Aciton_UP
:手指在屏幕上松开的瞬间
一般来说,正常触摸屏幕的行为,会触发一系列事件,比如
- 点击屏幕后立即松开,事件序列: DOWN -> UP
- 点击屏幕,滑动一会再松开,事件序列:DOWN -> MOVE -> …. -> MOVE -> UP
MotionEvent提供两组方法,分别用于获取相对于当前View的坐标,和相对于屏幕原点的坐标:
getX()
/getY()
:获取相对于当前View左上角的 x 和 y 坐标。getRawX()
/getRawY
:获取相对于屏幕原点的 x 和 y 坐标。
TouchSlop
TouchSlop是系统所能识别出的被认为是滑动的最小距离,通过如下代码获取:
ViewConfiguration.get(getApplicationContext()).getScaledTouchSlop();
在处理滑动时,可以以此来过滤掉距离太小的滑动,提升用户体验。
2. VelocityTracker、GestureDetector 和 Scroller
VelocityTracker
速度追踪器,可以追踪当前事件的速度:@Override public boolean onTouch(View v, MotionEvent event) { if (velocityTracker1 == null) { velocityTracker1 = VelocityTracker.obtain(); } velocityTracker1.addMovement(event); //参数单位:毫秒 velocityTracker1.computeCurrentVelocity(1000); int xVelocity = (int) velocityTracker1.getXVelocity(); int yVelocity = (int) velocityTracker1.getYVelocity(); Log.d(TAG, "velocity:" + xVelocity + ", " + yVelocity); return true; }
在合适的时候,需要回收它:
protected void onDestroy() { velocityTracker.clear(); velocityTracker.recycle(); super.onDestroy(); }
GestureDetector
创建
GestureDetector
对象,并实现接口.GestureDetector gestureDetector = new GestureDetector(new GestureDetector.OnGestureListener() { @Override public boolean onDown(MotionEvent e) { Log.d(TAG, "由ACTION_DOWN 触发 "+e.getAction()); return false; } @Override public void onShowPress(MotionEvent e) { Log.d(TAG, "由ACTION_DOWN 触发, 触摸屏幕后,尚未松开或者拖动 "+ e.getAction()); } @Override public boolean onSingleTapUp(MotionEvent e) { Log.d(TAG, "触摸屏幕后松开,单击行为 "+ e.getAction()); return false; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { Log.d(TAG, "触摸屏幕并拖动 "+ e1.getAction()+" , "+e2.getAction()); return false; } @Override public void onLongPress(MotionEvent e) { Log.d(TAG, "长按行为 "+ e.getAction()); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { Log.d(TAG, "快速滑动行为 "+e1.getAction()+" , "+e2.getAction()); return false; } });
然后接管
onTouchListener
的返回:View.OnTouchListener touchListener = new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { // ... gestureDetector.setIsLongpressEnabled(false); // return true; return gestureDetector.onTouchEvent(event); } };
这样就可以监控手势行为了。
双击回调接口:
OnDoubleTapListener
:GestureDetector.OnDoubleTapListener doubleTapListener = new GestureDetector.OnDoubleTapListener() { @Override public boolean onSingleTapConfirmed(MotionEvent e) { //严格单击行为。不可能是双击中的一次单击。 return false; } @Override public boolean onDoubleTap(MotionEvent e) { //双击,由2次连续的单击组成。它不可能和onSingleTapConfirmed共存。 return false; } @Override public boolean onDoubleTapEvent(MotionEvent e) { //表示发生了双击行为。 // 在双击的期间,ACTION_DOWN, ACTION_MOVE 和 ACTION_UP 都会触发此回调 return false; } };
你可以为
GestureDetector
对象注入OnDoubleTapListener
:gestureDetector.setOnDoubleTapListener(doubleTapListener);
Scroller
- 弹性滑动对象,使View的滑动有过渡效果。如果我们使用View的
scrollTo
/scrollBy
,滑动过程是瞬间完成的,用户体验较差。 使用方法:
在自定义View或ViewGroup类中:
Scroller scroller ; public void smoothScrollBy(int dx, int dy) { Log.d(TAG, "startX: " + scroller.getFinalX() + " ,startY: " + scroller.getFinalY()); scroller.startScroll(scroller.getFinalX(), scroller.getFinalY(), dx, dy); invalidate(); } @Override public void computeScroll() { if (scroller.computeScrollOffset()) { scrollTo(scroller.getCurrX(), scroller.getCurrY()); postInvalidate(); } }
当需要滑动时,调用
smoothScrollBy
即可。scroller.startScroll
方法的参数说明:/** * Start scrolling by providing a starting point and the distance to travel. * The scroll will use the default value of 250 milliseconds for the * duration. * * @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) { startScroll(startX, startY, dx, dy, DEFAULT_DURATION); }
- 当 startX > 0,起始坐标相对于View向左偏移
- 当 startY > 0,起始坐标相对于View向上偏移
- 当 dx > 0,View的内容移动方向为左
- 当 dy > 0,View的内容移动方向为上
- 弹性滑动对象,使View的滑动有过渡效果。如果我们使用View的
3. View的滑动
滑动方法有三种:
1. scrollTo
和 scrollBy
:
```
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
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();
}
}
}
```
请注意,移动的是View中的内容,而View在布局中的位置不变。
参数说明:
- x > 0 : View中的内容向左移动
- y > 0 :View中的内容向上移动
多调式几次机会明白参数与移动的规律。
使用动画
可以使用XML定义动画,也可以在代码直接设置:
ObjectAnimator.ofFloat(tvTest1,"translationX",0,300).setDuration(1000).start();
在”Android 开发艺术探索” 一书第132页中,作者提到移动动画结束之后的View,其位置并没有发生改变; 尽管View移动到了新位置,但只能点击其移动之前的区域,View才会响应点击事件。
不过在测试中发现,这个问题已经被修复,android版本是比较新的
API 27: Android 8.1 (Oreo)
。
改变布局参数:
直接改变View的坐标和尺寸,达到移动View或改变大小的目的:
``` private void moveView(View view) { ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); params.leftMargin += 100; view.requestLayout(); //或者重新注入布局参数:setLayoutParams // view.setLayoutParams(params); } ```
弹性滑动:在上一小节已经介绍了
Scroller
实现弹性滑动的方案。此外,还可以使用动画ValueAnimator
:final int deltaX = -100; final ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 1).setDuration(1000); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float fraction = animation.getAnimatedFraction(); llTest1.scrollTo((int) (deltaX * fraction),0); } }); //....启动动画,放在需要的地方。 valueAnimator.start();
此外,还有一种方法:启动一个线程,然后不断地给Handler发送消息,在
handleMessage
方法中,对View调用scrollTo
或scrollBy
,就可以实现View的弹性滑动效果。不过这已经属于奇技淫巧了,用力过度,浪费不必要的开销。