View的事件体系
一、view基础知识
什么是View
(1)View是Android中所有控件的基类,不管是简单的Button和TextView还是复杂的RelativeLayout和ListView,它们的共同基类都是View。
(2)ViewGroup也继承了View,这就意味着View本身就可以使单个控件也可以是有多个控件组成的一组控件,通过这种关系就形成了View树的结构。
View的位置参数
(1)View的位置主要由它的四个顶点来决定,分别对应于View的四个属性:top、left、right、bottom。其中top是左上角纵坐标,left是左上角横坐标,right是右下角横坐标,bottom是右下角纵坐标。这些坐标是一种相对坐标,都是相对于View的父容器来说的。
(2)可通过以下方式获取View的四个位置参数:
Left = getLeft(); Right = getRight(); Top = getTop(); Bottom = getBottom();
(3)从Android3.0开始,View增加了额外的几个参数:x、y、translationX和translationY,其中x和y是View内容左上角的坐标,而translationX和translationY是View左上角相对于父容器的偏移量,并且translationX和translationY的默认值是0。这几个参数的换算关系如下所示:
x = left + translationX y = top + translationY
(4)View在平移的过程中,top和left表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是x、y、translationX和translationY这四个参数。
MotionEvent和TouchSlop
MotionEvent
(1)在手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种:
- ACTION_DOWM——手指刚接触屏幕;
- ACTION_MOVE——手指在屏幕上移动;
- ACTION_UP——手指从屏幕上松开的一瞬间。
(2)正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,考虑如下几种情况:
- 点击屏幕后立即松开,事件序列为DOWM->UP;
- 点击屏幕滑动一会再松开,事件序列为DOWM->MOVE->...->MOVE->UP。
(3)getX/getY返回的是相对于当前View左上角的x和y坐标,而getRawX/getRawY返回的是相对于屏幕左上角的x和y坐标。
TouchSlop
(1)TouchSlop是系统所能识别出的被认为是滑动的最小距离,换句话说,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。
(2)通过如下方式即可获取这个常量:
ViewConfiguration.get(getContext()).getScaledTouchSlop();
(3)当我们在处理滑动时,可以利用这个常量来做一些过滤,比如当两次滑动事件的滑动距离小于这个值,我们就可以认为未达到滑动距离的临界值,因此就可以认为他们不是滑动。
VelocityTracker、GestureDetector和Scroller
VelocityTracker
(1)速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。在View的onTouchEvent方法中追踪当前单击事件的速度:
VelocityTracker velocityTracker = VelocityTracker。obtain(); velocityTracker.addMovement(event);
(2)可以采用如下方式来获得当前的速度:
velocityTracker.computeCurrentVelocity(1000); int xVelocity = (int)velocityTracker.getXVelocity(); int yVelocity = (int)velocityTracket.getYVelocity();
在这一步中有两点需要注意:
- 第一点,获取速度之前必须先计算速度,即必须要调用computeCurrentVelocity方法;
- 第二点,这里的速度是指一段时间内手指所滑过的像素数
(3)当不需要使用VelocityTracker的时候,需要调用clear方法来重置并回收内存:
velocityTracker.clear(); velocityTracker.recycle();
GestureDetector
gestrue—手势,什么叫手势呢?比如说,我们用魅族手机,我们在home键的两侧像屏幕内滑动,可以打开后台任务列表等等,在应用中通过手势来操作可以大大提升用户体验,手势是连续触碰的行为,比如左右上下滑等,安卓对上述两种手势行为都提供了支持:
而安卓中手势交互的执行顺序是:,其中MotionEvent这个类是用来封装手势,触摸笔,轨迹球等等的动作事件,其内部封装了两个重要的属性x和y,这两个属性分别用于记录横轴和纵轴的坐标,GestureDetector识别各种手势,OnGestureListener: 这是一个手势交互的监听接口,其中提供了多个抽象方法, 并根据GestureDetector的手势识别结果调用相对应的方法。
而对于GestureListener,有以下解释:
案例:下滑关闭Activity,上滑启动新的Activity
public class MainActivity extends AppCompatActivity { private GestureDetector mDetector; private final static int MIN_MOVE = 200; //最小距离 private MyGestureListener mgListener; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //实例化SimpleOnGestureListener与GestureDetector对象 mgListener = new MyGestureListener(); mDetector = new GestureDetector(this, mgListener); } @Override public boolean onTouchEvent(MotionEvent event) { return mDetector.onTouchEvent(event); //在某个activity或者是view中都可以,注意 } //自定义一个GestureListener,这个是View类下的,别写错哦!!! private class MyGestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float v, float v1) { if(e1.getY() - e2.getY() > MIN_MOVE){ startActivity(new Intent(MainActivity.this, MainActivity.class)); Toast.makeText(MainActivity.this, "通过手势启动Activity", Toast.LENGTH_SHORT).show(); }else if(e1.getY() - e2.getY() < MIN_MOVE){ finish(); Toast.makeText(MainActivity.this,"通过手势关闭Activity",Toast.LENGTH_SHORT).show(); } return true; } } }
Scroller
(1)弹性滑动对象,用于实现View的弹性滑动。
(2)Scroller本身无法让View弹性滑动,它需要和View的computeScroll方法配合使用才能共同完成这个功能。
Scroller mScroller = new Scroller(mContext); //缓慢滚动到指定位置 private void smoothScrollTo(int destX, int destY){ int scrollX = getScrollX(); int delta = destX - scrollX; //1000ms内滑向destX,效果就是慢慢滑动 mScroller.startScroll(scrollX, 0, delta, 0, 1000); invalidate(); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } }
二、View的滑动
有三种方式可以实现View的滑动:
- 第一种是通过View本身提供的scrollTo/scrollBy方法来实现滑动;
- 第二种是通过动画给View施加平移效果来实现滑动;
- 第三种是通过改变View的LayoutParams使得View重新布局从而实现滑动。
使用scrollTo/scrollBy
(1)scrollBy实际上也是调用了scrollTo方法,它实现了基于当前位置的相对滑动,而scrollTo则实现了基于所传递参数的绝对滑动。
(2)scrollTo和scrollBy只能改变View内容的位置而不能改变View在布局中的位置。
(3)在滑动的过程中,mScrollX的值总是等于View左边缘和View内容左边缘在水平方向的距离,而mScrollY的值总是等于View上边缘和View内容上边缘在竖直方向的距离。
(4)如果从左向右滑动,那么mScrollX为负值,反之为正;如果从上往下滑动,那么mScrollY为负值,反之为正值。
使用动画
(1)使用动画来移动View,主要是操作View的translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画,如果采用属性动画的话,为了兼容3.0以下版本,需要采用开源动画库nineoldandroids。
(2)在Android3.0以下的手机上通过nineoldandroids来实现的属性动画本质上仍然是View动画。
(3)View动画是对View的影像做操作,它并不能真正改变View的位置参数,包括宽/高,并且如果希望动画后的状态得以保留还必须将fillAfter属性设置为true,否则动画完成后其动画结果会消失。
改变布局参数
改变布局参数,即改变LayoutParams:
MarginLayoutParams params = (MarginLayoutParams)mButton.getLayoutParams(); params.width += 100; params.leftMargin += 100; mButton.requestLayout(); //或者mButton.setLayoutParams(params);
各种滑动方式的对比
- scrollTo/scrollBy:操作简单,适合对View内容的滑动;
- 动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果;
- 改变布局参数:操作稍微复杂,适用于有交互的View。
三、弹性滑动
使用Scroller
Scroller的工作原理:Scroller本身并不能实现View的滑动,它需要配合View的computeScroll方法才能完成弹性滑动的效果,它不断地让View重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller就可以得出View当前的滑动位置,知道了滑动位置就可以通过scrollTo方法来完成View的滑动。就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,这就是Scroller的工作机制。
通过动画
动画本身就是一种渐近的过程,因此通过它来实现的滑动天然就具有弹性效果。
使用延时策略
延时策略的核心思想是通过发送一系列延时消息从而达到一种渐近式的效果,具体来说可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。
四、View的事件分发机制
点击事件的传递规则
(1)点击事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
- public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发。如果事件能够传递给当前的View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
- public boolean onInterceptTouchEvent(MotionEvent ev)
在dispatchTouchEvent方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
- public boolea onTouchEvent(MotionEvent ev)
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
上述三个方法的关系可以用如下伪代码表示:
public boolean dispatchTouchEvent(MotionEvent ev){ boolean consume = false; if (onInterceptTouchEvent(ev)) { consume = onTouchEvent(ev); } else { consume = child.dispatchTouchEvent(ev); } return consume; }
通过上面的伪代码,我们也可以大致了解点击事件的传递规则:对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptEvent方法放回false就表示它不拦截当前事件,这时当前事件就会传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。
(2)给View设置的OnTouchListener,其优先级比onTouchEvent要高,onTouchEvent的优先级比OnClickListener要高。
(3)当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->View,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View。顶级View接到事件后,就会按照事件分发机制去分发事件。如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,依此类推,如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理,即Activity的onTouchEvent方法会被调用。
(4)关于事件传递机制,这里给出一些结论,根据这些结论可以更好地理解整个传递机制:
- 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。
- 正常情况下,一个时间序列只能被一个View拦截且消耗。因为一旦一个元素拦截了某次事件,那么同一个时间序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。
- 某个View一旦决定拦截,那么这一个事件序列都只能由它处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEvent不会再被调用。
- 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。
- 如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
- ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false。
- View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
- View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时返回false)。View的longClickable属性默认都为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false。
- View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
- onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。
- 事件的传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
五、View的滑动冲突
常见的滑动冲突场景
常见的滑动冲突场景可以简单分为如下三种:
- 场景1:外部滑动方向和内部滑动方向不一致;
- 场景2:外部滑动方向和内部滑动方向一致;
- 场景3:上面两种情况的嵌套。
滑动冲突的处理规则
(1)对于场景1,它的处理规则是:根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件。
(2)对于场景2,它无法根据滑动的角度、距离差以及速度差来做判断,但是这个时候一般都能在业务上找到突破点,比如业务上有规定:当处于某种状态时需要外部View响应用户的滑动,而处于另外一种状态是则需要内部View来响应View的滑动,根据这种业务上的需求我们也能得出相应的处理规则。
(3)场景3跟场景2一样,只能从业务上找到突破点,具体方法和场景2一样,都是从业务的需求上得出相应的处理规则。
滑动冲突的解决方式
外部拦截法
(1)所谓外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。这种方法的伪代码如下:
@Override 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: if(父容器需要当前点击事件){ intercepted = true; } else { intercepted = false; } break; case MotionEvent.ACTION_UP: intercepted = false; break; default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; }
(2)在onInterceptTouchEvent方法中,首先是ACTION_DOW这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,这是因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,这个时候事件没法再传递给子元素了;其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false;最后是ACTION_UP事件,这里必须要返回false,因为ACTION_UP事件本身没有太多意义。
(3)考虑一种情况,假设事件交由子元素处理,如果父容器在ACTION_UP时返回了true,就会导致子元素无法接收到ACTION_UP事件,这个时候子元素中的onClick事件就无法触发,但是父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它来处理,而ACTION_UP作为最后一个事件也必定可以传递给父容器,即便父容器的onInterceptTouchEvent方法在ACTION_UP时返回false.
内部拦截法
(1)内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截法稍显复杂。我们需要重写子元素的dispatchTouchEvent方法:
@Override public boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: parent.requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: int deltaX = x - mLastX; int deltaY = y - mLastY; if(父容器需要此类点击事件){ parent.requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(event);
(2)除了子元素需要做处理以外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。
为什么父容器不能拦截ACTION_DOWN事件呢?
那是因为ACTION_DOWN事件并不受FLAG_DISALLOW_INTERCEPT这个标记位的控制,所以一旦父容器拦截ACTION_DOWN事件,那么所有的事件都无法传递到子元素中去,这样内部拦截就无法起作用了。
父容器所做如下所示:
public boolean onInterceptTouchEvent(MotionEvent event) {int action = event.getAction();if (action == MotionEvent.ACTION_DOWN) {return false;} else {return true;}}
ZX