view的视图架
每一个Activity都包含的一个window,这个window的实现类是Phone Window。后Phone Window是顶层的view,叫docor view。docor view中有一个叫content的FrameLayout,我们经常在Activity的onCreate中使用setContentView(R.layout.id)设置我们自定义的视图,就是添加到这个叫content的framelayout中。然后上面还有一个TitleActionBar,这个就是TitleBar。
在content这个view中,是我们在xml中自定义的view。从图中可以看出,我们自定义的view形成了一个树形结构。viewGroup中可以放置若干个view,而由于viewGroup本身也继承了view,所以ViewGroup的子view也可以是另一个Viewgroup,这样形成一个树形结构。
MeasureSpec
这是一个很重要的概念,一个view的MeasureSpec可以看作这个view暂定的大小。
MeasureSpec是一个32位的int值。前两位表示测量模式,后两位表示暂定的测量大小。
测量模式有三种,分别是:UNSPECIFIED,即父容器对子容器的大小不作任何要求;EXACTLY,父容器已经确定了自容器的精确大小;AT_MOST,父容器指定了一个最大的大小,view不能超过这个大小。
MeasureSpec由当前view的parentView调用getChildMeasureSpec生成,传入的三个参数分别是,parentView的MeasureSpec(父容器的暂定大小),parentView的左右上下内边距和当前view的LayoutParams(自容器的期望大小)。
/**
* Does the hard part of measureChildren: figuring out the MeasureSpec to
* pass to a particular child. This method figures out the right MeasureSpec
* for one dimension (height or width) of one child view.
*
* 这个方法将根据父容器的MeasureSpec和子View LayoutParams中的宽/高
* 为子View生成最合适的MeasureSpec
*
* @param spec 父容器的MeasureSpec
* @param padding 父容器的内间距(padding)加上子View的外间距(margin)
* @param childDimension 子View的LayoutParams中封装的width/height
* @return 子View的MeasureSpec
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// ① 对父容器的MeasureSpec进行解包
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// ② 减去间距,得到最大可用空间
int size = Math.max(0, specSize - padding);
// 记录子View最终的大小和测量模式
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// ③ 父容器是精准测量模式
case MeasureSpec.EXACTLY:
if (childDimension >= 0) { //如果子view在LayoutParam中指定了大小,那么子view的resultSize 就是该大小,模式是EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子view中LayoutParams是MATCH_PARENT,则view的大小等于最大可用大小,测量模式是EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子view中LayoutParams是WRAP_CONTENT,则view的大小等于最大可用大小,测量模式是AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// ④ 父容器指定了一个最大可用的空间
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// ⑤ 父容器不对子View的大小作出限制
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
// ⑥ 将最终的size和mode打包为子View需要的MeasureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
在view中每个子view的MeasureSpec都是由父容器的MeasureSpec、间距和自容器的LayoutParam来生成,逐级往上,最顶层的view,也就是docor view的MeasureSpec是怎么生成的呢?docor view的MeasureSpec由其上层的window的大小和docor view的自身的LayoutParam来生成。
自定义View 6步
1.继承View
2.实现画笔paint类
3.覆写onMeasure(…)方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int w = MeasureSpec.getSize(widthMeasureSpec);
int h = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(w, h);
}
4.实现onSizeChanged(…)方法
这个方法是你获取View现在的宽和高. 这里我们计算的是中心和半径。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mCenterX = w / 2f;
mCenterY = h / 2f;
mRadius = Math.min(w, h) / 2f;
}
5.实现onDraw(…)方法
这个方法提供了如何绘制view,它提供的Canvas类可以进行绘制。
@Override
protected void onDraw(Canvas canvas) {
// draw face
canvas.drawCircle(mCenterX, mCenterY, mRadius, mCirclePaint);
// draw eyes
float eyeRadius = mRadius / 5f;
float eyeOffsetX = mRadius / 3f;
float eyeOffsetY = mRadius / 3f; canvas.drawCircle(mCenterX - eyeOffsetX, mCenterY - eyeOffsetY, eyeRadius, mEyeAndMouthPaint); canvas.drawCircle(mCenterX + eyeOffsetX, mCenterY - eyeOffsetY, eyeRadius, mEyeAndMouthPaint);
// draw mouth
float mouthInset = mRadius /3f;
mArcBounds.set(mouthInset, mouthInset, mRadius * 2 - mouthInset, mRadius * 2 - mouthInset);
canvas.drawArc(mArcBounds, 45f, 90f, false, mEyeAndMouthPaint);
}
6.添加你的View到xml使用
7.自定义View的触摸反馈
重写onTouchEvent(),在方法内部定制触摸反馈算法
是否取消事件取决于ACTION_DOWN事件或PONITER_DOWN事件是否返回true
MotionEvent
getActionMasked()和getAction()
POINTER_DOWN / POINTER_UP 和 getActionIndex()
onTouchEvent()
当用户按下(ACTION_DOWN)
如果不在滑动控件中,切换至按下状态,并注册长按计时器
如果在滑动控件中,切换至按下状态,并注册按下计时器
当进入按下状态并移动(ACTION_MOVE)
重绘Ripple Effect
如果移动出自己的范围,自我标记本次事件失效,忽略后续事件
当用户抬起(ACTION_UP)
如果是按下状态并且未触发长按,切换至抬起状态并触发点击事件,并清除一切状态
如果已经触发长按,切换至抬起状态并清除一切状态
当事件意外结束(ACTION_CANCEL)
切换至抬起状态,并清除一切状态
自定义ViewGroup 的触摸反馈
除了重写onTouchEvent(),还需要重写onInterceptEvent()
onInterceptEvent() 不用在第一时间返回true,而是在任意事件,需要拦截的时候返回true就行
触摸反馈的流程
Activity.dispatchEvent()
递归:ViewGroup(View).dispatchTouchEvent()
ViewGroup.onInterceptTouchEvent()
ViewGroup.onIntercepTouchEvent()
Child.dispatchTouchEvent()
Super.dispatchEvent()
View.onTouchEvent()
Activity.onTouchEvent()
View.dispatchTouchEvent()
如果设置了OnTouchListener,调用OnTouchListener.onTouch()
如果OnTouchListener消费了事件,返回true
如果onTouchListener没有了消费事件,继续调用自己的onTouchEvent(),并返回和onTouchEvent()相同的结果
如果没有设置onTouchEvent,同上
ViewGroup.dispatchEvent()
如果是用户初次按下(ACTION_DOWN),清空TouchTargets和DISALLOW_INTERCEPT标记
拦截处理
如果不拦截并且不是CANCEL事件,并且是DOWN或者POINTER_DOWN,尝试把pointer(手指)通过TouchTarget分配给子View;并且如果分配给了新的子View,调用child.dispathTouchEvent()把事件传递给子View
看有没有TouchTarget
如果没有TouchTarget,调用自己的super.dispatchTouchEvent()
如果有,调用child.dispatchEvent()把事件传给对应的子View(如果有的话)
如果是POINTER_UP,从TouchTargets中清除POINTER信息,如果是UP或CANCEL,重置状态
TouchTarget
作用:记录每个子View是被哪些pointer(手指)按下的
结构:单向链表
拦截处理
如果不是初次按下,并且没有TouchTarget,直接拦截
如果不是初次按下,或者有TouchTarget
如果设置了disallow intercept,不拦截
否则,调用onInterceptTouchEvent(),如果返回true则拦截,返回false则不拦截
事件分发机制涉及两个知识点:1、事件分发传递流程;2、滑动冲突问题
仔细看的话,图分为3层,从上往下依次是 Activity、ViewGroup、View
事件从左上角那个白色箭头开始,由 Activity 的 dispatchTouchEvent 做分发
箭头的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super 的意思是调用父类实现。
dispatchTouchEvent 和 onTouchEvent 的框里有个【true---->消费】的字,表示的意思是如果方法返回 true,那么代表事件就此消费,不会继续往别的地方传了,事件终止。
目前所有的图的事件是针对 ACTION_DOWN 的,对于 ACTION_MOVE 和 ACTION_UP我们最后做分析。
只有 return super.dispatchTouchEvent(ev) 才是往下走,返回true 或者 false 事件就被消费了(终止传递)。
1、如果事件不被中断,整个事件流向是一个类U型图,我们来看下这张图,可能更能理解U型图的意思。
所以如果我们没有对控件里面的方法进行重写或更改返回值,而直接用super调用父类的默认实现,那么整个事件流向应该是从 Activity---->ViewGroup--->View 从上往下调用 dispatchTouchEvent方法,一直到叶子节点(View)的时候,再由 View--->ViewGroup--->Activity 从下往上调用 onTouchEvent 方法。
2、dispatchTouchEvent 和 onTouchEvent 一旦 return true, 事件就停止传递了(到达终点)(没有谁能再收到这个事件)。看下图中只要 return true 事件就没再继续传下去了,对于 return true 我们经常说事件被消费了,消费了的意思就是事件走到这里就是终点,不会往下传,没有谁能再收到这个事件了。
3、dispatchTouchEvent 和 onTouchEvent return false 的时候事件都回传给父控件的onTouchEvent 处理。
看上图深蓝色的线,对于返回 false 的情况,事件都是传给父控件 onTouchEvent 处理。
对于 dispatchTouchEvent 返回 false 的含义应该是:事件停止往子 View 传递和分发同时开始往父控件回溯(父控件的 onTouchEvent 开始从下往上回传直到某个 onTouchEvent return true),事件分发机制就像递归,return false 的意义就是递归停止然后开始回溯。
对于onTouchEvent return false 就比较简单了,它就是不消费事件,并让事件继续往父控件的方向从下往上流动。
4、dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent ViewGroup 和 View 的这些方法的默认实现就是会让整个事件安装 U 型完整走完,所以 return super.xxxxxx() 就会让事件依照U型的方向的完整走完整个事件流动路径),中间不做任何改动,不回溯、不终止,每个环节都走到。
所以如果看到方法 return super.xxxxx() 那么事件的下一个流向就是走 U 型下一个目标,稍微记住上面这张图,你就能很快判断出下一个走向是哪个控件的哪个函数。
5、onInterceptTouchEvent 的作用
Intercept 的意思就拦截,每个 ViewGroup 每次在做分发的时候,问一问拦截器要不要拦截(也就是问问自己这个事件要不要自己来处理)如果要自己处理那就在 onInterceptTouchEvent 方法中 return true 就会交给自己的 onTouchEvent 的处理,如果不拦截就是继续往子控件往下传。默认是不会去拦截的,因为子 View 也需要这个事件,所以 onInterceptTouchEvent 拦截器 return super.onInterceptTouchEvent() 和r eturn false 是一样的,是不会拦截的,事件会继续往子 View 的 dispatchTouchEvent 传递。
6、ViewGroup 和View 的dispatchTouchEvent方法返回super.dispatchTouchEvent()的时候事件流走向。
首先看下 ViewGroup 的 dispatchTouchEvent,之前说的 return true 是终结传递。return false 是回溯到父 View 的 onTouchEvent,然后 ViewGroup 怎样通过 dispatchTouchEvent 方法能把事件分发到自己的 onTouchEvent 处理呢,return true 和 false 都不行,那么只能通过 Interceptor 把事件拦截下来给自己的 onTouchEvent,所以 ViewGroup dispatchTouchEvent 方法的 super 默认实现就是去调用 onInterceptTouchEvent,记住这一点。
那么对于 View 的 dispatchTouchEvent return super.dispatchTouchEvent() 的时候呢事件会传到哪里呢,很遗憾 View 没有拦截器。但是同样的道理 return true 是终结。return false 是回溯会父类的 onTouchEvent,怎样把事件分发给自己的 onTouchEvent 处理呢,那只能 return super.dispatchTouchEvent,View 类的 dispatchTouchEvent() 方法默认实现就是能帮你调用 View 自己的 onTouchEvent 方法的。
说了这么多,不知道有说清楚没有,我这边最后总结一下:
对于 dispatchTouchEvent,onTouchEvent,return true是终结事件传递。return false 是回溯到父 View 的 onTouchEvent 方法。
ViewGroup 想把自己分发给自己的 onTouchEvent,需要拦截器 onInterceptTouchEvent 方法 return true 把事件拦截下来。
ViewGroup 的拦截器 onInterceptTouchEvent 默认是不拦截的,所以 return super.onInterceptTouchEvent()=return false;
View 没有拦截器,为了让View可以把事件分发给自己的 onTouchEvent,View的 dispatchTouchEvent 默认实现(super)就是把事件分发给自己的 onTouchEvent。
ViewGroup 和 View 的 dispatchTouchEvent 是做事件分发,那么这个事件可能分发出去的四个目标
注:------> 后面代表事件目标需要怎么做。
1、 自己消费,终结传递。------->return true ;
2、 给自己的 onTouchEvent 处理-------> 调用 super.dispatchTouchEvent() 系统默认会去调用 onInterceptTouchEvent,在onInterceptTouchEvent return true就会去把事件分给自己的 onTouchEvent 处理。
3、 传给子 View ------>调用 super.dispatchTouchEvent() 默认实现会去调用 onInterceptTouchEvent 在 onInterceptTouchEvent return false,就会把事件传给子类。
4、 不传给子 View,事件终止往下传递,事件开始回溯,从父 View 的 onTouchEvent 开始事件从下到上回归执行每个控件的 onTouchEvent -------> return false;
注: 由于 View 没有子 View 所以不需要 onInterceptTouchEvent 来控件是否把事件传递给子 View 还是拦截,所以 View 的事件分发调用 super.dispatchTouchEvent() 的时候默认把事件传给自己的 onTouchEvent 处理(相当于拦截),对比 ViewGroup 的 dispatchTouchEvent 事件分发,View 的事件分发没有上面提到的4个目标的第3点
ViewGroup 和 View 的 onTouchEvent 方法是做事件处理的,那么这个事件只能有两个处理方式:
1、自己消费掉,事件终结,不再传给谁----->return true;
2、继续从下往上传,不消费事件,让父View也能收到到这个事件----->return false;View的默认实现是不消费的。所以super==false。
ViewGroup的onInterceptTouchEvent方法对于事件有两种情况:
1、拦截下来,给自己的onTouchEvent处理--->return true;
2、不拦截,把事件往下传给子View---->return false,ViewGroup默认是不拦截的,所以super==false;
关于ACTION_MOVE 和 ACTION_UP
上面讲解的都是针对ACTION_DOWN的事件传递,ACTION_MOVE和ACTION_UP在传递的过程中并不是和ACTION_DOWN 一样,你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个事件(如ACTION_DOWN)返回true,才会收到ACTION_MOVE和ACTION_UP的事件。具体这句话很多博客都说了,但是具体含义是什么呢?我们来看一下下面的具体分析。
上面提到过了,事件如果不被打断的话是会不断往下传到叶子层(View),然后又不断回传到Activity,dispatchTouchEvent 和 onTouchEvent 可以通过return true 消费事件,终结事件传递,而onInterceptTouchEvent 并不能消费事件,它相当于是一个分叉口起到分流导流的作用,ACTION_MOVE和ACTION_UP 会在哪些函数被调用,之前说了并不是哪个函数收到了ACTION_DOWN,就会收到 ACTION_MOVE 等后续的事件的。
下面通过几张图看看不同场景下,ACTION_MOVE事件和ACTION_UP事件的具体走向并总结一下规律。
//打印日志Activity|dispatchTouchEvent-->ACTION_DOWN ViewGroup1|dispatchTouchEvent-->ACTION_DOWN---->消费
在这种场景下ACTION_MOVE和ACTION_UP 将如何呢,看下面的打出来的日志
Activity|dispatchTouchEvent-->ACTION_MOVE ViewGroup1|dispatchTouchEvent-->ACTION_MOVE----Activity|dispatchTouchEvent-->ACTION_UP ViewGroup1|dispatchTouchEvent-->ACTION_UP----
下图中
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
2、我们在ViewGroup2 的dispatchTouchEvent 返回true消费这次事件
Activity|dispatchTouchEvent-->ACTION_DOWN ViewGroup1|dispatchTouchEvent-->ACTION_DOWNViewGroup1|onInterceptTouchEvent-->ACTION_DOWNViewGroup2|dispatchTouchEvent-->ACTION_DOWN---->消费Activity|dispatchTouchEvent-->ACTION_MOVE ViewGroup1|dispatchTouchEvent-->ACTION_MOVEViewGroup1|onInterceptTouchEvent-->ACTION_MOVEViewGroup2|dispatchTouchEvent-->ACTION_MOVE----TouchEventActivity|dispatchTouchEvent-->ACTION_UP ViewGroup1|dispatchTouchEvent-->ACTION_UPViewGroup1|onInterceptTouchEvent-->ACTION_UPViewGroup2|dispatchTouchEvent-->ACTION_UP----
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
3、我们在View 的 dispatchTouchEvent 返回 true 消费这次事件
这个我不就画图了,效果和在ViewGroup2 的dispatchTouchEvent return true的差不多,同样的收到ACTION_DOWN 的dispatchTouchEvent函数都能收到 ACTION_MOVE和ACTION_UP。
所以我们就基本可以得出结论如果在某个控件的dispatchTouchEvent 返回true消费终结事件,那么收到ACTION_DOWN 的函数也能收到 ACTION_MOVE和ACTION_UP。
4、我们在View 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
5、我们在ViewGroup 2 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
6、我们在ViewGroup 1 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
7、我们在Activity 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
8、我们在View的dispatchTouchEvent 返回false并且Activity 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
9、我们在View的dispatchTouchEvent 返回false并且ViewGroup 1 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
10、我们在View的dispatchTouchEvent 返回false并且在ViewGroup 2 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
11、我们在ViewGroup2的dispatchTouchEvent 返回false并且在ViewGroup1 的onTouchEvent返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
12、我们在 ViewGroup2 的 onInterceptTouchEvent 返回 true 拦截此次事件并且在ViewGroup 1 的onTouchEvent返回true消费这次事件。
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
对于在onTouchEvent消费事件的情况:在哪个View的onTouchEvent 返回true,那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给自己的onTouchEvent 并结束本次事件传递过程。
对于ACTION_MOVE、ACTION_UP总结:ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,就只会传到这个控件,不会继续往下传,如果ACTION_DOWN事件是在 dispatchTouchEvent 消费,那么事件到此为止停止传递(MOVE 和 UP也会传到这里),如果ACTION_DOWN事件是在 onTouchEvent 消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的 onTouchEvent 处理并结束传递。
作者:阿瑞921
链接:https://www.jianshu.com/p/11b37cfebb21
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。