一、基础知识
1.1什么是View
View是所有控件的基类,ViewGroup包含很多View,即一组View,而ViewGroup又继承View,所以View既可以使单个的控件,也可以是一组控件;
1.2.View的位置参数
1.View的位置由它的四个顶点决定,它们分别对应left、top、right、bottom。left是左上角横坐标,top是左上角纵坐标,right是右下角横坐标,bottom是右下角纵坐标;这几个坐标都是相对于父控件而言的相对坐标
2.view的宽度和坐标的关系:
width = right-left;
height = bottom-top;
3.获取坐标:
left = getLeft();
top = getTop();
right = getRight();
bottom = getBottom();
4.从android3.0开始,新增了几个属性x,y,translationX,transelationY
x和y是view左上角的坐标,translationX和translationY是view左上角相对于父容器的偏移量;
坐标关系:
x = left + transelationX;
y = right + translationY;
①left和right是view的初始坐标,view绘制完成后就不会改变;
②x和y是view移动后左上角的坐标,会实时改变;
1.3.触控系列
1.MotionEvent是手指接触屏幕后产生的一系列事件:
ACTION_DOWM:手指刚接触屏幕
ACTION_MOVE:手指在屏幕上移动
ACTION_UP:手指离开屏幕的一瞬间
事件列:从手指接触屏幕开始到离开屏幕这中间发生的一系列事件,都是从Down开始,Up结束,中间有无数个move事件;
通过MotionEvent可以得到当前点击事件发生的x、y坐标,getX()/getY()、getRawX()/getRawY();
getX()/getY()是相对于当前view的左上角坐标;
getRawX()/getRawY()是相对于手机屏幕的左上角坐标
2.TouchSlop
是指系统所能识别的滑动的最小距离,也就是说,滑动距离小于这个值,系统就不认为是在滑动;
这是一个数值,但是在不同手机上的值不同;
获取方式:
int small = ViewConfiguration.get(this).getScaledTouchSlop();//16
3.VelocityTracker
指速度追踪,手指在滑动过程中的速度,可以检测水平和垂直方向的速度,速度可以为负数(从右往左滑动的时候)
使用:
在onTouchEvent事件中:
//创建VelocityTracker实例
VelocityTracker velocityTracker = VelocityTracker.obtain();
//监听滑动事件
velocityTracker.addMovement(event);
//计算当前的速度(参数是ms),单位时间内滑动的像素个数,比如当前滑动了100个像素点,也就是说1s滑动100个像素点
velocityTracker.computeCurrentVelocity(1000);
//获取水平速度
int xVelocity = (int) velocityTracker.getXVelocity();
//获取垂直速度
int yVelocity = (int) velocityTracker.getYVelocity();
//不用的时候要回收
velocityTracker.clear();
velocityTracker.recycle();
4.GetureDetecor
手势检测,用于监听单击、双击、长按、滑动事件
使用:
//监听手势
final GestureDetector gestureDetector = new GestureDetector(this);
//监听双击事件
gestureDetector.setOnDoubleTapListener(this);
mButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
});
方法名 | 描述 | 事件 | 接口 |
---|---|---|---|
onDown | 手指轻触屏幕的一瞬间 | 1个onDown | |
onShowPress | 手指轻触屏幕,还没有松开或移动 | 1个ACTION_DOWN | OnGestureListener |
onDown | 手指轻触屏幕的一瞬间 | 1个ACTION_DOWN | OnGestureListener |
onSingleTapUp | 手指轻触屏幕后松开 | 1个ACTION_UP | OnGestureListener |
onScroll | 手指按下屏幕后移动 | n个ACTION_MOVE | OnGestureListener |
onFling | 触摸屏幕,快速滑动后松开 | 1个ACTION_Down,n个ACTION_MOVE,1个ACTION_UP | OnGestureListener |
onLongPress | 长按屏幕 | 1个onDown | OnGestureListener |
onSingleTapConfirmed | 严格的单击事件,触发了它,后面就不可能再跟着一个单击事件 | OnDoubleTapListener | |
onDoubleTap | 双击事件,和onSingleTapConfirmed不共存 | OnDoubleTapListener | |
onDoubleTapEvent | 表示双击行为发生了 | ACTION_DOWN、ACTION_MOVE、ACTION_UP都会回调这个方法 | OnDoubleTapListener |
建议:如果只是监听滑动相关,建议自己在onTouchEvent()中实现;如果要监听双击事件,就用OnGestureListener
5.Scroller
弹性滑动对象
二、View的滑动
2.1使用ScrollTo/ScrollBy
- ScrollTo/ScrollBy只能改变View内容的位置,而不能改变View在布局中的位置;
- ScrollBy的内部调用了ScrollTo
- 二者的区别:ScrollBy是基于当前位置的相对滑动,ScrollTo是基于所传位置的绝对滑动;
mScrollX和mScrollY分别表示View在X、Y方向的滚动距离。mScrollX:View的左边缘减去View的内容的左边缘;
mScrollY:View的上边缘减去View的内容的上边缘。
从右向左滑动,mScrollX为正值,反之为负值;
从下往上滑动,mScrollY为正值,反之为负值。
(更直观感受:查看下一张照片或者查看长图时手指滑动方向为正)
2.2使用动画
//方法1 使用View动画
mButton.startAnimation(AnimationUtils.loadAnimation(this,R.anim.move));
//方法2:使用属性动画
ObjectAnimator.ofFloat(mButton,"translationX",0,100).setDuration(100).start();
区别:
- View动画是对View的影像做操作,并不能真正改变View的位置参数和宽高,移动后就无法触发onClick事件,在原来的位置还可以触发;
- 属性动画可以解决这个问题,但是3.0以下无法使用属性动画,可以使用nineoldandroids来实现属性动画,但是本质上仍然是View动画
2.3改变布局参数
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mButton.getLayoutParams();
params.leftMargin += 100;
//mButton.requestLayout();
mButton.setLayoutParams(params);
2.4三种方式的对比
- ScrollTo和ScrollBy可以方便实现滑动,但是只能滑动View的内容,不能滑动View;
- 动画:操作简单,适合于没有交互的View或者是复杂的动画
- 改变布局参数:操作稍微复杂,适合有交互的View
三、弹性滑动
核心思想:将一个大的滑动,分成若干个小的滑动,并在一段时间内完成;
3.1使用Scroller
典型的使用方法:
/**
* 缓慢滑动
* @param destX
* @param destY
*/
private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();//view左边缘和view内容左边缘的距离
int scrollY = getScrollX();//view上边缘和view内容上边缘的距离
int deltaX = destX - scrollX;//x方向上的位移量
int deltaY = destY - scrollY;//y方向上的位移量
scroller.startScroll(scrollX,scrollY,deltaX,deltaY,1000);//开始滑动
invalidate();//重绘View,刷新界面
}
startScroll源码:
只是将我们传进去的数据进行保存,这里仍然是view内容的滑动;
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;//x滑动距离
mDeltaY = dy;//y滑动距离
mDurationReciprocal = 1.0f / (float) mDuration;
}
使用场景:在ACTION_UP触发的时候会调用startScroll()进行数据的保存--->调用invalidate重绘view--->view的draw()方法会调用computeScroll(),这个方法要自己实现;
实现过程:先判断computeScrollOffset,若为true(表示滚动未结束),则执行scrollTo方法,它会再次调用postInvalidate,如此反复执行,直到返回值为false。
@Override
public void computeScroll() {
super.computeScroll();
if (scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),scroller.getCurrY());
postInvalidate();
}
}
3.2使用动画
只能改变view的内容
final int startX = 0;
final int deltaX = 100;
final ValueAnimator valueAnimator = ValueAnimator.ofInt(0,1).setDuration(1000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//获取当前动画的执行进度
float fraction = valueAnimator.getAnimatedFraction();
Log.e("testslide","fraction:"+fraction);//0.008856356-->1.0
mButton.scrollTo((int) (startX+deltaX*fraction),0);
}
});
valueAnimator.start();
3.3使用延时策略
- 使用handler
- 使用sleep
private static final int MESSAGE_SLIDE = 0;
private int allCount = 30;
private int delayTIme = 33;
private int count;
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
case MESSAGE_SLIDE:
count++;
if (count < allCount){
//完成进度,注意要用float
float fracsaction = count/(float)allCount;
mButton.scrollTo((int) (fracsaction*100),0);
//33ms后再发送一次消息
sendEmptyMessageDelayed(MESSAGE_SLIDE,delayTIme);
}
break;
default:
break;
}
}
};
public void slideByDelay(View view) {
mHandler.sendEmptyMessage(MESSAGE_SLIDE);
}
四、事件分发机制
4.1点击传递规则
点击事件的事件分发,实质上就是MotionEvent事件的分发,当一个点击事件发生的时候,系统需要将这个事件传递给一个具体的View,这个过程就叫事件分发;
三个重要方法:
①dispatchTouchEvent(MotionEvent event):
用于事件的分发,如果事件能传递给当前View,则这个方法一定会被调用,它的返回结果受当前view的onTouchEvent()和下级view的dispatchTouchEvent()的影响,表示是否消耗当前事件;
②onInterceptTouchEvent(MotionEvent ev)
在dispatchTouchEvent中调用,该方法只在ViewGroup中有,表示是否拦截这个事件,如果拦截了,那么在同一个事件序列中,此方法不会再被调用,返回结果表示是否拦截了当前事件;
③onTouchEvent(MotionEvent event)
在dispatchTouchEvent中调用,用于处理点击事件,返回结果表示是否消耗了当前事件,如果不消耗,那么在同一个事件序列中,当前view无法再次接收到事件;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume;
if (onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
传递规则:对于根ViewGroup而言,当一个点击事件发生的时候,首先会传递给它,它的dispatchTouchEvent会被调用,如果这个ViewGroup的onInterceptTouchEvent返回true,表示ViewGroup拦截了这个事件,会调用自己的onTouchEvent方法进行处理;否则,会调用子元素的dispatchTouchEvent,如此下去,知道该事件被消费;
注意
-
①当一个View设置了setOnTouchListener,那么它的onTouch事件会被回调,这时候事件如何处理,要看onTouch的返回值,(返回值默认为false),返回值为true时,onTouchEvent()不会被调用;返回值为false,onTouchEvent()会被调用;在onTouchEvent()中,如果设置了onClickListener(),则它的onClick会被调用,也就是说onClickListener的优先级最低,处于事件传递的底端。
优先级:onTouchListener > onTouchEvent() > onClickLisener()
-
②当一个点击事件产生的时候,传递顺序是:
Activity-->Window-->View,传到顶级View后,再按事件分发机制分发,如果这个View的onTouchEvent()返回false,那么它父容器的onTouchEvent()将会被调用,如果所有元素都不处理,那么最后将会传到Activity处,则Activity的onTouchEvent()会被调用。
理解:我的上级接到了一个任务,他不想做,就分给我了,会调用我的onTouchEvent(),最后我实在解决不了了,只能返回false,然后让上级去解决,然后会调用上级的onTouchEvent();
结论:
- ①同一个事件序列,是指手指接触屏幕的一瞬间起,到离开屏幕的一刻为止,中间发生的一系列事件,以DOWN开始,中间经过n个MOVE事件,以UP结束;
- ②某个view一旦决定拦截,那么这个事件序列都由他来处理,并且它的onInterceptTouchEvent()不会再被调用了;
- ③正常情况下,一个事件序列只能被一个View拦截且消耗;
- ④某个View一旦开始处理事件,如果它不处理ACTION_DOWN事件(也就是说它的onTouchEvent()返回false),那么同一事件序列中的其他事件都不会再交于它处理,并且把事件交由它的父类处理;
- ⑤如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,同时不会调用父类的onTouchEvent()方法,后续还是会持续收到事件,这些消失的事件最后会传递给Activity处理;
- ⑥ViewGroup默认不拦截任何事件,onInterceptTouchEvent()默认返回false;
- ⑦View没有onInterceptTouchEvent(),一旦有事件传过来,就会调用onTouchEvent();
- ⑧View的onTouchEvent()默认都会消耗该事件(返回true),除非它是不可点击的(clickable和longClickable为false),View的longClickable默认为false,clickable要分情况:Button默认为true,TextView默认为false;
- ⑨View的enable不影响onTouchEvent()的返回值,即便一个View enable为false,只要clickable或longClickable有一个为true,onTouchEvent()返回值就为true;
- ⑩onClick发生的前提是当前view可点击,并且受到了down和up事件;
- 11.事件传递是由外到内的,总是由父类分发给子view,通过
requestDisallowInterceptTouchEvent()
可以影响父元素的分发,但是ACTION_DOWN除外;
4.2源码分析
①当一个点击事件发生的时候,事件最先传递给Activity,Activity的dispatchTouchEvent()会调用进行事件分发:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//②交给附属的Window进行分发,如果返回true,则结束循环,否则,这个事件没人处理,所有view的onTouchEvent()返回false,就会交给Activity的onTouchEvent()处理;
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
window是一个抽象类,superDispatchTouchEvent也是一个抽象方法,它的实现类是PhoneWindow;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
在PhoneWindow中,将事件传递给了mDecor;
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
那什么是DectorView呢?
通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);
可以获取当前Activity设置的View,那么getWindow().getDecorView()
获取到的就是DectorView,Activity通过setContentView设置的View是DectorView的子View;
事件传递到DectorView后,查看DectorView的源码:
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
.....
}
DecorView 继承FrameLayout 且是父View,最终事件会传递给View。从这里开始事件已经传递到了顶级View(根View),即通过setContentView设置的View。
public class FrameLayout extends ViewGroup {
...
}
顶级View对点击事件的分发:
点击事件到达顶级View后,会调用ViewGroup的dispathchTouchEvent()方法,如果onInterceptTouchEvent返回true拦截了该事件,则事件交由ViewGroup处理;这时,如果ViewGroup设置了onTouchListener,会调用onTouch()方法,不会调用onTouchEvent()方法,否则,调用onTouchEvent()方法,如果设置了onClickListener事件,则onClick会被调用;如果ViewGroup没有拦截该事件,则会传给给所在点击事件链上的子View,这时,子View的dispatchTouchEvent()会被调用,一直循环,直到该事件被消耗;
在ViewGroup的DispatchTouchEvent()中有这样一段代码:
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
从上面的二代码可以看到有两种情况下回判断是否拦截当前事件,actionMasked == MotionEvent.ACTION_DOWN或者mFirstTouchTarget != null;
从后面的代码可以知道当前事件由ViewGroup的子View处理时,mFirstTouchTarget !=null,也就是说,当ViewGroup自己处理的时候,mFirstTouchTarget =null。
当ViewGroup拦截了当前事件,当MOVE事件到来的时候,if(true || false ){}会执行,当MOVE/UP事件再来的时候,if(false || false),则else中的内容会执行,onInterceptTouchEvent再不会被调用,所以证明了上面的那条结论,③正常情况下,一个事件序列只能被一个View拦截且消耗;
这里也有一种特殊情况,就是FLAG_DISALLOW_INTERCEPT这个标志位,它是在子View中通过requestDisallowInterceptTouchEvent(true);来设置的,它的源码是:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
....
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
...
}
如果在子View中设置了requestDisallowInterceptTouchEvent,则ViewGoup只能拦截Down事件,因为在ViewGroup的onDispatchTouchEvent()中,Down事件到来的时候会调用resetTouchState();重置FLAG_DISALLOW_INTERCEPT标志位,也就是说,即便这时候子View设置了requestDisallowInterceptTouchEvent也无效,down事件来的时候总是会调用ViewGroup的onInterceptTouchEvent方法
①设置了true
mGroupFlags = mGroupFlags | FLAG_DISALLOW_INTERCEPT;
②down事件来的时候会重置标志位:
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
-->mGroupFlags = mGroupFlags &( ~FLAG_DISALLOW_INTERCEPT);
-->mGroupFlags = mGroupFlags | FLAG_DISALLOW_INTERCEPT &( ~FLAG_DISALLOW_INTERCEPT);
-->boolean disallowIntercept = (mGroupFlags | FLAG_DISALLOW_INTERCEPT &( ~FLAG_DISALLOW_INTERCEPT) & FLAG_DISALLOW_INTERCEPT)
假设mGroupFlags一开始为000,FLAG_DISALLOW_INTERCEPT为111
则boolean disallowIntercept = (000 | 111 & (~111) & 111) = 0
所以down事件来的时候,disallowIntercept = false-->onInterceptTouchEvent()
③UP或MOVE事件来的时候,不会重置标志位:
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
-->mGroupFlags = mGroupFlags | FLAG_DISALLOW_INTERCEPT;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT)
-->boolean disallowIntercept = (mGroupFlags | FLAG_DISALLOW_INTERCEPT & FLAG_DISALLOW_INTERCEPT)
假设mGroupFlags一开始为000,FLAG_DISALLOW_INTERCEPT为111
则boolean disallowIntercept = (000 | 111 & 111) = 1 -->true
-->
intercepted = false;
根据上面的源码可以得出结论:
①当ViewGroup决定拦截该直接后,后续的点击事件都会交给它处理;
②FLAG_DISALLOW_INTERCEPT是为了让ViewGroup不再拦截该事件,前提是 不拦截down事件
当ViewGroup不拦截这个事件的时候,会交给它的子View进行处理
if (!canceled && !intercepted){
.....
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
.....
}
根据代码可知,会先遍历ViewGroup的子View,如果子元素在播放动画,并且触摸点在子元素的范围内,则子元素能够接收这个事件,然后调用dispatchTransformedTouchEvent(),在它的内部有这样一段代码:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
可以知道,dispatchTransformedTouchEvent()内部其实调用的是dispatchTouchEvent,如果dispatchTransformedTouchEvent()返回值为true,这会执行以下这段代码:
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
....
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
...
}
| |
| |
↓ ↓
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
根据上面的代码可以知道,这时候mFirstTouchTarget 就被赋值了,同时会跳出for循环(不再执行下面的子View了),如果dispatchTransformedTouchEvent()返回值false,就会把事件分给下一个子元素(如果有的话);
如果遍历了所有的子元素后都没有被合适的处理这个事件,那么有两种情况:
①这个ViewGroup没有子View
②dispatchTransformedTouchEvent()返回false,这一般都是子View的onTouchEvent()返回false;
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
↓ ↓ ↓
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
上面的这段代码就证明了:
④某个View一旦开始处理事件,如果它不处理ACTION_DOWN事件(也就是说它的onTouchEvent()返回false),那么同一事件序列中的其他事件都不会再交于它处理,并且把事件交由它的父类处理;
说完顶级View的事件分发之后,再来看看View对点击事件的处理过程:
注意:这里的View不包括ViewGroup
先分析它的dispatchTouchEvent()方法:
public boolean dispatchTouchEvent(MotionEvent event) {
....
boolean result = false;
....
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
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;
}
因为View是一个单一的元素,他没有子元素可以向下传递,所以事件传到他这的时候只能自己处理;根据①②可知,会先判断View是否设置了onTouchListener事件,如果设置了,并且onTouch()返回true,则②中的 onTouchEvent(event)不会执行,这里注意&&中只要前者为false,后面的就不会再执行;
结论:onTouch的优先级高于onTouchEvent
接下来看看View的onTouchEvent事件,先看看不可点击的情况下:
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
根据上面的代码可以看到,虽然此时View不可点击,但是它仍然会消耗点击事件(仍然走了onTouchEvent);
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
当View设置了代理的时候,会执行代理的onTouchEvent()事件,和onTouchListener一样;
下面看看View对具体的事件是如何处理的:
/**
* <p>Indicates this view can display a tooltip on hover or long press.</p>
* {@hide}
*/
static final int TOOLTIP = 0x40000000;
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
....
}
break;
....
}
return true;
}
从上面的代码可以看出,只要是点击或长按事件中的一个为true,都会
消耗这个事件,onTouchEvent()都会返回true,不管它设置的是不是disable状态,也就证明了下面的几个结论:
⑧View的onTouchEvent()默认都会消耗该事件(返回true),除非它是不可点击的(clickable和longClickable为false),View的longClickable默认为false,clickable要分情况:Button默认为true,TextView默认为false;
⑨View的enable不影响onTouchEvent()的返回值,即便一个View enable为false,只要clickable或longClickable有一个为true,onTouchEvent()返回值就为true;
⑩onClick发生的前提是当前view可点击,并且受到了down和up事件
在上面的代码中,可以看到,当UP事件发生的时候,会执行PerformClick
private final class PerformClick implements Runnable {
@Override
public void run() {
performClick();
}
}
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
从上面代码可以看到,当设置了onClickListener事件的时候,会执行onClick方法;
View的LONG_CLICKABLE默认为false,CLICKABLE的值要根据具体的View来定,比如TextView是不可点击的,Button是可点击的;
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
通过源码可以看到,可以通过这两个方法设置点击可长按事件,并且在设置的时候,会自动将LONG_CLICKABLE和CLICKABLE设置为true
五、滑动冲突
5.1常见的冲突场景
①外部滑动和内部滑动的方向不一致
②外部滑动和内部滑动的方向一致
③上面两种情况嵌套
5.2处理规则
①对于场景1,左右滑动的时候,由内部控件处理;上下滑动的时候由外部控件处理;至于如何判断是水平滑动还是垂直滑动,可以计算水平滑动差和竖直滑动差;
②场景2和场景3都比较复杂,不能根据角度和滑动的方向来判断谁拦截,只能根据具体的业务需求来定;
5.3处理方式
①对于场景1有两种方式处理:
- 外部拦截法:
即在外部控件判断是否拦截
外部拦截:
在父容器中重写onInterceptTouchEvent()方法:
伪代码如下:
Public boolean onInterceptTouchEvent(Event event){
boolean intercepted = false;
//获取开始的相对坐标
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case down:
intercepted = false;//如果为true,那么后续的事件就都会交给父容器处理了
break;
case move:
//水平差
float detX = mLastXIntercept - x;
//垂直差
float detY = mLastYIntercept - y;
if(Math.abs(detX) > Math.abs(detY)){
intercepted = true;
}else{
intercepted = false;
}
break;
case up:
//这里必须返回false,因为这里没有太大的意义
intercepted = false;
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
问题:如果在up时返回了true会怎么样?
答:up时返回true,也就是说这个点击事件还是会交给父容器处理,子元素是接收不到这个事件的,它的onClick()也就执行不了了。
- 内部拦截法:
即父容器全部都不拦截,都交由子View,如果子View需要处理这事件,就消耗;
重写子View的dispatchTouchEvent():
伪代码如下:
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//不拦截,因为对于父容器来说,设置了也无效
getParent().requestDisallowInterceptTouchEvent(false);
break;
case MotionEvent.ACTION_MOVE:
//水平差
int deltaX = x - mInterceptX;
//垂直差
int deltaY = y - mInterceptY;
if (Math.abs(deltaX) > Math.abs(deltaY)){
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
mInterceptX = x;
mInterceptY = y;
return super.dispatchTouchEvent(event);
}
父容器中要这样处理:
public boolean onInterceptTouchEvent(MotionEvent ev) {
int event = ev.getAction();
if (event == MotionEvent.ACTION_DOWN){
//如果是down事件不拦截
return false;
}else {
return true;
}
}