View的事件体系
本篇是个人在阅读安卓开发艺术探索一书后的知识归纳以及分享自己的一些理解所写的博客,在于以后可以方便的进行复习相关知识,毕竟这些基础知识是多多去理解的。(本来这一章已经写完了大部分了,由于自己的疏忽没保存导致将近6k字的文章就这么没了,唉,所以我是打算从后续弹性滑动开始补吧,至于之前的知识后续考虑补吧。。。)
同样我也看到了一些比较好的博客,比如这篇也是讲View的,讲的很全,她的其他博客也挺好的:要点提炼|开发艺术之View.
三、弹性滑动
3.1使用Scroller
使用Scroller和computeScroll方法可以实现弹性滑动,且现在是有典型代码了的:
Scroller scroller = new Scroller(mContext); //实例化一个Scroller对象
private void smoothScrollTo(int dstX, int dstY) {
int scrollX = getScrollX();//View的左边缘到其内容左边缘的距离
int scrollY = getScrollY();//View的上边缘到其内容上边缘的距离
int deltaX = dstX - scrollX;//x方向滑动的位移量
int deltaY = dstY - scrollY;//y方向滑动的位移量
scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); //开始滑动
invalidate(); //刷新界面
}
@Override//计算一段时间间隔内偏移的距离,并返回是否滚动结束的标记
public void computeScroll() {
if (scroller.computeScrollOffset()) { //通过时间流逝百分比计算出相应位置
scrollTo(scroller.getCurrX(), scroller.getCurY());
postInvalidate();//通过不断的重绘不断的调用computeScroll方法
}
}
首先要知道的是Scroller调用startScroll方法是无法实现滑动的,他只是保存了一些参数,如起始位置,滑动的距离,通过这个算出最终位置,滑动持续时间,具体可以看看源码:
public void startScroll(int startX,int startY,int dx,int dy,int duration){
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;//滑动时间
mStartTime = AnimationUtils.currentAminationTimeMills();//开始时间
mStartX = startX;//滑动起点
mStartY = startY;//滑动起点
mFinalX = startX + dx;//滑动终点
mFinalY = startY + dy;//滑动终点
mDeltaX = dx;//滑动距离
mDeltaY = dy;//滑动距离
mDurationReciprocal = 1.0f / (float)mDuration;
}
那么是如何实现滑动的呢,其实是靠下面的invalidate方法,具体过程来详细理一遍:首先初始化一个Scroller对象,接着调用它的startScroll方法保存一些参数,接着再调用invalidate方法,invalidate方法会导致View进行重绘,而在View的draw方法中又会调用computeScroll方法,这就到了我们重写的computeScroll方法中了,接着在computeScroll方法中调用Scroller对象的computeScrollOffset方法,这个方法是会根据时间流逝的百分比算出ScrollX和ScrollY改变的百分比并计算出当前值,同样computeScrollOffset方法的返回值为true则表示滑动未结束,通过if判断,接着根据算出的值调用scrollTo方法来进行滑动,进行滑动一小段距离并调用postInvalidate方法进行再次重绘。通过一次次的重绘,每次根据时间的百分比计算出滑动的位置滑动一小段距离,最终形成弹性滑动。 流程可看下图:
图片是源自上述所给链接的博客,为厘米姑娘所著。
3.2通过动画
动画本省就是一种渐近的过程,因此通过动画实现的滑动天然就具有弹性的效果,下例就是通过动画让一个View在100ms内向右滑动100像素:
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
3.3使用延时策略
通过发送一系列延时消息从而达到渐进式的效果,具体可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。具体操作就是,指定延时的时间长度,延时多少次,每次根据次数比例计算出滑动到的位置。
四、View的事件分发机制
首先要明白点击事件就是MotionEvent对象。所谓点击事件的分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,这个传递的过程就是分发过程。点击事件分发过程由三个方法共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent:
- public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件分发。如果事件能传递给当前View,那么此方法就一定会被调用,返回结果受当前View的OnTouchEvent和下级View的dispatchTouchEvent方法影响,表示是否消耗当前事件。 - public boolean onInterceptTouchEvent(MotionEvent event)
在dispatchTouchEvent方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么同一个事件序列中,这个方法不会再次调用,直接交给onTouchEvent处理,返回结果表示是否拦截当前事件。 - public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,同一个事件序列中,当前View无法再次接受到事件。
它们的关系可以通过伪代码来理解一下:
public boolean dispatchTouchEvent(MotionEvent ev) { //用来进行事件分发
//dispatchTouchEvent返回值受当前View的onTouchEvent和子View的dispatchTouchEvent影响
boolean consume = false;
if(onInterceptTouchEvent(ev)) { //用来判断是否拦截某事件
consume = onTouchEvent(ev); //用来处理事件
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
事件处理相关:
其次当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被回调,这时事件如何处理需要看onTouch的返回值,如果onTouch返回false,则当前View的onTouchEvent会被调用;如果返回true,那么onTouchEvent则不会被调用。由此可见OnTouchListener的优先级比onTouchEvent要高。在onTouchEvent方法中,如果当前设置有OnClickListener,它的onClick方法会被调用。由此可以看出,我们常用的OnClickLitener优先级最低,即处于事件传递的尾端。
具体分发过程:
当一个事件发生后,传递过程是Activity->Window->DectorView(就是setContentView的那个View的父容器)->顶级View(就是Activity中setContentView的那个View,一般都是ViewGroup);接下来就是View对点击事件的分发过程,先调用dispatchTouchEvent方法,接着,如果顶级ViewGroup拦截事件,onInterceptTouchEvent方法返回true,事件交由该ViewGroup处理,如果该ViewGroup的OnTouchListener被设置,调用onTouch方法,否则onTouchEvent方法。在onTouchEvent方法中如果设置了OnClickListener,则onClick会被调用。如果顶级ViewGroup不拦截事件,点击事件则会传递给点击事件链上的子View,调用子view的dispatchTouchEvent方法,事件就已经从顶级View传递给了下一层View,接下来分发同样如此,完成整个事件的分发。
其他的一些结论:
- 事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,整个过程产生的一些列事件。ACTION_DOWN—>ACTION_MOVE—>…—>ACTION_MOVE—>ACTION_UP;
- 某个View一旦决定拦截,那么这一个事件序列都由他来处理,且不会调用onInterceptTouchEvent不会被调用。
- 某个View一旦开始处理事件,如果不消耗ACTION_DOWN事件(onTouchEvent放回false),那么同一个事件序列的其他时间都不会交给它来处理,并且事件将重新交给它的父元素处理,即父元素onTouchEvent会被调用。
- 如果View不消耗ACTION_DOWN以外的事件,这个点击事件会消失,这时不会交给父元素处理,并且当前View仍可以接收到后续事件,这些消失的点击事件回传给Activity处理。(注意和第三条的差别)
- ViewGroup默认不拦截任何事件。源码中ViewGroup的onInterceptTouchEvent默认返回false。
- View没有onInterceptTouchEvent方法(很好理解啊,因为没有子View,不需要判断),一旦有点击事件传递给它,它的onTouchEvent方法就会被调用。
五、View的滑动冲突
在界面中只要内外两层可以同时滑动,这时就会产生滑动冲突。
5.1常见滑动冲突场景
一般有三种常见的滑动冲突场景:
- 外部滑动与内部滑动方向不一致;
- 外部滑动与内部滑动方向一致;
- 上面两种情况的嵌套。
5.2滑动冲突的处理规则
- 对于场景一:根据滑动的特征来解决滑动冲突,具体就是通过判断水平滑动还是竖直滑动来判断由谁来拦截事件,一般是通过滑动路径与水平夹角或x、y滑动距离判断滑动方向。
- 对于场景二:一般要从业务触发,判断什么条件时让哪一个View拦截事件。
- 对于场景三:同样时根据业务来找突破点。
5.3滑动冲突的解决方式
一般有两种方法,外部拦截法和内部拦截法。
- 外部拦截法:所谓外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,不需要此事件就不拦截,比较符合点击事件的分发机制,需要重写父容器的onInterceptTouchEvent方法,在内部做相应拦截,伪代码如下:
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case ACTION_DOWN:
intercepted = false;
break;
case ACTION_MOVE:
if(父容器需要点击事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
case ACTION_UP:
intercepted = false;
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
解释一下代码:a. 对于ACTION_DOWN事件,父容器必须返回false,因为父容器一旦拦截这个事件那么后续的ACITON_MOVE和ACTION_UP都直接交给父容器处理了;b. ACTION_MOVE事件根据需要来决定是否拦截,需要拦截就返回true,不需要返回false;c. 对于ACTION_UP事件,必须返回false,因为ACTION_UP事件没有意义,举个例子,本来事件是交由子元素处理,如果父容器在ACTION_UP事件返回了true,子元素就接受不到ACTION_UP事件,这时子元素的onClick事件就无法触发,但父容器不一样,因为父容器一旦拦截事件,后续的事件包括ACTION_UP是直接交给父容器来处理的,不会调用onIntercepteTouchEvent方法,直接处理,所以即使返回false也无所谓。(可见上述事件分发的几条规则)
- 内部拦截法:内部拦截法是指父容器不拦截任何事件,所有事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,我们需要重写子元素的dispatchTouchEvent方法,伪代码如下:
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true); //使父容器不拦截任何事件
break;
case ACTION_MOVE:
if(父容器需要点击事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case ACTION_UP:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return super.dispatchTouchEvent(event);
}
同样父元素也需要处理一下,父容器默认拦截除了ACTION_DOWN以外的任何事件,这样子元素调用requestDisallowInterceptTouchEvent(false)时将事件交给父容器时才能拦截所需事件,父元素修改如下:
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACITON_DOWN) {
return false;
} else {
return true;
}
}
解释一下代码:在子元素中接受到ACTION_DOWN事件时调用requestDisallowInterceptTouchEvent(true)使父容器不拦截任何事件,在ACTION_MOVE事件时进行判断如果需要事件直接消耗,父容器需要则调用requestDisallowInterceptTouchEvent(false)将事件交给父容器,父容器默认拦截除ACTION_DOWN外的任何事件。