- 1.事件分发流程图(究极重点)
- 2.在遍历子View时如何从内层的子View开始遍历?
- 3.滑动冲突有哪些场景?滑动冲突处理原则是什么?
- 4.ACTION_POINTER_DOWN ,event.getX(int index)什么时候发生?
- 5.View的滑动方式有哪些?
- 6.ScrollView里面有一个button,然后按住button向上滑,讲述事件传递过程?
- 7.按住一个button,然后手指移到别处,click事件还能不能响应?
- 一般来说,一组事件序列为
ACTION_DOWN(一个,手指点下)->ACTION_MOVE(N个,手指移动)->ACTION_UP(一个,手指抬起)
,必须以DOWN事件开始,UP事件结束。 - 当一个View消费事件后,后续的事件都直接交由它去处理但有两种情形需要注意:
- ViewGroup进行了拦截,后续事件将交由ViewGroup的onTouchEvent去处理。
- 可以在处理事件的View的onTouchEvent()中手动的去调用其他View的onTouchEvent()将事件强行传递给其他View处理,但这样违背了事件分发的本质。
- (这里指的消费事件其实并不是onTouchEvent返回true而是ACTION_DOWN事件时是否返回true,当一个View消费事件后后续的MOVE和UP事件都交由当前这个消费了事件的View去处理。)
- onInterceptTouchEvent在DOWN事件和MOVE事件返回true进行拦截其实是非常没有必要的,如果在DOWN拦截那么后续事件都不会交由子View去判断,在UP事件拦截那么消费了事件的子View的UP事件将无法进行响应。
- 如果onInterceptTouchEvent在MOVE事件返回true的话那么首先会发送一个ACTION_CANCEL事件给原先处理事件的View,之后后续的MOVE和UP事件将直接发送给其自身的onTouchEvent去处理且其自身的onInterceptTouchEvent将不会被调用。(也就是说onInterceptTouchEvent一旦返回true那么之后的事件将不会在触发其自身的onInterceptTouchEvent方法,onInterceptTouchEvent在返回true以后将不再调用)。
- View的onTouchEvent默认消耗事件(ACTION_DOWN返回true),除非它是不可点击的(clickable和longClickable同时为false),如果View设置了onClickListener则clickable为true,设置了onLongClickListener则longClickable为true。View默认longClickable都为false,clickable非情况(如Button为true,TextView为false)。
- View是否消耗事件顺序:onTouch(setOnTouchListener)->onTouchEvent->setOnClickListener->setOnLongClickListener。
- View的enable不影响onTouchEvent的返回值,View当enable为false时只要clickable为true照样可以消费事件只不过ACTION_UP时不会有任何响应。
可以通过重写**getChildDrawingOrder
**方法去改变遍历规则。
滑动冲突的本质其实是一个策略问题,在开发中我们通常都是通过在子View中去调用requestDisallowInterceptTouchEvent方法配合父View中的onInterceptTouchEvent方法去使用。
下边给出一个例子:
public class MyLayout extends LinearLayout{
public MyLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE: //表示父类需要拦截
return true;
default:
break;
}
return false; //如果设置拦截,除了down,其他都是父类处理
}
}
public class MyButton extends Button {
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);//禁用父View的拦截方法。
break;
case MotionEvent.ACTION_MOVE:
if(满足条件){
getParent().requestDisallowInterceptTouchEvent(false);//解除父View的拦截的禁用。
}
}
return true;
}
}
- 可以看到当 ACTION_DOWN 事件时我们在View自身的onTouchEvent中调用了 getParent().requestDisallowInterceptTouchEvent(true)这个方法,当此方法调用后在方法内部中会改变FLAG_DISALLOW_INTERCEPT标志位为true,这时在ViewGroup中的dispatchTouchEvent中如果检测到FLAG_DISALLOW_INTERCEPT为true的话将跳过onInterceptTouchEvent的调用而直接返回false,也就是父View直接不进行拦截,这时我们的事件都将由子View去处理,同时也不用担心父View的拦截方法会对事件进行拦截,当我们在移动时满足事件可被父View进行拦截时则需要调用getParent().requestDisallowInterceptTouchEvent(false)将父View的拦截方法的禁用解除掉,这时父View的onInterceptTouchEvent将可继续去判断是否需要进行事件的拦截。
在ViewGroup的dispatchTouchEvent中我们可以看到如下代码:
// 发生ACTION_DOWN事件或者已经发生过ACTION_DOWN,并且将mFirstTouchTarget赋值,才进入此区域,主要功能是拦截器
final boolean intercepted;
//onInterceptTouchEvent返回true后之后将不再执行onInterceptTouchEvent方法,因为其将mFirstTouchTarget字段置为了null。
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
//disallowIntercept:是否禁用事件拦截的功能(默认是false),即不禁用
//可以在子View通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改,不让该View拦截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//默认情况下会进入该方法
if (!disallowIntercept) {
//调用拦截方法
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 当没有触摸targets,且不是down事件时,开始持续拦截触摸。
intercepted = true;
}
获取事件时需调用MotionEvent.getActionMasked()而不是MotionEvent.getAction(),只有MotionEvent.getActionMasked()可以支持多点触控。
常见值:
- ACTION_DOWN :第一个手指按下(之前没有任何手指触摸到 View)
- ACTION_UP :最后一个手指抬起(抬起之后没有任何?指触摸到 View,这个手指未必是 ACTION_DOWN 的那个手指)
- ACTION_MOVE 有手指发生移动
- ACTION_POINTER_DOWN 额外手指按下(按下之前已经有别的手指触摸到 View)
- ACTION_POINTER_UP 有手指抬起,但不是最后一个(抬起之后,仍然还有别的手指在触摸着 View)
默认的event.getX()其实可以理解为 event.getX(0),这是针对于一根手指的情况,再多点触控的情况下我们需要通过调用event.getX(int index)来传入参数以区别当前是第几根手指在进行移动 (这里的index是会变的,但是手指的ID是不会变的,我们需要通过ID找到对应手指的index)。
大致可分为下边三个方法(只有layout方法是可以真正改变View坐标位置)
对View进行重新布局定位。在onTouchEvent()方法中获得控件滑动前后的偏移。然后通过layout方法重新设置。
// 视图坐标方式
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录触摸点坐标
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
// 计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
// 在当前left、top、right、bottom的基础上加上偏移量
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
// offsetLeftAndRight(offsetX);
// offsetTopAndBottom(offsetY);
break;
}
return true;
}
本质是View内容的移动,需要通过父容器的该方法来滑动当前View,Scroller: 平滑滑动,通过重载computeScroll(),使用scrollTo/scrollBy完成滑动效果,Scroller只是一个移动的机制,真正还是需要调用去scrollTo/scrollBy去进行移动。
Scroll中与之相关的各种API中的参数都要跟实际我们认知相反,比如想往自身右边移动100不是去调用scrollerBy(100,0)而是调用scrollerBy(-100,0)。
public class ScrollButton extends android.support.v7.widget.AppCompatButton {
Scroller scroller;
int direction = -1;
public ScrollButton(Context context) {
this(context,null);
}
public ScrollButton(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public ScrollButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
scroller = new Scroller(context);
}
@Override
public void computeScroll() {
if(scroller!=null){
if(scroller.computeScrollOffset()){//判断scroll是否完成
((View) getParent()).scrollTo(
scroller.getCurrX(),scroller.getCurrY()
);//执行本段位移
invalidate();//进行下段位移
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
scroller.startScroll(((int) getX()), ((int) getY()), ((int) getX())*direction,
((int) getY())*direction);//开始位移,真正开始是在下面的invalidate
direction*=-1;//改变方向
invalidate();//开始执行位移
break;
}
return super.onTouchEvent(event);
}
}
动画对View进行滑动: setTranslationX,setTranslationY。
当手指按下时由于scrollView中onInterceptTouchEvent没对down事件进行拦截同时button的onTouchEvent是默认返回true的(clickable=true)那么button首先会消耗down事件,当我们手指移动时会触发MOVE事件,这时ScrollView的拦截事件将进行拦截(onInterceptTouchEvent在MOVE时返回true)同时会发送 CANCLE事件给button(CANCLE的触发时机是父View进行拦截后会发送给原先处理事件的子View通知它不要处理后续事件了),之后的MOVE和UP事件将直接交由ScrollView的onTouchEvent去处理同时其自身的onInterceptTouchEvent不会再被触发(onInterceptTouchEvent返回true后将不被调用)。
不能响应。
当手指移动时在View的OnTouchEvent的MOVE事件中会不断检测当前手指是否在View区域内,如果出了View区域的话那么会将mPressed这个标志位置为false,当手指抬起时在UP事件中如果mPressed为false的话将不会触发任何响应(一定要注意的是会触发MOVE和UP事件,因为一个View在DOWN事件返回true后后续的事件序列都会交给其去处理,只不过在这种情况下没有任何响应效果)。
View的onTouchEvent:
public boolean onTouchEvent(MotionEvent event) {
....
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
//如果mPrivateFlags为false则prepressed为false,将不会执行后续UP事件中的任何逻辑
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
....
}
break;
...
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
//判断手指是否在View的区域中
if (!pointInView(x, y, mTouchSlop)) {
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
removeLongPressCallback();
//如果手指移出View区域将改变mPrivateFlags
setPressed(false);
}
}
break;
}
return true;
}
return false;
}