Android开发艺术探索学习笔记
VIEW的基础知识
MotionEvent和TouchSlop
MotionEvent
getX/getY: View相对于父容器的x和y坐标
getRawX/getRawY: 相对于屏幕左上角的x和y坐标
TouchSlop
TouchSlop是系统能识别的滑动的最小距离! 和设备有关,不同设备的值可能不一样,通过ViewConfiguration.get(getContext).getScaledTouchSlop()
获取!
VelocityTracker 、 GestureDetector和Scroller
VelocityTracker
VelocityTracker表示速度追踪,在View的onTouchEvent方法中追踪当前点击事件的速度:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
//获取当前速度:
velocityTracker.computeCurrentVelocity(1000);//设置计算速度的时间间隔
int xVelocity = (int)velocityTracker.getXVelocity();//获取水平速度
int yVelocity = (int)velocityTracker.getYVelocity();//获取垂直速度
最后,不使用时,调用clear功能回收:
velocityTracker.clear();
velocityTracker.recycle();
GestureDetector
手势检测,用于辅助检测用户的单机、滑动、长按、双击等行为!
方法名 | 描述 | 所属接口 |
---|---|---|
onDown | 手指轻轻触摸屏幕的一瞬间,由1个ACTION_DOWN触发 | OnGestureListener |
onShowPress | 手指轻轻触摸屏幕,尚未松开或拖动,由1个ACTION_DOWN触发 注意和onDown的区别,它强调的是没有松开或者拖动的状态 | OnGestureListener |
onSingleTapUp | 手指(轻轻触摸屏幕后)松开,伴随着1个MotionEvent.ACTION_UP而触发,这是单击行为 | OnGestureListener |
onScroll | 拖动,由一个ACTION_DOWN,多个ACTION_MOVE触发 | OnGestureListener |
onLongPress | 用户长久地按着屏幕不放,即长按 | OnGestureListener |
onFling | 用户按下触摸屏、快速滑动后松开,由1个ACTION_DOWN、多个ACTION_MOVE和1个ACTION_UP触发,这是快速滑动行为 | OnGestureListener |
onDoubleTap | 双击,由个连续的单击组成,和onSingleTapConfirmed共存 | onDoubleTapListener |
onSingleTapConfirmed | 严格的单击行为 它和onSingleTapUp不同,在双击事件中会触发后者 | onDoubleTapListener |
onDoubleTapEvent | 表示发生了双击行为,双击期间ACTION_DOWN、ACTION_MOVE和ACTION_UP都会触发 | onDoubleTapListener |
Scroller
弹性滑动对象,用于实现View的弹性滑动。一般和computeScroll配合使用:
Scroller scroller = new Scroller(mContext);
//缓慢滚动到指定位置
private void smoothScrollTo(int destX, int destY){
int scrollX = getScrollX();
int delta = destX - destY;
//1000ms内滑向destX, 效果就是慢慢滑动
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
@Verride
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postINVALIDATE();
}
}
VIEW的滑动
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 x代表view到达的位置
* @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();
}
}
}
/**
* Move 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 amount of pixels to scroll by horizontally x代表view横向滑动的距离
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
这两种方法移动的是内容,view本身的位置,并没有变!如一个TextView移动的只是其文字,点击位置,背景色所在位置均不变!
动画
动画也可以实现view的滑动,除属性动画外,其他动画并不能实现view的位置参数,只是其内容的移动包括背景色的位置!
属性动画可以真正改变view的位置参数:
ObjectAnimator.ofFloat(textView, "translationX", 0, 100).setDuration(500).start();
改变布局参数
改变LayoutParams也可以实现view的滑动,这种移动可以真正改变view的位置信息!
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) textView.getLayoutParams();
layoutParams.height += 50;
layoutParams.topMargin += 100;
textView.setLayoutParams(layoutParams);
下面是一个实现跟手滑动的例子:
private android.widget.ImageView iv1;
private int mLastX;
private int mLastY;
...
iv1.setOnTouchListener(this);
...
@Override
public boolean onTouch(View v, MotionEvent event) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
int translationX = (int) (v.getTranslationX() + deltaX);
int translationY = (int) (v.getTranslationY() + deltaY);
v.setTranslationX(translationX);
v.setTranslationY(translationY);
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
View的弹性滑动
可以使用Scroller、动画、延时等方法实现:
//属性动画
ObjectAnimator.ofFloat(tv1, "translationX", 0, 100).setDuration(500).start();
//下面是模仿Scroller实现弹性滑动:动画实质上没有作用再任何对象,只是提供一个过
//程,真正的实现是自己利用动画的时间百分比计算距离,最终使用scrollTo实现!
final int startX = 0;
final int deltaX = 500;
final ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 1).setDuration(1000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = valueAnimator.getAnimatedFraction();
Log.i("ss", "onAnimationUpdate: "+ fraction);
tv2.scrollTo(startX+(int) (deltaX*fraction), 0);
}
});
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.start();
View的事件分发机制
事件分发的过程有三个方法共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent
dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发,如果事件能够传递到View,那么此方法一定会被调用!返回结果受当前View的onTouchEvent和子view的dispatchTouchEvent方法影响!
onInterceptTouchEvent(MotionEvent event)
判断是否拦截某个事件,如果拦截了某个事件,那么在同一事件序列中,此方法不会再次调用!
onTouchEvent(MotionEvent ev)
改方法在dispatchTouchEvent中调用,用来处理点击事件,返回结果表示是否消费此事件,如果不消费,同一事件序列中,当前view无法再次接收到事件!
他们的关系可以用以下的伪代码来表示:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean cosume = false;
if(onInterceptTouchEvent(ev)){//如果拦截了 由当前view的onTouchEvent决定
consume = onTouchEvent(ev);
}else{//如果没有拦截,交由子组件处理,然后再去子组件重复判断
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
整体来说一个view 内部的事件调用,大致有一下流程:
dispatchTouchEvent => onInterceptTouchEvent => onTouch => onTouchEvent => onClick
首先是调用dispatchTouchEvent分发,然后判断其onInterceptTouchEvent,若返回true,则事件由当前view处理,这时如果设置了OnTouchListener,
则onTouch会被调用,否则调用onTouchEvent,也就说都提供的话,onTouch会屏蔽onTouchEvent。在onTouchEvent中,如果设置了onClickListener,
则onClick会被调用。而如果当前view不拦截事件,则继续调用子view的dispatchTouchEvent...
View的滑动冲突
常见的滑动冲突场景有以下三种
场景一处理冲突的规则:
左右滑动时,让外部view拦截点击事件;上下滑动时,让内部view拦截点击事件!
另外两种,要根据实际情况、业务逻辑具体解决!
外部拦截法:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
float x = ev.getX();
float y = ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要当前点击事件){
intercepted = true;
}else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
mLastY = y;
mLastX = x;
return intercepted;
}
内部拦截法:
//子view
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要此类点击事件){
parent.requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
Log.i("TAG", "ACTION_UP:inner ---------------");
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
//父view
public boolean onInterceptTouchEvent(MotionEvent event){
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN){
return false;
}else{
return true;
}
}