Android View 从测量布局到触摸反馈
#自定义View
在Android开发中,自定义 View 最关键的有三个点:绘制、布局和触摸反馈(绘制内容多而简单,查看手册即用即学,这里不记录了)
#布局
- 测量阶段 :从上到下
递归地调用每个View或者ViewGroup的measure()方法,测量他们的尺寸并计算它们的位置- 布局阶段:从上到下
递归地调用a每个View或者ViewGroup的layout()方法,把测得的它们的尺寸和位置赋值给它们
#####测量阶段
measure()方法被父View调用,在measure()中做一些准备和优化工作后,调用onMeasure()来进行实际的自我测量
- View:
View在onMeasure()中会计算出自己的尺寸然后保存- ViewGroup:
ViewGroup在onMeasure()中会调用所有子 View的measure()让它们进行自我测量,并根据子 View计算出的期望尺寸来计算出它们的实际尺寸和位置然后保存。同时,它也会根据子 View的尺寸和位置来计算出自己的尺寸然后保存
注:保存会调用setMeasuredDimension(int,int),可通过getMeasuredWidth()和getMeasuredHeight()获取保存的值
#####布局阶段
layout()方法被父View调用,在layout()中它会保存父View传进来的自己的位置和尺寸,并且调用onLayout()来进行实际的内部布局
- View:由于没有子 View,所以
View的onLayout()什么也不做- ViewGroup:
ViewGroup在onLayout()中会调用自己的所有子 View的layout()方法,把它们的尺寸和位置传给它们(layout方法中会有参数,即实际让子View布局的尺寸参数),让它们完成自我的内部布局
下面给出示意图:
了解上面的测量布局过程之后,我们很容易地想到以下3种自定义布局过程
##自定义布局过程
- 重写
onMeasure()来修改已有的View的尺寸(先调用super.onMeasure()) - 重写
onMeasure()来全新定制自定义View的尺寸(不用super.onMeasure()) - 重写
onMeasure()和onLayout()来全新定制自定义ViewGroup的内部布局
######第一种(修改已有尺寸)
1.重写onMeasure()方法,并在里面调用 super.onMeasure(),触发原有的自我测量
2.super.onMeasure()的下面用getMeasuredWidth()和getMeasuredHeight()来获取到之前的测量结果(宽和高),并加上自己的代码,根据测量结果计算出新的结果
getMeasuredWidth()和getMeasuredHeight()是测得的尺寸(即View在onMeasure中调用setMeasureDimension()保存下来的数据)未必与之后父View调用layout()时传递进来的的尺寸参数相等,具体值是由父View决定!
3.使用setMeasureDimension()保存自定义测量的尺寸值
######第二种(完全自己计算尺寸)
1.重写onMeasure(),计算尺寸(自己计算图、文字等等的长宽作加法运算)
2.把计算结果用resolveSize()修正一下
3.使用setMeasureDimension()保存自定义测量的尺寸值(也可以自己实现方法来满足父View的限制)
在
onMeasure()中有两个参数widthMeasureSpec和heightMeasureSpec,它们是父View对子View的测量尺寸的限制,来源于xml中以layout_打头的属性参数,这两个属性各自可以被MeasureSpec.getMode和MeasureSpec.getSize拆分为Mode和SIZEMode是限制的类型,包含3种:无限制UNSPECIFIED、限制上限AT_MOST、限制固定值EXACTLYView为我们提供了resolveSize()方法用来便捷地对应这种限制
######第三种(ViewGroup自定义测量以及布局过程)
1.重写onMeasure()来计算内部布局
- 调用每个子View的measure, 让
子View自我测量- 根据子View给出的尺寸,得到子View的位置,并保存它们的位置和尺寸
- 根据子View的位置和尺寸计算出自己的尺寸并用setMeasuredDimension()保存
2.重写onLayout()来摆放子View
- 在
onMeasure()中,需要根据ViewGroup自身的 可用空间 结合子View的layout_打头的属性去测量每个子View的尺寸,并且用MeasureSpec.makeMeasureSpec()压缩成MeasureSpec(子View的可用空间)并保存 layout_打头的属性:这类属性是子View提供给父View测量时用的,在Java代码中可以通过view.getLayoutParam()获得。全新自定义ViewGroup时只有layout_width和layout_height,开发者可以继续自定义这类属性例如layout_gravity,在自定义测量过程时将其考虑进去即可- 可用空间:对于
ViewGroup本身来说最初的可用空间是onMeasure(int widthMeasureSpec, int heightMeasureSpec)的参数,而在往子View分配可用空间时,我们可以自己制定规则,可以将widthMeasureSpec和heightMeasureSpec直接作为第一个子View的 可用空间,也可以自己做一些删减。当第一个子View的测量完成,继续测量第二个子View的时候,需要在widthMeasureSpec或者heightMeasureSpec基础上将第一个子View的 已用空间 减去,就得到了第二个子View的可用空间,以此类推 - 可用空间判断方法(通用方式,有特例):
首先根据子View在的xml布局声明的layout_width和layout_height(lp.width、lp.height)分两种情况
- MATCH_PARENT:
ViewGroup的限制为EXACTLY或AT_MOST:由于子View依赖父View,父View需要告诉子View其可用宽度,并且ViewGroup本身可用空间可以确定,所以应当给予子View的限制属性是一个具体值,mode为EXACTLY;此处给予子View的宽度是可用宽度,不管 父View 是AT_MOST还是EXACTLY,两种的原则都是这块空间子View随便用,so子View的可用空间就是当前ViewGroup的初始可用空间(onMeasure()传来的widthMeasureSpec)减去已用空间ViewGroup的限制为UNSPECIFIED:子View依赖父View,但ViewGroup本身是UNSPECIFIED无限制大小的(这个地方说大小不是很合适,可用空间可能更佳),于是无法计算出子View的可用空间,所以直接将子View的mode也写为UNSPECIFIED, 不限制其可用空间大小。size直接给0,因为在mode为UNSPECIFIED情况下size无意义,实际在高版本Android有意义,这里不做解释
- WARP_CONTENT:
ViewGroup的限制为EXACTLY或AT_MOST:虽然子View是warp_content,子 View自我测量,但却不能直接将UNSPECIFIED给子 View,因为wrap_content有个隐藏条件是不超过父View,so这里给子View的mode是AT_MOST来限制它的最大尺寸;由于ViewGroup的mode为EXACTLY或AT_MOST,我们就可以得到可用空间大小,将其减去已用空间传给子View的可用空间即可(与match_parent时做法类似)ViewGroup的限制为UNSPECIFIED:同上,子View需要自我测量,隐藏条件不超过父View应当被满足,但由于ViewGroup的限制为UNSPECIFIED,无法给出具体的可用空间大小,于是无法满足开发者在xml中给子View的wrap_content属性,无奈只能传入UNSPECIFIED不对其进行限制,size依旧是0即可
- 指定值(sp、dp):直接给
子View指定一个值,ViewGroup什么都不用做,直接将值下发给子View的可用空间,mode给EXACTLY即可
布局过程基本结束,接下来是触摸反馈过程
#触摸反馈
触摸反馈的本质就是把一系列的
触摸事件解读为对应的操作,比如按下、弹起、滑动等等,开发者再根据解读出来的操作进行反馈
对于触摸事件,有两点需要注意
触摸事件不相互独立,它们是成序列(成组)出现的- 每组事件由
DOWN开头,由UP或CANCEL结尾
大家都知道,自定义触摸反馈只需要重写View的onTouchEvent(MotionEvent event)方法,event中包含了此次触摸事件的事件类型、坐标等其他信息,当触摸事件不断被触发,onTouchEvent()就不断被调用,这是触摸反馈的核心。对于简单的 自定义触摸反馈,重写这个方法已经够了,但难免我们会遇上新的问题 —— 滑动冲突,只有当我们了解整个事件分发机制,才能够彻底解决滑动冲突。
在Android中,当一个触摸事件产生,MotionEvent 将从 Activity(Window)——>ViewGroup(多个)——> View

实际上学习Android的触摸事件分发机制就是学习以下3个组件的事件分发机制
Activity对触摸事件的分发机制ViewGroup对触摸事件的分发机制View对触摸事件的分发机制
在Android的事件分发机制中,传递的核心方法有3个:
dispatchTouchEvent():分发(传递)点击事件,当点击事件能够传递给当前View,该方法就会被调用onInterceptTouchEvent():只存在于ViewGroup中,在dispatchTouchEvent()内部被调用,判断是否拦截了某个事件onTouchEvent():处理点击事件,在dispatchTouchEvent()内部调用
这三个方法的解释不严谨,目的只是让大家现在有一个关系概念,而不是将每个细节都全理解,之后在源码中会有细节
先上一个粗略的图,大概对事件分发流程有个印象,方便看源码的时候理解
##Activity的事件分发机制
当一个触摸事件发生时,事件最先传到 Activity 的 dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN)
onUserInteraction();
}
//获取Activity的window对象(实现类PhoneWindow)并调用其方法 `superDispatchTouchEvent()`
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//当未被处理,直接调用Activity的 `onTouchEvent()` 处理
return onTouchEvent(ev);
}
//空方法,当Activity在栈顶,触摸、按Home、back、menu都会触发该方法
public void onUserInteraction() {
}
//Window
@Override
public boolean superDispatchTouchEvent(MotionEvent event)
// mDecor = 顶层View(DecorView)的实例对象
//DecorView是PhoneWindow的内部类,继承自FrameLayout,所以是一个ViewGroup
return mDecor.superDispatchTouchEvent(event);
}
//DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
// 调用父类的方法 = ViewGroup的dispatchTouchEvent()
// 即 将事件传递到ViewGroup去处理,详细看ViewGroup的事件分发机制
return super.dispatchTouchEvent(event);
}
public boolean onTouchEvent(MotionEvent event) {
// 当一个点击事件未被Activity下任何一个View接收 / 处理时
// 应用场景:处理发生在Window边界外的触摸事
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;//即只有在点击事件在Window边界外才会返回true,一般情况都返回false
}
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
// 主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
return true;
}
return false;
// 返回true:说明事件在边界外,即 消费事件
// 返回false:未消费(默认)
}
流程图
红框中是重点!事件从这里下发到子View/View Group!
#ViewGroup事件的分发机制
Android 5.0后,ViewGroup.dispatchTouchEvent()的源码发生了变化(更加复杂),但原理相同;
为了便于理解,采用Android 5.0前的版本
public boolean dispatchTouchEvent(MotionEvent ev) {
... // 仅贴出关键代码
// ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// 判断值1:disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
// 判断值2: !onInterceptTouchEvent(ev) = 对onInterceptTouchEvent()返回值取反
// a. 若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部
// b. 若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断
// c. 关于onInterceptTouchEvent() ->>分析1
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
// 通过for循环,遍历了当前ViewGroup下的所有子View
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) { //可见或正在执行动画
child.getHitRect(frame);
// 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
// 若是,则进入条件判断内部
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
// 条件判断的内部调用了该View的dispatchTouchEvent()
// 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面的View事件分发机制)
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
// 调用子View的dispatchTouchEvent后是有返回值的
// 若该控件可点击,那么点击时,dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
// 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
// 即把ViewGroup的点击事件拦截掉
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
//如果是ACTION_UP或者ACTION_CANCEL, 将disallowIntercept设置为默认的false
//假如我们调用了requestDisallowInterceptTouchEvent()方法来设置disallowIntercept为true
//当我们抬起手指或者取消Touch事件的时候要将disallowIntercept重置为false
//所以说上面的disallowIntercept默认在我们每次ACTION_DOWN的时候都是false
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
// 若点击的是空白处(即无任何View接收事件) / 拦截事件(手动复写onInterceptTouchEvent(),从而让其返回true)
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
// 调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()
// 因此会执行ViewGroup的onTouch() ->> onTouchEvent() ->> performClick() ->> onClick(),即自己处理该事件,事件不会往下传递(具体请参考View事件的分发机制中的View.dispatchTouchEvent())
// 此处需与上面区别:子View的dispatchTouchEvent()
}
...
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
//返回true = 拦截,即事件停止往下传递(需手动设置,即复写onInterceptTouchEvent(),从而让其返回true)
//返回false = 不拦截(默认)
return false;
}
到这里为止,你会发现没有任何一个地方消费了(使用了)触摸事件,因为目前为止所有的过程都只是在下发(往下传递MotionEvent),而真正要处理事件,是等到View(真的View,不是ViewGroup)在 dispatchTouchEvent() 中去做操作,在这里才会 真正 让 dispatchTouchEvent()与 onTouchEvent() 产生交集,接着往下看
#View事件的分发机制
public boolean dispatchTouchEvent(MotionEvent event) {
// 只有以下3个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent()
// 1. mOnTouchListener != null
// 2. (mViewFlags & ENABLED_MASK) == ENABLED
// 3. mOnTouchListener.onTouch(this, event)
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
// 假如onTouch返回了true,直接返回True
onTouchEvent()处理
return true;
}
// 假如onTouch没返回true,交给此View的
return onTouchEvent(event);
}
// 在这里为mOnTouchListener赋值
public void setOnTouchListener(OnTouchListener l) {
// 即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)
mOnTouchListener = l;
}
/**
* 回调控件注册Touch事件时的onTouch();需手动复写设置,具体如下(以按钮Button为例)
*/
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!







