View的事件体系小结
一、view的基础概念
(1)啥为View?
View为Android中所有控件的基类(控件:Button、TextView等),它是界面层控件的一种抽象,我们日常所用的View以及ViewGroup都是继承于view。
View树结构:已知View和ViewGroup都是继承于View,ViewGroup里面可以包含其他子View,这些子View又可以为其他ViewGroup,以此类推可形成View树的结构,这个结构有利于我们去理解View的事件分发机制。
(2)View的位置参数
View的位置主要由其四顶点决定
top:View的左上角纵坐标、left:View的左上角横坐标、right:View的右下角横坐标、bottom:View的右下角纵坐标。需注意这几个坐标都是相对于当前View的父View来说的。
Android中提供了对应的方法来获取四个值:
Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom();
//view的宽度
width = Right - Left
//view的高度
height = Bottom - Top
这里还有几个值需要注意的值(其坐标都是相对于当前view的父容器来说的)
x、y:分别为view的Left和Top变化的后的坐标值。
translationX、translationY:为View左上角相对于父容器的偏移量。
x = Left + translationX
y = top + translationY
(3)MotionEvent(触摸事件)
主要为三种事件:
Action_Down:手指刚接触屏幕
Action_Move:手指在屏幕上移动
Action_Up:手指离开屏幕瞬间
Android提供两钟方法来获取点击事件发生的坐标:
getX/getY:返回相对于当前View左上角的x、y坐标。
getRawX/getRawY:返回相对于手机屏幕左上角的x、y坐标。(全屏滑动使用)
这里还得提到另一个属性:TouchSlop(系统所能识别的滑动的最小距离)
获取这个常量的方法:
ViewConfiguration. get(getContext()).getScaledTouchSlop()。
这个常量定义在frameworks/base/core/res/res/values/config.xml文件中
<dimen name="config_viewConfigurationTouchSlop">8dp</dimen>
(4)VelocityTracker、GestureDetector、Scroller(粗略介绍)
1、VelocityTracker:顾名思义速度追踪器,用来获得手机在屏幕上滑动的速度。
使用方法:
(1)在View的onTouchEvent加上两行代码来记录当前单击事件的速度,至于onTouchEvent这个方法,后续将会讨论到。
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
(2)获取速度,通过以下api来实现
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
在使用getXVelocity()和getYVelocity()获取速度之前,需要调用velocityTracker.computeCurrentVelocity(1000);来计算速度。速度为矢量,所以有方向,手指顺着坐标系正方向来移动,所得到的值为正值。
(3)不使用它时,需要通过以下API来回收
velocityTracker.clear();
velocityTracker.recycle();
2、GestureDetector:手势识别器,用于检测用户单击、滑动、双击等等动作。
使用方法:
//(1)新建手势识别器对象
GestureDetector mGestureDetector = new GestureDetector(this);
//(2)接管目标View的onTouchEvent方法
boolean flag = mGestureDetector.onTouchEvent(event);
return flag;
下面列举几种常用到的方法:(1)onSingleTapUp(检测单击事件) (2)onDoubleTap(检测双击时间) (3)onLongPress(检测长按事件)。
3、Scroller:弹性滑动对象,滑动过程有滑动效果,增加用户体验。
前因:使用scrollTo、scrollBy进行滑动时,都是瞬间完成,体验不佳。scrollTo、scrollBy这两个方法,后面会有具体的解释。
使用方法:
Scroller scroller = 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的滑动方法主要有三种:
(1)scrollTo/scrollBy
以下是上述两个方法的实现代码:
//scrollTo方法的实现
public void scrollTo(int x,int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;//mScrollX的值为View左边缘和View内容左边缘水平方向的距离
int oldY = mScrollY;//mScrolly的值为View上边缘和View内容上边缘水平方向的距离
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX,mScrollY,oldX,oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
//scrollBy方法的实现
public void scrollBy(int x,int y) {
scrollTo(mScrollX + x,mScrollY + y);
}
注意点:
(1)scrollTo和scrollBy只能改变view的内容的位置,不能改变view在布局中的位置。
(2)view的左边缘在view内容左边缘的右边时,mScrollX为正值,反之为负值。
(3)view的上边缘在view内容上边缘的下边时,mScrollY为正值,反之为负值。
(2)使用动画
使用View动画来操作view,主要就是操作View的translationX和translationY属性(移动的还是View的内容),除非用属性动画,才能真正移动View。
View动画的使用:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true" android:zAdjustment="normal" >
<translate android:duration="100"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="100"
android:toYDelta="100" />
</set>
View动画是对View的影像做操作,想要保留动画后的状态,需要把fillAfter属性设为true。
属性动画的使用:
ObjectAnimator.ofFloat(View,"translationX",0,100).setDuration(100).start();
(3)通过布局参数
例:
MarginLayoutParams params = (MarginLayoutParams)mButton1.getLayoutParams();
params.width += 100;// 保持button原本的大小,不被挤压变形。
params.leftMargin += 100;//左外边距增加100px
mButton1.requestLayout();
三种方式对比:
1、scrollTo、scrollBy:对View内容的移动,操作简单,适合无交互的View。
2、动画:(1)View动画:对View内容的移动,适合无交互的View,可以实现相对复杂的效果 。 (2)属性动画:操作View的属性,适合有交互的View,可以实现复杂的效果。
3、改变布局参数:适合有交互的View,但是操作比较复杂。
三、弹性滑动的实现
使用弹性活动的方法主要有三种:
(1)通过Scroller:
Scroller scroller = new Scroller(mContext);//创建对象
// 缓慢滚动到指定位置
private void smoothScrollTo(int destX,int destY) {
int scrollX = getScrollX();//mScrollX的值为View左边缘和View内容左边缘水平方向的距离
int deltaX = destX -scrollX;
// 1000ms内滑向destX
mScroller.startScroll(scrollX,0,deltaX,0,1000);//只是用来保存数据
invalidate();
}
startScroll()方法的具体实现:(可以得知,只起到保存数据的作用)
public void startScroll(int startX,int startY,int dx,int dy,int duration){
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
startScroll方法只是起到保存数据的作用。invalidate方法才是真正实现View的弹性滑动,其原因是:invalidate会导致View的重绘,所以会调用View的draw方法,View的draw方法又会调用computeScroll()方法,接下来看computeScroll()的具体实现。
//这是一个空方法,需要自己来实现
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
由上述函数可发现,最后还是调用了scrollTo方法,来实现View的滑动,然后再调用postInvalidate()来实现重绘,并没有看到弹性是在哪里实现,所以我们把问题定位到mScroller.computeScrollOffset()上。接下来来看下computeScrollOffset()的一个实现。
public boolean computeScrollOffset() {
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() -mStartTime);//经过的时间
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed *
mDurationReciprocal);//插值器,根据时间流逝的百分比计算出动画改变的百分比
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
}
}
return true;
}
通过上述函数可得知,在一段时间内,computeScrollOffset函数会根据时间的流逝计算出View当前移动到哪个位置,所以当前View不会出现瞬间移动到的情况,弹性滑动实现。
(2)通过动画
前因:总所周知,动画本来就是随着时间流逝慢慢播放的,所以其本身以实现弹性滑动的效果
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
这里的动画只是一个粗略的讲解,有兴趣可以关注稍后写的关于动画的小结。
(3)通过延时操作
实现原理:通过不断发送延时消息来更新UI,从而实现View的弹性滑动。
实现方法:使用Handler或者View的postDelay方法
示例:通过Handler来实现
private static final int MESSAGE = 1;
private static final int COUNT = 50;
private int mCount = 0;
private Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE: {
mCount++;
if (mCount <= COUNT) {
float fraction = mCount / (float) COUNT;
int scrollX = (int) (fraction * 100);
mButton1.scrollTo(scrollX,0);
mHandler.sendEmptyMessageDelayed(MESSAGE,
50);
}
break;
}
default:
break;
}
};
};
总结:其实以上三种方法的本质都是一样的,要想实现弹性滑动,就不能让View的滑动瞬间完成,通过给View设置一定的时间慢慢移动,弹性滑动效果实现。
四、View的事件分发机制
1、点击事件的传递规则
首先要了解点击事件,先要了解事件分发过程中的三个重要方法:
(1)public boolean dispatchTouchEvent(MotionEvent ev)
事件传给当前View,则此方法一定会被调用,至于这个方法返回false或者返回true,由当前View的onTouchEvent和下级View(如果有下级View的话)的dispatchTouchEvent影响,表示是否消耗当前事件。
(2)public boolean onInterceptTouchEvent(MotionEvent event)(此方法存在于ViewGroup中)
此方法表示是否拦截某事件,如果拦截某事件,那么在同一事件序列中(例如:down-move-move-up),此方法不会被再次调用,返回的结果表示是否拦截当前事件。
(3)public boolean onTouchEvent(MotionEvent event)
用来处理点击事件,如果不消耗,在同一事件序列中,当前View无法再接收到事件。
优先级问题:onTouchListener>onTouchEvent>onClickListener
补充几个概念:
(1)事件序列:手指从接触到屏幕,到离开屏幕,这个过程所产生的一系列事件。以down开始,以move结束。
(2)正常情况下,一旦某个View拦截了某事件,那么这个事件序列都会交给这个View来处理,除非这个View又在onTouchevent把事件抛出。
(3)如果当前View不消耗除ACTION_DOWN以外的事件,此点击事件会消失,父元素的onTouchEvent也不会被调用,当前View可以接收到后续事件,消失的点击事件最后会交给Activity来处理。
(4)View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
(5)onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件
2、深入解析事件分发机制
1、Activity对点击事件的分发过程:当一个点击事件发生时,事件是最先传递给当前Activity,接下来看看它的一个代码实现
Activity的dispatchTouchEvent:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
流程
(1)点击事件首先传递到Activity,然后Activity的dispatchTouchEvent方法被调用。
(2)在上述代码中可看到做了一个这样的判断if (getWindow().superDispatchTouchEvent(ev)) ,这是把事件交给Activity所附属的Window进行分发。
(3)来看看getWindow().superDispatchTouchEvent(ev)这个方法的一个实现,从代码上可得知window是一个抽象类,而他的方法superDispatchTouchEvent也是抽象方法。所以要找到它们的具体实现,分析Android源码可得知,Window在Android中的唯一实现类就是PhoneWindow。
(4)来到PhoneWindow中,找到superDispatchTouchEvent方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
从代码中可得知,将事件交给mDecor来处理,这个mDecor就是DecorView,至于DecorView在我的另一篇博客《View的工作原理小结》有提到,这里就不再详解。
(5)现在事件传递到DecorView(DecorView本身为一个ViewGroup)接下来的流程就是常规的事件分发流程。接下来附图详解这个流程:
2、FLAG_DISALLOW_INTERCEPT:这个标记位,能让子View控制父ViewGroup无法拦截除ACTION_DOWN之外点击事件,为何除了ACTION_DOWN? 拦截ACTION_DOWN,会重置FLAG_DISALLOW_INTERCEPT这个标志位,导致这个标志位无效。所以要想使用这个标志位阻止父ViewGroup拦截事件,需要让父ViewGroup不拦截ACTION_DOWN。
//父ViewGroup在拦截ACTION_DOWN后所作的操作。
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();//重置标志位
}
子View通过调用
requestDisallInterceptRouchEvent(boolean disallowIntercept)
来改变这个标志位。
3、view能否接受点击事件有两点来衡量:
(1)子元素是否在播放动画。
(2)点击事件的坐标是否落在子元素的区域内。
4、View对点击事件的处理过程(这里指的是非ViewGroup)
View的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this,event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
从上面函数可得知,首先会判断有没有OnTouchListener,如果onTouchListener中的onTouch方法返回true,那么View的onTouchEvent就不会被调用。
接下来看看View的onTouchEvent方法
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
从上述可得知,就算View处于不可用的状态,还是会消耗点击事件。
接下来看看onTouchEvent中对点击事件的处理
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
if (!mHasPerformedLongPress) {
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
...
}
break;
}
...
return true;
}
首先只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true,不管这个View是不是可用(DISABLE)。然后当ACTION_UP事件发生时,会触发performClick方法,当View中有设置OnClickListener,performClick方法就会调用onClick方法。
五、View的滑动冲突
起因:我们的布局经常是View嵌套View,不同的View又接收不同滑动事件,所以哪个滑动由哪个View来处理显得至关重要。
1、常见的滑动冲突场景(三种)
(1)内外滑动方向不一致
(2)内外滑动方向一致
(3)第一第二两种情况的混合
处理原则:具体场景,具体分析。判断在具体情况下,应该由哪个View来处理这个事件。
处理滑动冲突的方法:
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: {
if (父容器需要当前点击事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
} default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
对于ACTION_DOWN,如果ViewGroup拦截了,那么接下来的整个事件序列都会交给它来处理,所以一般返回为false,对于ACTION_UP一般ViewGroup都要返回false(不管拦不拦截事件),一但返回true会导致子元素中的onClick事件无法触发。
2、内部拦截法
内部拦截法是指父容器不拦截任何事件,全部传给子元素去处理,子元素需要就消耗掉,不然最后还是会传递给父容器处理。这种方法需要通过上文所说到的一个标志位来帮忙实现:FLAG_DISALLOW_INTERCEPT,通过parent.requestDisallowInterceptTouchEvent(true);这个方法来控制父容器不拦截事件,伪代码如下所示:
//子元素的dispatchTouchEvent方法
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);
}
除了子元素需要处理,还要记得父容器不能拦截ACTION_DOWN,至于原因,上面已经提及过了。