3.1 View 基础知识
(1). view是一种界面层的控件的一种抽象,它代表了一个控件。ViewGroup
也是view。
(2). view的位置参数:top、left、right、bottom,分别对应View的左上角和右下角相对于父容器的横纵坐标值。
从Android 3.0开始,view增加了x、y、translationX、translationY
四个参数,这几个参数也是相对于父容器的坐标。x和y是左上角的坐标,而translationX和translationY是view左上角相对于父容器的偏移量,默认值都是0。
x = left + translationX
y = top + translationY
(3). MotionEvent
是指手指接触屏幕后所产生的一系列事件,主要有ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
等。正常情况下,依次手指触屏会触发一系列点击事件,主要有下面两种典型情况:
- 点击屏幕后离开,事件顺序为 DOWN -> UP
- 点击屏幕后滑动一会再离开,事件序列为DOWN -> MOVE ->...->MOVE->UP
。
getX/getY
返回相对于当前View左上角的x和y坐标,而getRawX/getRawY
返回的是相对于手机屏幕左上角的x和y坐标。
(4). TouchSlop
是系统所能识别出的被认为是滑动的最小距离,获取方式ViewConfiguration.get(getContext()).getScaledTouchSlop()
(5). VelocityTracker
用于追踪手指在滑动过程中的速度,包括水平和垂直方向上的速度。速度的计算公式:速度 = (终点位置 - 起点位置) / 时间段。速度可能是负值,例如当手指从屏幕右边往左边滑动的时候。此外,速度是单位时间内移动的像素数,可以使用方法computeCurrentVelocity
这个方法指定单位时间是多少,单位是ms。例如通过computeCurrentVelocity(1000)
来获取速度,手指在1s中滑动了100个像素,那么速度是100,即100(像素/1000ms)。
VelocityTracker的使用方式:
/初始化
VelocityTracker mVelocityTracker = VelocityTracker.obtain();
//在onTouchEvent方法中
mVelocityTracker.addMovement(event);
//获取速度
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
//重置和回收
mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的时候调用
mVelocityTracker.recycle(); //一般在onDetachedFromWindow中调用
(6). GestureDetector
用于辅助检测用户的单击、滑动、长按、双击等行为。GestureDetector的使用比较简单,主要也是辅助检测常见的触屏事件。作者建议:如果只是坚挺活动相关的事件在onTouchEvent中实现;如果要监听双击这种行为的话,那么就使用GestureDetector。
(7). Scroller
:我们一般使用Scroller和View的computeScroll来实现View的弹性滑动。代码如下:
Scroller scroller = new Scroller(mContext);
private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
// 1000ms 内花香destX,效果就是慢慢滑动
mScroller.startScrool(scrollX, 0, delta, 0, 1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
3.2 View的滑动
(1). 常见的实现view的滑动的方式有三种:
第一种是通过view本身提供的scrollTo和scrollBy方法:操作简单,适合对view内容的滑动;
第二种是通过动画给view施加评议效果来实现活动:操作简单,适用于没有交互的view和实现复杂的动画效果;
第三种是通过改变view的LayoutParams使得view重新布局而实现滑动:操作稍微复杂,适用于有交互的view。
(2). scrollTo和scrollBy方法只能改变view内容的位置而不能改变view在布局中的位置。scrollBy是基于当前位置的相对滑动,而scrollTo是基于所传参数的绝对滑动。通过view的getScrollX和getScrollY方法可以得到滑动的距离。
(3). 使用动画来移动view主要是操作view的translationX和tranlationY属性,既可以使用传动的view动画,也可以使用属性动画,使用后者需要考虑兼容性问题,如果要兼容Android3.0以下的版本系统的话推荐使用nineoldandroids。2016-10-21更新:现在一般都不需要再兼容那么低版本的Android系统了,所以nineoldandroids这个库已经不再维护了
(4). 改变布局参数,通过改变LayoutParams的方式去实现view的滑动童谣是一种很灵活的方法,需要根据不同情况去做不同的处理。
方法名称 | 优点 | 缺点 |
---|---|---|
scrollTo/scrollBy | 操作简单,适合对view内容的滑动 | 只能滑动view的内容,并不能滑动view本身 |
属性动画 | 操作简单,主要适用于没有交互的view和实现复杂的动画效果 | 没有明显的缺点 |
改变布局参数 | 适用于有交互的View | 操作稍微复杂 |
3.3 弹性滑动
(1). Scroller的工作原理:Scroller本省并不能实现view的滑动,它需要配合view 的computeScroll方法才能完成弹性滑动效果,它不断地让view重绘,而每一次重绘距滑动起始时间都有一个时间间隔,通过这个时间间隔Scroller就可以得出view 的当前的滑动位置,知道了滑动位置就可以通过scrollTo方法来完成view的滑动。就这样,view的每一次重绘都会导致view进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,这就是Scroller的工作原理。
(2). 使用延时策略来实现弹性滑动,它的核心思想是通过发送一系列延时消息从而达到一种渐进式的效果,具体来说可以使用Handler的sendEmptyMessageDelayed(xxx)
或者view的postDelayed
方法,也可以使用线程的sleep方法
## 3.4 view的事件分发机制
(1). 所谓点击事件的事件分发,其实就是对MOtionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的view,而这个传递的过程就是分发过程。
(2). 事件分发过程的三个重要方法:
public boolean dispatchTouchEvent(MotionEventev)
用来进行事件的分发。如果事件能够传递给当前view,那么此方法一定会被调用,返回结果受当前view的onTouchEvent和下级view的dispatchTouchEvent方法的影响,表示是否消耗当前的事件。public boolean onInterceptTouchEvent(MotionEvent event)
在dispatchTouchEvent
方法内部调用,用来判断是否拦截某个事件,如果当前view拦截了某个事件,那么在同一个时间序列当中,此方法不会再被调用,返回结果表示是否拦截当前事件。若返回值为True事件会传递到自己的onTouchEvent();若返回值为false传递到子view 的dispatchTouchEvent()。public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent
方法内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前view无法再次接受到事件。若返回值为True,事件由自己处理,后续事件序列让其处理;若返回值为false,自己不消耗事件,向上返回让其他父容器的onTouchEvent接受处理。
三个方法的关系可以用下面的伪代码表示:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
(3). OnTouchListener的优先级比onTouchEvent要高
如果给一个view设置了onTouchListener,那么OnTouchListener中的onTouch
方法就会被回调。这时事件如果处理还需要看onTouch的返回值,如果是返回false,那么当前view的onTouchEvent方法会被调用;如果返回True,那么onTouchEvent方法将不会被调用。
在onTouchEvent方法中,如果当前view设置了onClickListener,那么它的onClick方法会被调用,所以onClickListener的优先级最低。
(4). 当一个点击事件发生后,传递过程遵循如下顺序:Activity -> Window -> View。如果一个view的onTouchEvent方法返回false,那么它的父容器的onTouchEvent方法将会被调用,依次类推,如果一个view的onTouchEvent方法返回false,那么它的父容器的onTouchEvent
方法将会被调用,依次类推,如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理(调用Activity的onTouchEvent方法)。
(5). 正常情况下,一个事件序列只能被一个view拦截并消耗,因为一旦某个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,并且该元素的onInterceptTouchEvent
方法不会再被调用了。
(6). 某个view一旦开始处理事件,如果它不消耗ACTION_DOWN
事件,那么同一事件序列的其他事件都不会再交给它来处理,并且事件将重新交给它的父容器去处理(低啊用父容器的onTouchEvent方法);如果它消耗ACTION_DOWN
事件,但是不消耗其他类型事件,但是这些事件最后都会传递给Activity处理。
(7). 如果View不消耗除ACTION_DOWN
以外的其他事件,那么这个点击事件会小时,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续受到后续的事件,最后这些小时的点击事件会传递给Activity处理。
(8). ViewGroup默认不拦截任何事件,因为它的onInterceptTouchEvent
方法默认返回false。view没有onInterceptTouchEvent
方法,一旦有点击事件传递给它,那么它的onTouchEvent
方法就会被调用。
(9). View的onTOuchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable
和longClickable
都为false)。view 的longClickable
默认是false的,clickable
则不一定,Button默认是true,而TextView默认是false。
(10). view的enable
属性不影响onTouchEvent的默认返回值。哪怕是一个view是disable
的状态,只要它的clickable或者longClickable有一个是true,那么它的onTouchEvent就会返回true。
(11). 事件传递过程总是先传递给父元素,然后再由父元素分发给子view,通过requestDisallowInterceptTouchEvent
方法可以再子元素中干预父元素的事件分发过程,但是ACTION_ DOWN事件除外,即当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件。ViewGroup的dispatchTouchEvent
方法中有一个标志位 FLAG_DISALLOW_INTERCEPT
, 这个标志位就是通过子view调用requestDisallowInterceptTouchEvent
方法来设置的,一旦设置为true,那么ViewGroup不会拦截该事件。
(12). 以上结论可以再书中的源码解析部分得到解释。Window的实现类为PhoneWindow
,回去Activity的contentView方法。
((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);
3.5 view的滑动冲突
(1). 常见的滑动冲突的场景:
* 外部滑动方向和内部滑动方向不一致,例如viewpager中包含listview;
* 外部滑动方向和内部滑动方向一致,例如viewpager的单页中存在可以滑动的bannerview
* 上面两种情况的潜逃,例如viewpager的单个页面中包含了bannerview和listview。
(2). 滑动冲突的处理规则
可以根据滑动距离和水平方向形成的夹角;或者根据水平和数值方向滑动的距离差;或者是两个方向上的速度差等。
(3). 解决方式:
外部拦截法:点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要就不拦截。该方法需要重写父容器的onInterceptTouchEvent
方法,在内部做相应的拦截即可,其他均不需要做修改。
伪代码如下:
public boolean onInterceptTouchEvent(MotionEvent ev) {
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;
beak;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
retrun intercepted;
}
内部拦截法:父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器来处理。这种方法和Android中的事件分发机制不一致,需要配合rquestDisallowInterceptTouchEvent
方法才能正常工作。
下面是伪代码:
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);
}
除了子元素需要做处理以外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调用getParent().requestDisallowInterceptTouchEvent(false)
方法时,父元素才能继续拦截所需的事件。因为ACTION_ DOWN事件并不受FLAG_ DISALLOW_ INTERCEPT这个标记位的控制,所以一旦父容器拦截ACTION_DOWN事件,那么所有的事件都无法传递到子元素中去,这样内如拦截就无法起作用了。
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}