参考资料:
《Android开发艺术探索》
https://www.jianshu.com/p/3d2c49315d68
-
View的事件分发机制,滑动冲突;ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL
-
view的分发机制
点击事件产生后,这个事件被分装成一个类:MotionEvent。系统首先会将事件传递给当前的 Activity,Activity会调用它的 dispatchTouchEvent 方法,将事件交给 PhoneWindow,通过 PhoneWindow 传递给 DecorView,然后再传递给根 ViewGroup,进入 ViewGroup 的 dispatchTouchEvent 方法,执行 onInterceptTouchEvent() ,如果ViewGroup 的 onInterceptTouchEvent() 返回true,表示它要拦截这个事件,false表示不拦截,再不拦截的情况下,此时会遍历 ViewGroup 的子元素,进入子 View 的 dispatchToucnEvent 方法,如果子 view 设置了 onTouchListener,不为null,就执行 onTouch 方法,并根据 onTouch 的返回值为 true 还是 false 来决定是否执行 onTouchEvent 方法,如果是 true,则表示事件被消费了,不会执行onTouchEvent(),如果是 false 则继续执行 onTouchEvent,可见onTouchListener优先级>onTouch>onTouchEvent。在源码中的话可以看到,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,该方法就会返回true消耗这件事,在 onTouchEvent 的 ACTION_UP 事件发生时会触发 performClick(),如果View设置了 onClickListener ,就会调用 performClick() 中的 onClick()。
注意:
-
ViewGroup默认不拦截任何事件
-
View没有onInterceptTouchEvent方法,一旦有点击事件交给他,onTouchEvent()一定会被调用
-
View的onTouchEvent()默认都会消费事件(返回true),除非长短点击都为False
// 发生ACTION_DOWN事件或者已经发生过ACTION_DOWN,并且将mFirstTouchTarget赋值,才进入此区域,主要功能是拦截器 final boolean intercepted; 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.ACTION_DOWN,则进入判断,调用ViewGroup.onInterceptTouchEvent()方法的值,判断是否拦截。如果mFirstTouchTarget != null,即已经发生过MotionEvent.ACTION_DOWN,并且该事件已经有ViewGroup的子View进行处理了,那么也进入判断,调用ViewGroup. onInterceptTouchEvent()方法的值,判断是否拦截。如果不是以上两种情况,即已经是MOVE或UP事件了,并且之前的事件没有对象进行处理,则设置成true,开始拦截接下来的所有事件。这也就解释了如果子View的onTouchEvent()方法返回false,那么接下来的一些列事件都不会交给他处理。如果VieGroup的onInterceptTouchEvent()第一次执行为true,则mFirstTouchTarget = null,则也会使得接下来不会调用onInterceptTouchEvent(),直接将拦截设置为true。
-
-
滑动冲突
https://note.youdao.com/yws/public/resource/328432cea4f2eeddc18f0ca0446558d9/xmlnote/FB40C25074044D4FBD01D94CBF53F043/23669
1.外部拦截法
从父View着手,重写onInterceptTouchEvent方法,在父View需要拦截的时候拦截,不需要则不拦截返回false。其伪代码如下:
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; } 在这里,首先down事件父容器必须返回false ,因为若是返回true,也就是拦截了down事件,那么后续的move和up事件就都会传递给父容器,子元素就没有机会处理事件了。move事件,根据需要决定是否拦截,如果父容器需要拦截就返回false,否则返回true。其次是up事件也返回false,一是因为up事件对父容器没什么意义,其次是因为若事件是子元素处理的,却没有收到up事件会让子元素的onClick事件无法触发。
2.内部拦截法
所有事件都传递给子元素,如果子元素需要就消耗掉,不需要就交给父元素处理,需要子元素配合requestDisallowInterceptTouchEvent方法才能正常工作;此外,父元素需要默认拦截除ACTION_DOWN以外的事件(下文的Flag作用与他相反,一旦子view设置,ViewGroup无法拦截除ACTION_DOWN以外的事件),这样子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截需要的事件。
因为viewGroup在分发事件时,如果是down事件,会重置这个标记,那么子view设置的就会无效(标记在requestDisallowInterceptTouchEvent方法中设置),down事件不受FLAG_DISALLOW_INTERCEPT这个标记的控制,所以一旦父容器拦截down事件,那么所有事件都无法传递到子元素去。
@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); }
然后修改父容器的onInterceptTouchEvent方法:
@Override public boolean onInterceptTouchEvent(MotionEvent event) { int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { return false; } else { return true; } }
-
MotionEvent事件
-
MotionEvent.ACTION_DOWN
当屏幕检测到第一个触点按下之后就会触发到这个事件。
-
MotionEvent.ACTION_MOVE
当触点在屏幕上移动时触发,触点在屏幕上停留也是会触发的,主要是由于它的灵敏度很高,而我们的手指又不可能完全静止(即使我们感觉不到移动,但其实我们的手指也在不停地抖动)。
-
MotionEvent.ACTION_UP
当最后一个触点松开时被触发。
-
MotionEvent.ACTION_CANCEL
不是由用户直接触发,有系统再需要的时候触发,例如当父view通过使函数onInterceptTouchEvent()返回true,拦截了事件,也就是从子view拿回处理事件的控制权时,就会给子view发一个ACTION_CANCEL事件,这里了view就再也不会收到事件了。可以将其视为ACTION_UP事件对待。
-
-
-
View的绘制流程,如何自定义View
View的绘制是从上往下一层层迭代下来的。DecorView-->ViewGroup(--->ViewGroup)-->View ,按照这个流程从上往下,从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽和高(几乎所有的情况下测量的宽高就是View最终的宽高),layout来确定View在父容器的放置位置(View的4个顶点和实际的View宽高),而draw则负责将View绘制在屏幕上。
理解View的测量过程,我们需要先理解一下MeasureSpec。MeasureSpec(32位int值)由两部分组成,一部分是测量模式(SpecMode高2位),另一部分是测量的尺寸大小(SpecSize低30位)。
SpecMode有三类:
-
UNSPECIFIED :不对View进行任何限制,要多大给多大,一般用于系统内部
-
EXACTLY:对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,
-
AT_MOST :对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。
那么MeasureSpec又是如何确定的?
对于顶级View,也就是DecorView,其确定是通过屏幕的大小和自身的布局参数LayoutParams确定的。那么对于View,其MeasureSpec由父布局的MeasureSpec和自身的布局参数LayoutParams来决定。
img
-
当View采用固定宽/高时(即设置固定的dp/px),不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY模式,并且大小遵循Layoutparams的大小。
-
当View的宽/高是match_parents时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式那么View也是最大模式并且其大小不会超过父容器的剩余空间
-
当View的宽/高是wrap_content时,View的MeasureSpec都是AT_MOST模式并且其大小不能超过父容器的剩余空间。
-
UNSPECIFIED主要用于系统内部多次Measure的情况,不太需要关注
measure:
-
View的measure过程由其measure()方法完成,measure()方法是final类型的,子类不能重写。在View的measure()方法中会去调用View的onMeasure()方法来完成测量。有两个重要的方法如下:
img
注意:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent.
解决:只需要给View指定一个默认的内部宽高(mWidth和mHeight),并在wrap_content时设置此宽高即可
-
ViewGroup的measure过程
从ViewGroup至子View、自上而下遍历进行(即树形递归),通过计算整个ViewGroup中各个View的属性,从而最终确定整个ViewGroup的属性。
img
单一View的measure过程对onMeasure()有统一的实现,但ViewGroup的measure过程是没有的,因为ViewGroup是一个抽象类,它的子类如:LinearLayout、RelativeLayout、自定义ViewGroup子类等具备不同的布局特性,这导致它们的子View测量方法各有不同,所以onMeasure()的实现也会有所不同,无法对onMeasure()作统一实现,所以其测量过程的onMeasure方法由各个子类去具体实现。
img
注意:
针对Measure流程,自定义ViewGroup的关键在于:根据需求复写onMeasure(),从而实现子View的测量逻辑。复写onMeasure()的步骤主要分为三步:
-
遍历所有子View及测量:measureChildren()
-
合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值:需自定义实现
-
存储测量后View宽/高的值:setMeasuredDimension()
-
layout:
测量完View大小后,就需要将View布局在Window中,View的布局主要通过确定上下左右四个点来确定的。
其中布局也是自上而下,不同的是ViewGroup先在layout()中确定自己的布局,然后在onLayout()方法中再调用子View的layout()方法,让子View布局。相反在Measure过程中,ViewGroup一般是先测量子View的大小,然后再确定自身的大小。
img
注意:onLayout()用来确定子View的布局,onLayout()是一个可继承的空方法,具体实现和具体布局有关。
draw:
View的绘制过程遵循如下几步:
-
绘制背景 background.draw(canvas)
-
绘制自己(onDraw)
-
绘制Children(dispatchDraw)
-
绘制装饰(onDrawScrollBars)
无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。
img
-
网友评论