View在Android中是一切控件的基类,是所有控件的抽象,代表着一个控件。ViewGroup则是代表着一个控件组,包含了多个控件。在Android的设计中,ViewGroup也继承了View,这也就意味着View本身就可以是单个控件也可以是多个控件组成的一组控件,通过这种关系就形成了View树的结构。
基础知识
View的位置参数
View的位置主要由它的四个顶点来决定。它的坐标是以父容器的左上角为基准,x轴方向向右,y轴方向向下。
- top:左上角纵坐标
- left:左上角横坐标
- right:右下角横坐标
- bottom:右下角纵坐标
- X:左上角横坐标
- Y:左上角纵坐标
- TranslationX:左上角相对于父容器的偏移量
- TranslationY:左上角相对于父容器的偏移量
//各个属性之间的关系
width = right - left;
height = bottom - top;
X = left + TranslationX;
Y = right + TranslationY;
需要注意的是这些参数在View被绘制出来之后才有确定的值,在之前则为0。也就是说如果直接在Activity的onCreate()方法中打印View的位置属性,则得到的结果是全部为0。Activity中有一个onWindowFocusChanged(boolean hasFocus)方法,会在这个焦点变化时被调用,这个时候View的宽高和位置信息都是已经确定的了。
View的事件
- ACTION_DOWN: 手指刚接触屏幕
- ACTION_MOVE: 手指在屏幕移动
- ACTION_UP: 手指从屏幕上松开的一瞬间
正常情况下,一次触摸屏幕会触发一系列点击事件。
TouchSlop系统所能识别的最小滑动距离,也就是说当滑动距离小于这个值的时候,系统不认为这是滑动。这个值是一个常量,和设备有关,可以通过ViewConfiguration(getContext).getScaledTouchSlop()来获得。它存在的意义就是为我们在处理一些滑动的时候做过滤。
View的滑动
实现View滑动的的三种方式:
- View本身的scrollTo和scrollBy(这个方法移动的是View本身的位置,而我们看到的是View中内容的相对位移,这就是为什么我们希望的效果是右移却将移动值设置为负数的原因)
- 通过给View设置平移动画来实现(在动画中,移动的是View的影相而不是View这个对象)
- 改变View的Layout.Params是的View重新布局(使用复杂,就是在修改View的属性)
View的事件分发机制
//用来进行事件的分发,如果事件能够传递给当前的View,那么这个方法一定会被调用,返回值表示是否消耗当前事件
public boolean dispatchTouchEvent(MotionEvent ev)
//用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,
//此方法不会被再次调用,返回值表示是否拦截当前事件
public boolean onInterceptTouchEvent(MotionEvent event)
//在dispatchTouchEvent()方法中调用,用来处理点击事件
//返回值表示当前事件是否被消耗
public boolean onTouchEvent(MotionEvent event)
View的事件分发机制从用户点击屏幕开始,每一个事件都会由当前Activity获取接受然后开始事件分发流程。Activity -> PhoneWindow -> DecorView -> ViewGroup(布局文件实现的View)。每一个View(或者ViewGroup)对传递过来的事件都是执行相同的原则,即先判断是否拦截这个事件,如果拦截即消耗这个事件,否则不拦截则传递给子View。上面三个重要的方法,执行的伪代码如下所示。这三个方法的返回值都是boolean,如果返回true,则代表消耗这个事件,false则代表不拦截或者不消耗。
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
重点知识点总结:
- 同一个事件序列从down开始,以up结束,中间包含了数量不定的move事件。
- 正常情况下,一个事件序列只能被一个View拦截且消耗。
- 某个View一旦决定拦截,那么这个事件序列都只能由它来出来,并且它的onInterceptTouchEvent()不会再被调用。
- 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(即onTouchEvent返回了false),那么统一事件的其它时间也不会再交给他来处理,并且时间交给它的父元素去处理(即父元素的onTouchEvent()会被调用)。
- ViewGroup默认不拦截任何事件(即ViewGroup的onInterceptTouchEvent()的返回值默认为false)
- View没有onInterceptTouchEvent(),一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
- View的onTouchEvent默认都会消耗事件(默认返回true)。除非它是不可点击的(即clickable和longClickable同时为false)。
滑动冲突
View的工作原理
ViewRoot和DecorView
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。
DecorView作为顶级View,一般情况下它的内部会包含一个竖直方向的LinearLayout,分为上下两个部分,上面是标题栏,下面是内容栏(内容栏的id是content,android.R.id.content)。View的事件都会经过DecorView,然后再传递给我们的View。
View的绘制
View的Measure过程
MeasureSpec代表一个32位的int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(指在某种测量模式下的规格大小)。
SpecMode有三种模式:
- UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部。
- EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。对应着布局中的match_parent和具体数值两种模式。
- AT_MOST:父容器制定了一个可用大小即SpecSize,View的大小不能大于这个值。对应着布局中的wrap_content。
通过父View获得子View的MeasureSpec方法源码
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
子View通过父View和自身属性生成自己的MeasureSpec
父View的Mode | 子View的属性 | 子View的Mode | 子View的Size |
---|---|---|---|
EXACTLY | 固定尺寸 | EXACTLY | 固定尺寸 |
EXACTLY | match_parent | EXACTLY | 父Size |
EXACTLY | wrap_content | AT_MOST | 父Size |
AT_MOST | 固定尺寸 | EXACTLY | 固定尺寸 |
AT_MOST | match_parent | AT_MOST | 父Size |
AT_MOST | wrap_content | AT_MOST | 父Size |
UNSPECIFIED | 固定尺寸 | EXACTLY | 固定尺寸 |
UNSPECIFIED | match_parent | UNSPECIFIED | 父Size或者0 |
UNSPECIFIED | wrap_content | UNSPECIFIED | 父Size或者0 |
父View与子View的尺寸关系
- 不管父View是何种模式,若子View有确切的数值,则子View大小就是其本身大小,且子View的Mode是EXACTLY。
- 若子View是match_parent,则模式与父View相同,且大小同父View。
- 若子View是wrap_content,则模式是AT_MOST,大小同父View,表示不可超过父View大小。
View的Measure是一个从根布局开始遍历设置width和Height的过程,子View的尺寸由父View和本身属性共同决定。
ViewGroup中没有实现onMeasure()方法,因为不同的布局计算的方式是不同的,却实现了measureChildren()方法,用来测量布局中子View的尺寸。
View的Layout过程
View树在经过第一步的Measure过程之后,已经确定了每个View的大小,而Layout过程则是确定每一个View的位置的过程。这个位置是父布局中的相对位置,而不是相对于屏幕的绝对坐标系。
在layout()方法中会执行对当前的View的位置信息操作(真正确定位置信息的方法是setFrame(),其返回值表示了当前View的位置信息有没有发生改变),而在onLayout()方法中则是确定当前View的子View的位置信息,对于View类并没有实现onLayout()方法,只有继承ViewGroup的布局类才会根据不同的规则来实现onLayout()方法。其伪代码如下。
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (遍历子View) {
/**
根据如下数据计算。
1、自己当前布局规则。比如垂直排放或者水平排放。
2、子View的测量尺寸。
3、子View在所有子View中的位置。比如位置索引,第一个或者第二个等。
*/
计算每一个子View的位置信息;
child.layout(上面计算出来的位置信息);
}
}
View的Draw过程
在经过Measure和Layout之后,每一个View都已经在确定了自己的大小和位置,Draw过程就是将这些View绘制出来。其中ViewGroup除了绘制自己,还要将子View给绘制出来。
绘制流程的六个步骤
- 对View的背景进行绘制
- 保存当前的图层信息(可跳过)
- 绘制View的内容
- 对View的子View进行绘制(如果有子View)
- 绘制View的褪色的边缘,类似于阴影效果(可跳过)
- 绘制View的装饰(例如:滚动条)
绘制子View的伪代码
protected void dispatchDraw(Canvas canvas) {
if (需要绘制布局动画) {
for (遍历子View) {
绑定布局动画;
}
启动动画控制,通知动画开始;
}
for (遍历子View) {
child.draw();
}
}