1.View基础知识
1.什么是View
View是Android中所有控件的基类。ViewGroup继承View,内部可包含多个控件,即一组View。
2.View的位置参数
View相对于父容器的位置坐标,对应于View的四个属性:top,left,right,bottom。
View的宽高和坐标的关系:
width = right - left
height = bottom - top
从Android3.0开始,View增加了几个额外的相对于父容器坐标的参数:x,y,translationX和translationY。
x和y是View左上角坐标,translationX和translationY是View左上角想对于父容器的偏移量,默认值是0。
x = left + translationX
y = top + translationY
View在平移过程中,top和left表示原始左上角位置信息,其值不变,发生改变的是x,y,translationX和translationY。
3.MontionEvent和TouchSlop
1.MontionEvent是指手指接触屏幕后所产生的一系列事件,主要有:
ACTION_DOWN:手指刚接触屏幕。
ACTION_MOVE:手指在屏幕上移动。
ACTION_UP:手指从屏幕上松开的一瞬间。
正常情况下,一次手指触摸屏幕行为会触发一系列点击事件,主要有以下两种:
点击屏幕后离开松开,事件序列为DOWN->UP。
点击屏幕滑动一会再松开,事件序列为DOWN->MOVE->...->MOVE->UP。
getX/getY:返回相对于当前View左上角x和y坐标。
getRawX/getRawY:返回想对于手机屏幕左上角x和y坐标。
2.TouchSlop是系统所能识别出的被认为是滑动的最小距离:
ViewConfiguration.get(getContext()).getScaledTouchSlop()
4.VolocityTracker,GestureDetector和Scroller
1.VolocityTracker用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。
速度可为负值,计算公式:
速度 = (终点位置 - 起点位置) / 时间段
首先,在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();
需要注意的是,computeCurrentVelocity()方法的参数表示的是一个时间间隔,单位是毫秒(ms),计算速度得到的就是在这个时间间隔内手指在水平或竖直方向上所滑动的像素数。
2.GestureDetector用于辅助检测用户的单击,滑动,长按,双击等行为。
首先,需要创建一个GestureDetector对象并实现OnGestureListener接口,根据需要还可以实现OnDoubleTapListener从而能够监听双击行为:
GestureDetector mGestureDetector = new GestureDetector(this);
mGestureDetector.setIsLongpressEnabled(false); //解决长按屏幕后无法拖动的现象
接着,接管目标View的onTouchEvent方法,在待监听View的onTouchEvent方法中添加如下实现:
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
作者建议:如果只是监听滑动相关的事件在onTouchEvent中实现;如果要监听双击这种行为的话,那么就使用GestureDetector。
3.Scroller用于实现View的弹性滑动。Scroller本身无法让View弹性滑动,它需要和View的computeScroll方法配合使用才能共同完成这个功能。
Scroller mScroller = new Scroller(mContext);
private void smoothScrollTo(int destX, int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
2.View的滑动
通过三种方式可以实现View的滑动:
1.通过View本身提供的scrollTo/scrollBy方法。
2.通过动画给View施加平移效果。
3.通过改变View的LayoutParams使得View重新布局。
1.使用scrollTo/scrollBy
//view内容在水平方向的偏移量
protected int mScrollX;
//view内容在竖直方向的偏移量
protected int mScrollY;
//x,y 滚动到的位置坐标
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();
}
}
}
//x,y 水平/竖直方向上的滚动偏移量,上正下负,左正右负
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
mScrollX和mScrollY的单位为像素,如果从左向右滑动,mScrollX为负值,反之为正值。如果从上向下滑动,mScrollY为负值,反之为正值。这两个属性可通过getScrollX和getScrollY方法取得。
scrollTo和scrollBy只能改变View内容的位置而不能改变View在布局中的位置。
2.使用动画
使用动画来移动View,主要是操作View的translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画。
View动画是对View的影像做操作,它并不能真正改变View的位置参数,包括宽/高,并且如果希望动画后的状态得以保留还必须将fillAfter属性设置为true。如果View设置了点击事件,这将导致新位置无法触发点击事件,而老位置仍然可以触发点击事件。
从Android3.0开始,使用属性动画可以解决上面的问题。
3.改变布局参数
改变LayoutParams实现动画效果。
MarginLayoutParams params = (MarginLayoutParams) view.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
view.requestLayout();
4.各种滑动方式的对比
scrollTo/scrollBy:操作简单,适合对View内容的滑动。
动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果。
改变布局参数:操作稍微复杂,适用于有交互的View。
3.弹性滑动
将一次大的滑动分成若干次小的滑动并在一个时间段内完成,弹性动画的具体实现方式有很多,比如通过Scroller,Handler#postDelayed以及Thread#sleep等。
1.使用Scroller
Scroller本身并不能实现View的滑动,它需要配合View的computeScroll方法才能完成弹性滑动的效果,它不断地让View重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller就可以得出View当前的滑动位置,知道了滑动位置就可以通过scrollTo方法来完成View的滑动。就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅滑动就组成了弹性滑动,这就是Scroller的工作机制。
//停止动画。
public void abortAnimation ()
//调用此方法可获取最新位置。如果返回true,表示动画未结束。
public boolean computeScrollOffset ()
//返回当前X/Y滚动偏移量。
public final int getCurrX ()
public final int getCurrY ()
//根据起点坐标和滚动距离滑动,默认250ms。上正下负,左正右负。
public void startScroll (int startX, int startY, int dx, int dy)
public void startScroll (int startX, int startY, int dx, int dy, int duration)
2.使用动画
动画本身就是一种渐进的过程,因此通过它来实现的滑动天然就具有弹性效果。
以下代码可以让一个View在1000ms内向右移动100像素:
ObjectAnimator.ofFloat(view, "translationX", 0, 100).setDuration(1000).start();
以下代码可以让一个View的内容在1000ms内向左移动100像素:
final int startX = 0;
final int deltaX = 100;
final ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animator.getAnimatedFraction();
view.scrollTo(startX + (int) (deltaX * fraction), 0);
}
});
animator.start();
注释:这应该是本书的一个小错误,注意属性动画和scrollTo在滑动方面的区别。
3.使用延时策略
通过发送一系列延时消息从而达到一种渐进式的效果,具体来说可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。
以下代码可以让一个View的内容在1000ms内向左移动100像素:
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 33;
private int mCount = 0;
private Handler mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case MESSAGE_SCROLL_TO:
mCount++;
if (mCount <= FRAME_COUNT) {
float fraction = mCount / (float) FRAME_COUNT;
int scrollX = (int) (fraction * 100);
view.scrollTo(scrollX, 0);
mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
}
break;
default:
break;
}
};
};
4.View的事件分发机制
1.点击事件的传递规则
触摸事件的分发过程由三个很重要的方法共同完成:
//View方法,用来分发触摸事件。
public boolean dispatchTouchEvent (MotionEvent event)
//View方法,用来判断是否拦截触摸事件,返回结果表示是否消耗当前事件。
public boolean onTouchEvent (MotionEvent event)
//ViewGroup方法,用来处理触摸事件,返回结果表示是否拦截当前事件。
public boolean onInterceptTouchEvent (MotionEvent ev)
ViewGroup中,三个方法的关系可用如下为代码表示:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
当一个触摸事件产生后,它的传递过程遵循如下顺序:Activity->Window->View,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View。顶级View接收到事件后,就会按照事件分发机制去分发事件。
事件传递机制的一些结论:
1.同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。
2.正常情况下,一个事件序列只能被一个View拦截和消耗。
3.某个View一旦决定拦截,那么这一个事件序列都只能由它来处理,并且它的onInterceptTouchEvent不会再被调用。
4.某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。
5.如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的触摸事件会传递给Activity处理。
6.ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false。
7.View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
8.View的onTouchEvent方法默认都会消耗事件,除非它是不可点击的。
9.View的enable属性不影响onTouchEvent的默认返回值。
10.onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。
11.事件传递过程是有外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowonInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
2.事件分发的源码解析
1.Activity对触摸事件的分发过程
Activity的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
getWindow()返回的window实现类是PhoneWindow,PhoneWindow的superDispatchTouchEvent()方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
mDecor是getWindow().getDecorView()返回的View,即当前界面的底层容器,setContentView所设置的View的父容器,可通过如下方式获得:
((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);
DecorView继承自FrameLayout。
2.ViewGroup对触摸事件的分发过程
特殊情况,FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件。
3.View对触摸事件的处理过程
5.View的滑动冲突
1.常见的滑动冲突场景
场景1:外部滑动方向和内部滑动方向不一致。
场景2:外部滑动方向和内部滑动方向一致。
场景3:上面两种情况嵌套。
2.滑动冲突的处理规则
对于场景1:当用户左右滑动时,需要让外部的View拦截触摸事件,当用户上下滑动时,需要让内部View拦截触摸事件。
对于场景2:在业务上找突破点,根据业务得出相应的处理规则。
3.滑动冲突的解决方式
1.外部拦截法
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (父容器需要拦截当前事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
2.内部拦截法
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (当前view需要拦截当前事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
补充:参考五道口宅男潇涧-Job Hunting
1.onTouchEvent的传递
当有多个层级的View时,在父层级允许的情况下,这个action会一直向下传递直到遇到最深层的View,因此touch事件最先调用的是最底层View的onTouchEvent。如果View的onTouchEvent接收到某个touch action并作了相应处理,最后有两种返回方式:返回true或false。返回true会告诉系统当前View需要处理这次touch事件,以后系统发出的ACTION_MOVE,ACTION_UP需要继续监听并接收,且父层的View不会触发onTouchEvent。如果返回false,便会通知系统,当前View不关心这一次的touch事件,此时这个action会传向父级,调用父级View的onTouchEvent,且这一次touch事件之后发出的ACTION_MOVE,ACTION_UP,不会再传入这个View,也不会再触发onTouchEvent。
2.父层onInterceptTouchEvent截获
前面说了底层View能够接收到touch事件有一个前提条件:在父层级允许的情况下。假设不改变父层级的dispatch方法,在系统调用底层View的onTouchEvent方法之前会先调用父View的onInterceptTouchEvent方法判断,父层View是不是要截获本次touch事件之后的action。如果onInterceptTouchEvent返回了true,那么本次touch事件之后的所有action都不会再向深层的View传递,统统都会传给父层View的onTouchEvent,就是说父层已经截获了本次touch事件,之后的action也不必询问onInterceptTouchEvent,即onInterceptTouchEvent不会再次调用,直到下一个touch事件的来临。如果onInterceptTouchEvent返回false,那么本次action将发送给更深层的View,并且之后的action都会询问父层的onInterceptTouchEvent需不需要截获本次touch事件。
3.底层View的getParent().requestDisallowInterceptTouchEvent(true)
对于底层的View来说,有一种方法可以阻止父层View截获touch事件,即调用getParent().requestDisallowInterceptTouchEvent(true)方法。一旦底层View收到touch的action后调用这个方法,那么父层View就不会再调用onInterceptTouchEvent了,也无法截获以后得action。