内容简介:事件分发机制虽然大家都知道是什么东西,但有可能其中的一些细节重点要点还是不清晰,本文将结合实例带你攻克事件分发。可以通过重写滑动冲突的本质其实是一个
- 1.事件分发流程图(究极重点)
- 2.在遍历子View时如何从内层的子View开始遍历?
- 3.滑动冲突有哪些场景?滑动冲突处理原则是什么?
- 4.ACTION_POINTER_DOWN ,event.getX(int index)什么时候发生?
- 5.View的滑动方式有哪些?
- 6.ScrollView里面有一个button,然后按住button向上滑,讲述事件传递过程?
- 7.按住一个button,然后手指移到别处,click事件还能不能响应?
事件分发机制虽然大家都知道是什么东西,但有可能其中的一些细节重点要点还是不清晰,本文将结合实例带你攻克事件分发。
1. 事件分发流程图(究极重点)
- 一般来说, 一组事件序列为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时不会有任何响应。
2.在遍历子View时如何从内层的子View开始遍历?
可以通过重写 getChildDrawingOrder 方法去改变遍历规则。
3.滑动冲突有哪些场景?滑动冲突处理原则是什么?
滑动冲突的本质其实是一个 策略问题 ,在开发中我们通常都是通过在子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; } 复制代码
4.ACTION_POINTER_DOWN ,event.getX(int index)什么时候发生?
获取事件时需调用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) 。
多点触控一般写法实例: github.com/rengwuxian/…
5.View的滑动方式有哪些?
大致可分为下边三个方法(只有layout方法是可以真正改变View坐标位置)
1.layout:
对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; } 复制代码
2.ScrollTo/ScrollBy:
本质是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); } } 复制代码
3.属性动画:
动画对View进行滑动: setTranslationX,setTranslationY。
6.ScrollView里面有一个button,然后按住button向上滑,讲述事件传递过程?
这里的情形可以理解为上图的情景,大家可以自行带入场景。
当手指按下时由于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后将不被调用)。
7.按住一个button,然后手指移到别处,click事件还能不能响应?
不能响应。
当手指移动时在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; } 复制代码
以上所述就是小编给大家介绍的《面试系列之事件分发机制全解析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Android 面试题:Handler、自定义View、Java三大特性、分发机制、动画(第1期)
- View的事件分发(一)分发流程
- Android事件分发机制
- OSPF路由重分发
- 【Leetcode】135.分发糖果
- View事件分发机制分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。