Android View的事件体系教程详解
一、什么是View?什么是ViewGroup?
View是Android中所有控件的基类,不管是Button、ListView还是RelativeLayout,它们的基类都是View。View是一种界面层的控件的一种抽象,代表了一个控件。
而什么是ViewGroup,从字面上看,ViewGroup应该指的是一个控件组,即ViewGroup中可以包含许多控件。而ViewGroup继承自View,所以View本身就可以是单个控件也可以由多个控件组成的一组控件。这样就构成了View树。
二、View的位置
View的位置由它的四个顶点确定,top(左上角纵坐标)、left(左上角横坐标)、bottom(右下角纵坐标)、right(右下角横坐标),这几个参数都是相对父级容器而言的。
在Android中,X轴和Y轴的正方向分别为向右和向下。
根据四个顶点及AndroidView的坐标系,我们可以很容易得到View的宽高和坐标的关系:
width=right-left
height=bottom-top
那么如何得到这四个顶点呢?
left=getLeft();
right=getRight();
top=getTop();
bottom=getBottom();
从Android3.0开始,View增加了x,y,translationX和translationY。其中x和y是view左上角的坐标(相对坐标系),而translationX和translationY是View左上方相对父容器的偏移量。
x=left+translationX
y=top+translationY
需要注意的是View在平移过程中,top和left表示的是原始左上角的位置信息,其值不会改变,此时改变的是x、y、translationX和translationY
三、View的触摸事件
1.MotionEvent
在手指接触屏幕后所产生的一系列事件中,典型的事件有:
ACTION_DOWN——手指刚接触屏幕
ACTION_MOVE——手指在屏幕上移动
ACTION_DOWN——手指从屏幕上松开
一般我们可以将一次手指接触屏幕的行为分为两种情况:
点击屏幕后松开,事件序列为DOWN->UP
点击屏幕滑动一段时间后松开,事件序列为DOWN->MOVE->…->MOVE->UP
2.TouchSlop
TouchSlop即系统能识别滑动的最小距离,这是一个与设备有关的系统常量。不难得知其意思,当手指在屏幕上滑动小于这个距离时,系统不认为你在进行滑动操作。
通过ViewConfiguration.get(getContext()).getScaledTouchSlop()方法来获取这个系统常量。
3.VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向上的速度。
具体用法:
在View的onTouchEvent方法中追踪当前单击事件的速度。
VelocityTracker velocityTracker =VelocityTracker.obtain(); velocityTracker.addMovement(event); //接着当我们我们想知道当前的滑动速度时 //获取速度前先计算速度, 参数 时间间隔 单位ms velocityTracker.computeCurrentVelocity(1000); //获取速度 int xVelocity = (int)velocityTracker.getXVelocity(); int yVelocity=(int)velocityTracker.getYVelocity();
需要注意的是,这边的计算得到的速度与时间间隔有关,其计算公式如下:
速度=(终点位置-起点位置)/时间间隔
计算速度时得到是就是一定时间间隔内手指在水平或竖直方向上滑动的像素数,如
100像素/1000ms,这里的速度值即为100。
当然在不需要使用它时,需要调用clear方法来重置并回收内存。
velocityTracker.clear();
velocityTracker.recycle();
4.GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。如果只是监听滑动相关的建议在onToucheEvent实现,如果需要监听双击,使用GestureDetector。
5.Scroller
弹性滑动对象,用来实现View的弹性滑动,View的scrollTo/scrollBy是瞬间完成的,使用Scroller配合View的computeScroll方法配合使用达到弹性滑动的效果
其典型代码是通用的.
/** * 平滑滚动 * @param dx 横向位移 * @param dy */ private void smoothScrollBy(int dx, int dy) { //水平滑动 mScroller.startScroll(getScrollX(),0,dx,0,500); invalidate(); } @Override public void computeScroll() { if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postInvalidate(); } }
四、View的滑动
实现View滑动的几种方式:
View的滑动方式 | 特点 | 适用场景 |
使用ScrollTo/ScrollBy | 只能改变View的内容,不能改变View本身的位置 | 适合对view内容的滑动 |
通过动画实现View的平移效果 | 只对影像进行操作,不能改变View的位置参数 | 适用于没有交互的View和实现复杂的动画效果。 |
使用属性动画实现View的平移效果 | 改变View的位置参数,可响应触摸等事件 | 适用于有交互的View,适配到Android3.0 |
改变View的LayoutParams,使得View重新布局实现滑动 | 改变View的位置参数,可响应触摸等事件 | 适用于有交互的View,使用稍复杂 |
前面提到了弹性滑动对象Scroll,其实实现弹性滑动的方法不止这一种,它们的共同思想就是将一次大的滑动分成若干次小的滑动并要求在一定时间内完成。实现弹性滑动的具体实现方式有:
- 通过Scroll实现
- 通过动画
- 使用延时策略
1)使用Scroll
使用Scroll实现弹性滑动需要配合View的computeScroll方法实现,简单来讲就是实现多次重绘,每一次重绘有一定的时间间隔,通过这个时间间隔Scroller可以得到View的当前滑动位置,然后通过ScrollTo方法实现滑动。
具体实现方法是在自己实现的平滑滑动方法中调用invalidate方法,它会导致View重绘,又因为在View的draw方法中又会去调用computeScroll方法,而在computeScroll方法中,我们实现了scrollTo方法来实现滑动,接着调用postInvalidate来进行第二次重绘,此时又会调用View中的draw方法,,继而调用computeScroll方法,如此反复,直到整个滑动过程完成。
2)通过动画
动画本身就是一种渐渐地过程,可以很好地实现弹性滑动。
3)使用延时策略
使用延时策略完成滑动,核心思想就是通过发送一系列的延时消息从而达到一种渐进的效果。具体的实现可以采用Handler或View的postDelayed方法,也可以采用sleep休眠。对于postDelayed方法们可以通过它来延时发送一个消息,然后在消息中进行View的滚动。如果接连不断发的发送这种消息,则可以达到弹性滑动对象。
而对于sleep方法,通过在while循环中不断滑动View和sleep即可实现。
五、View的事件分发机制
分发对象:MotionEvent,所谓的事件分发其实就是对MotionEvent事件的分发过程,即需要将这个事件传递到一个具体的View上进行处理。而完成这一过程需要三个重要方法来共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
public boolean onInterceptTouchEvent(MotionEvent ev)
在dispatchTouchEvent方法内部调用,用于判断是否拦截某个时间,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用。
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中调用,用于处理点击事件,返回结果表示是否消耗事件,如果不消耗,则在同一个事件序列中,当前View无法再次收到事件。
建立在上述方法的基础上,我们简单分析一下事件分发的过程。
由上述流程图不难发现,在MotionEvent被一个View所拦截时,其内部的事件分发的过程中,onTouchListener的优先级高于onTouchEvent,而常用的onClickListener的优先级是最低的。即在onTouch->onTouchEvent->onClick。
几个重要的结论:
- 在整个View树中的事件分发中,如果一个View一旦开始处理事件,但它不消耗ACTION_DOWN事件(onTOuchEvent返回false),那么同一个事件序列中的其他事件也不会交给它来处理,而是将事件重新交给它的父元素进行处理,即父元素的onTouchEvent会被调用。
- 而如果一个View消耗了ACTION_DOWN,但没有消耗事件序列中的其他事件,那么这个点击事件会消失,并且此时父元素的onTouchEvent也不会被调用,当前View可以持续受到后续的事件,最终这些消失的点击事件会传递给Activity处理。
- ViewGroup默认不拦截任何事件
- View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,就好调用它的onTouchEvent方法。
- View的onTouchEvent默认都是会消耗事件的,除非它是不可点击的(clickable和longClickable为false)
- 事件传递过程是由外向内的,即事件总是传给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素中的分发过程(除ACTION_DOWN)
六、View的滑动冲突问题
View的滑动冲突常见可以简单分为三种:
1.外部滑动和内部滑动方向不一致
2.外部滑动方向和内部滑动方向一致
3.上面两张情况的嵌套
对于第一种情况,一个很好的例子就是ViewPager和Fragment嵌套使用组成的页面滑动效果,而在Fragment内部又会嵌套一个ListView。大家都知道ViewPager的滑动方向是水平的,而ListView的滑动方向是竖直的,这种情形和第一种情况是相符的。当然ViewPager在内部处理了这种滑动冲突,因此采用ViewPager不用考虑这个问题。而如果我们采用的是ScrollView,则必须手动处理这种滑动冲突了。
对于第二种情况,即内外两层都是需要上下滑动或者左右滑动的。可以举一个常见的例子,即ViewPager和NavigationDrawer。这两者都是水平方向的滑动。当然在实际使用中,会发现并没有滑动冲突,还是上一个原因,ViewPager内部处理了这种滑动冲突。
第三种情况即前面两个例子的融合。
滑动冲突的处理规则
如何解决滑动冲突,这就需要用到前面讲到的事件分发机制了,其核心思想就是根据实际事件的特点(down的位置,水平滑动距离,竖直滑动距离等)来判断由哪个View来拦截事件。对于第一种情况可以简单地判断是水平滑动还是竖直滑动来判断由哪个View来拦截事件。(可以根据水平和竖直方向上的距离差或速度差来进行判断),而对于第二种情况,可根据down的位置来加以区分。
滑动冲突的解决方法
- 外部拦截法 —— 即点击事件先经过父容器的拦截处理,如果父容器需要此事件就拦截,不需要就不拦截,需要重写父容器的onInterceptTouchEvent方法;在onInterceptTouchEvent方法中,首先ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE/ACTION_UP都会直接交给父容器处理;其次是ACTION_MOVE,根据需求来决定是否要拦截;最后ACTION_UP事件,这里必须要返回false,在这里没有多大意义。
- 内部拦截法 —— 所有事件都传递给子元素,如果子元素需要就消耗掉,不需要就交给父元素处理,需要子元素配合requestDisallowInterceptTouchEvent方法才能正常工作;父元素需要默认拦截除ACTION_DOWN以外的事件,这样子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截需要的事件。(ACTION_DOWN事件不受requestDisallowInterceptTouchEvent方法影响,所以一旦父元素拦截ACTION_DOWN事件,那么所有元素都无法传递到子元素去)
两种拦截方法的范式(伪代码形式):
外部拦截法:只需要重写父容器的onInterceptTouchEvent方法
private int mLastXIntercepet=0; private int mLastYIntercepet=0; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted =false; int x=(int) ev.getX(); int y=(int) 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; } mLastXIntercepet=x; mLastYIntercepet=y; return intercepted; }
内部拦截法:需要重写子元素的dispatchTouchEvent方法和父容器的onInterceptTouchEvent方法。
子元素的dispatchTouchEvent方法
//分别记录上次滑动的坐标 private int mLastX=0; private int mLastY=0; @Override public boolean dispatchTouchEvent(MotionEvent ev) { int x= (int) ev.getX(); int y= (int) ev.getY(); switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: int deltaX=x-mLastX; int deltaY=y-mLastY; if(父容器需要此类点击事件){ getParent().requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; default: break; } mLastX=x; mLastY=y; return super.dispatchTouchEvent(ev); }
父容器的onInterceptTouchEvent:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { int x= (int) ev.getX(); int y= (int) ev.getY(); int action=ev.getAction(); if(action==MotionEvent.ACTION_DOWN) return false; else return true; }
栏 目:Android代码
下一篇:Flutter使用RepositoryProvider解决跨组件传值问题
本文地址:http://www.codeinn.net/misctech/204429.html