4.1 View的事件体系
一、View的基础知识
1、View的位置参数
1.1、两种坐标系
Android坐标系:以屏幕左上角点作为坐标系原点。
View坐标系:以View的左上角点作为坐标系原点。
1.2、View的位置属性
View的位置主要由四个属性决定:top、left、right、bottom。从Android3.0开始,还增加了x、y、translationX、translationY。这几个参数都是相对于父容器坐标系而言。
width = right - left
height = bottom - top
x = left + translationX //left不会变
y = top + translationY //top不会变
x、y是View的左上角坐标
translationX、translationY是View的左上角相对于父容器的偏移量,默认值是0
2、MotionEvent
典型的事件类型
- ACTION_DOWN 手指刚接触屏幕
- ACTION_MOVE 手指在屏幕上移动
- ACTION_UP 手指从屏幕上松开
MotionEvent的getX()
和getY()
是相对于发生事件的View本身坐标系而言的,getRawX()
和getRawY()
是相对于Android坐标系而言的。
若在View处按下,View接收到了MotionEvent对象,移到View上方时,getY()返回负数,移到View下方时,getY()将返回的值大于getHeight(),getX()也是类似的。
3、TouchSlop
系统所能识别出的被认为是滑动的最小距离,这是一个常量,与设备有关,可通过以下方法获得
ViewConfiguration.get(getContext()).getScaledTouchSloup()
当我们处理滑动时,比如滑动距离小于这个值,我们就可以过滤这个事件(系统会默认过滤),从而有更好的用户体验。
4、VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度,包括水平放向速度和竖直方向速度。使用方法:
- 在View的onTouchEvent方法中追踪当前事件的速度
VelocityRracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
- 计算速度,获得水平速度和竖直速度
velocityTracker.computeCurrentVelocity(1000);//计算速度。获取速度之前,必须调用。
int xVelocity = (int)velocityTracker.getXVelocity();
int yVelocity = (int)velocityTracker.getYVelocity();
这里的速度是指一段时间内手指滑过的像素数,1000指的是1000ms,得到的是1000ms内滑过的像素数。速度可正可负:速度 = ( 终点位置 - 起点位置) / 时间段
- 当不需要使用的时候,需要调用clear()方法重置并回收内存
velocityTracker.clear();
velocityTracker.recycle();
5、GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。
使用过程
- 创建一个GestureDetector对象并实现OnGestureListener(或OnDoubleTapListener)接口:
GestureDetector mGestureDetector = new GestureDetector(this);
//解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);
2.接管目标View的onTouchEvent方法
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
OnGestureListener和OnDoubleTapListener接口中的方法:
其中常用的方法有:onSingleTapUp
(单击)、onFling
(快速滑动)、onScroll
(拖动)、onLongPress
(长按)和onDoubleTap
( 双击)。建议:如果只是监听滑动相关的,可以自己在onTouchEvent中实现,如果要监听双击这种行为,那么就使用GestureDetector。
2、View的滑动
三种方式实现滑动:①通过View本身提供的scrollTo/scrollBy方法。②通过动画对View施加平移效果。③通过改变View的LayoutParams使得View重新布局来实现滑动。
2.1、使用scrollTo/scrollBy
View的两个属性:mScrollX和mScollY。
mScrollX = View布局x(左边缘) - View内容x(内容左边缘)可能为负数。
scrollTo/scrollBy 只能改变View内容的位置而不能改变View在布局中的位置。
View内容:若View是一个ViewGroup,指的就是其子元素。若View如Buttom,那么指的就是text值。
scrollTo(int x, int y)
scrollBy(int x, int y)
getScrollX()
getScrollY()
2.2、使用动画
使用动画移动View,主要是操作View的translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画,如果使用属性动画,为了能够兼容3.0以下的版本,需要采用开源动画库nineolddandroids。
2.3、改变参数布局
LinearLayout.MarginLayoutParams params //取决于button的父容器是什么布局
= (LinearLayout.MarginLayoutParams) button.getLayoutParams();
params.width = 100;
params.height = 200;
params.leftMargin = 100;
button.requestLayout();//或者 button.setLayoutParams(params)
ViewParent
View需要与其父ViewGroup进行交互时的API,基本所有的View都实现了这个接口
重要方法:
View的getParent() ViewParent
ViewParent的requestLayout()
requeLayout()
: 子View调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量、布局、绘制。
invalidate()
:当子View调用了invalidate方法后,会为该View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到ViewRootImpl中,最终触发performTraversals方法,进行开始View树重绘流程(只绘制需要重绘的视图)。
postInvalidate()
:这个方法与invalidate方法的作用是一样的,都是使View树重绘,但两者的使用条件不同,postInvalidate是在非UI线程中调用,invalidate则是在UI线程中调用。
layout()
:对控件进行重新定位执行onLayout()这个方法,比如要做一个可回弹的ScrollView,思路就是随着手势的滑动子控件滑动,那么我们可以将ScrollView的子控件调用layout(l,t,r,b)这个方法就行了。
Android View 深度分析requestLayout、invalidate与postInvalidate
2.4、各种滑动方式的对比
- scrollTo/scrollBy:操作简单,适合对View内容的滑动;
- 动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果;
- 改变布局参数:操作稍微复杂,适用于有交互的View。
3、弹性滑动
共同思想:将一次大的滑动分成若干次小的滑动,并在一定时间段内完成。
3.1、使用Scroller
使用Scroller实现弹性滑动的典型使用方法如下
Scroller scroller = new Scroller(mContext);
//缓慢移动到指定位置
private void smoothScrollTo(int destX,int dextY){
int scrollX = getScrollX();
int deltaX = destX - scrollX;
//1000ms内滑向destX,效果就是缓慢滑动
mScroller.startSscroll(scrollX,0,deltaX,0,1000);//仅仅保存了传递的参数,并不会滑动
invalidate();//View会进行重绘
}
@override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
invalidate()
导致View重绘,在View的draw方法中调用了computeScroll()
,computeScroll()
在View中是一个空的实现,需要我们自己去实现。computeScrollOffset()
会根据时间流逝去计算当前的mScrollX和mScrollY,并调用scrollTo
方法实现滑动,接着又调用postInvalidate()
进行第二次重绘。如此反复,直到绘制结束。
Scroller方法:
startScroll(int startX, int startY, int dx, int dy, int duration)
-
boolean computeScrollOffset()
//返回true,代表滑动未结束 -
int getCurrX()
//当前时刻应该所处的位置
3.2、通过动画
方法一
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start()
方法二
//当然,我们也可以利用动画来模仿Scroller实现View弹性滑动的过程:
final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener(){
@override
public void onAnimationUpdate(ValueAnimator animator){
float fraction = animator.getAnimatedFraction();
mButton1.scrollTo(startX + (int) (deltaX * fraction) , 0);
}
});
animator.start();
3.3、使用延时策略
延时策略的核心思想是通过发送一系列延时信息从而达到一种渐近式的效果,具体可以通过Hander和View的postDelayed方法,也可以使用线程的sleep方法。 下面以Handler为例:
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELATED_TIME = 33;
private int mCount = 0;
@suppressLint("HandlerLeak")
private Handler handler = new handler(){
public void handleMessage(Message msg){
switch(msg.what){
case MESSAGE_SCROLL_TO:
mCount ++ ;
if (mCount <= FRAME_COUNT){
float fraction = mCount / (float) FRAME_COUNT;
int scrollX = (int) (fraction * 100);
mButton1.scrollTo(scrollX,0);
mHandelr.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO , DELAYED_TIME);
}
break;
default : break;
}
}
}
四、事件的分发机制
1、基础认知
当用户触摸屏幕时将产生MotionEvent对象
典型的事件类型:
MotionEvent.ACTION_DOWN:按下View(所有事件的开始)
MotionEvent.ACTION_MOVE:滑动View
MotionEvent.ACTION_CANCEL:非人为原因结束本次事件
MotionEvent.ACTION_UP:抬起View(与DOWN对应)
事件分发的本质:即当一个点击事件发生后,系统需要将这个事件传递给一个具体的View去处理。这个事件传递的过程就是分发过程。由三个重要方法来共同完成。
boolean dispatchTouchEvent(MotionEvent event)
用来进行事件的分发
boolean onInterceptTouchEvent(MotionEvent ev)
用来判断是否拦截事件
boolean onTouchEvent(MotionEvent event)
用来处理事件
他们之间的关系,可以用如下伪代码表示:
public boolean dispatchTouchEvent (MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvnet(ev)){
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEnvet(ev);
}
return consume;
}
事件分发机制的重要结论:
- 同一个事件序列以down事件开始,中间包含数量不定的move事件,最终以up事件结束。
- 正常情况下,一个事件序列只能由一个View拦截并消耗。
- 某个View拦截了事件后,该事件序列只能由它去处理,并且它的onInterceptTouchEvent不会再被调用。
- 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件( onTouchEvnet返回false) ,那么同一事件序列中的其他事件都不会交给他处理,并且事件将重新交由他的父元素去处理,即父元素的onTouchEvent被调用。好比一个程序员,如果这件事没有处理好,短期内上级不会再把事情交给他处理。
- 如果View不消耗ACTION_DOWN以外的其他事件,那么这个事件将会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终消失的点击事件会传递给Activity去处理。
- ViewGroup默认不拦截任何事件。
- View没有onInterceptTouchEvent方法。一旦事件传递给它,它的onTouchEvent方法会被调用。
- View的onTouchEvent默认消耗事件,除非他是不可点击的( clickable和longClickable同时为false) 。View的longClickable属性默认false,clickable默认属性分情况(如TextView为false,button为true)。
- View的enable属性不影响onTouchEvent的默认返回值。
- onClick会发生的前提是当前View是可点击的,并且收到了down和up事件。
- 事件传递过程总是由外向内的,即事件总是先传递给父元素,然后由父元素分发给子View,通过
requestDisallowInterceptTouchEvent
方法可以在子元素中干预父元素的分发过程,但是ACTION_DOWN事件除外。 -
onTouch
(dispatchTouchEvent中调用)优先于onTouchEvent
执行,onClick
优先级最低。onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。
参考文献:
OnFling和onSingleTapUp不执行的问题的一种解决方法
4.2 View的工作原理
一、解析Activity的构成
1、DecorView的创建
当我们调用startActivity
方法时,最终调用ActivityThread#handleLaunchActivity
,该方法中会首先会调用Activity的onCreate
方法。在onCreate
方法中,会调用Activity#setContentView
,setContentView
内部会调用Activity的成员变量mWindow的(Window是抽象类,其实现类是PhoneWindow,mWindow是PhoneWindow的一个实例)setContentView。其setContentView方法中,首先new一个DecorView对象,然后DecorView对象会根据不同的情况(主题,Window的feature等)加载不同的布局资源。DecorView是Activity中的根View,继承了FrameLayout。至此DecorView创建完成。
2、添加DecorView到Window
完成DecorView的创建之后,接着调用ActivityThread#handleResumeActivity
方法。在handleResumeActivity方法中,首先调用Activity#onResume方法,handleResumeActivity方法接着会得到一个DecorView对象和一个WindowManager对象(接口,实现类是WindowManagerImpl),然后调用WindowManagerImpl#addView方法,DecorView对象作为入参传入。在WindowManager#addView中,创建了一个ViewRootImpl对象(ViewRoot的实现类),并调用了ViewRootImpl#setView,DecorView对象作为入参。在ViewRootImpl#setView方法内部,会通过跨进程的方式向WMS(WindowManagerService)发起一个调用,从而将DecorView最终添加到Window上,才能真正显示出来。在这个过程中,ViewRootImpl、DecorView和WMS会彼此关联,最后通过WMS调用ViewRootImpl#performTraverals方法开始View的测量、布局、绘制流程。
Window是一个抽象类,具体是实现是PhoneWindow,Activity、Dialog等的视图都需要附加到Window上来呈现。
WindowManager是外界访问Window的入口,实现类是WindowManagerImpl,Window的具体实现是在WindowManagerService中,WindowManager和WindowManagerService的交互是一个IPC过程。。
DecorView是顶级View,是一个FrameLayout布局,代表了整个应用的界面。内部有titlebar和contentParent两个子元素,contentParent的id是content,而我们设置的main.xml布局则是contentParent里面的一个子元素。
ViewRoot的实现类是ViewRootImpl,在WindowManager中创建,用于将DecorView添加到Window中。
二、理解MeasureSpec
MeasureSpec代表一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(某种测量模式下的规格大小)。
//主要理解 & ~ | 位运算的作用,体会这样设计的妙处
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;//11000000 0000...000
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size,int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
MeasureSpec通过将SpecSize和SpecMode打包成了一个int值来避免过多对象的内存分配。
SpecMode有三类:
UNSPECIFIED :父容器不对View进行任何限制,要多大给多大,一般用于系统内部。
EXACTLY:父容器检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,对应LayoutParams中的match_parent和具体数值这两种模式(也不一定,还受父容器影响,详见下面的表格)。
AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,对LayoutParams中的wrap_content。
说明:上面描述的是理论上应该有的逻辑。
对于顶级DecorView,其MeasureSpec是由窗口尺寸和自身的LayoutParams共同确定。对于普通的View,其MeasureSpec由父容器和自身的LayoutParams共同确定。一旦MeasureSpec确定,onMeasure中就可以确定View的测量宽/高。
三、View的工作流程
主要指measure、layout、draw这三大流程。measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点的位置,而draw则将View绘制到屏幕上。
ViewRootImpl#performTraversals
会依次调用performMeasure
、performLayout
和performDraw
三个方法,这三个方法分别开启顶级View的measure、layout和draw这三大流程。
其中performMeasure
中会调用顶级View#measure 方法,measure调用onMeasure
,在onMeasure 方法中则会测量自身并调用所有子元素measure方法,这样就完成了一次measure过程;子元素会重复父容器的measure过程,如此反复完成了整个View树的遍历。另外两个过程同理。
1、ViewGroup的Measure流程
对于ViewGroup既要测量自身,也要遍历子元素的measure方法(通过实现onMeasure方法)。
在performMeasure方法中,调用了DecorView#measure(继承自View,其实调用的是View#measure),measure会调用onMeasure方法。ViewGroup并没有定义onMeasure,这个方法需要子类去实现,主要需要实现两个功能:①测量自身②测量子View。
ViewGroup提供了measureChildWithMargins
和measureChildren
方法。
1.1、measureChildWithMargins方法
protectedvoidmeasureChildWithMargins(Viewchild,
intparentWidthMeasureSpec,intwidthUsed,
intparentHeightMeasureSpec,intheightUsed){
finalMarginLayoutParamslp=(MarginLayoutParams)child.getLayoutParams();
//入参:父容器的MeasureSpec;父的padding和自身的margin(剩下为子元素可用空间);自身的宽度。
finalintchildWidthMeasureSpec=getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft+mPaddingRight+lp.leftMargin+lp.rightMargin
+widthUsed,lp.width);
finalintchildHeightMeasureSpec=getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop+mPaddingBottom+lp.topMargin+lp.bottomMargin
+heightUsed,lp.height);
//注意:此时的入参是自身的MeasureSpec。measure又会调用child#onMeasure方法
child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
}
从上面的方法可以看出,View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定,MeasureSpec一旦确定,onMeasure中就可以确定View的测量宽/高。getChildMeasureSpec(int spec, int padding, int childDimension)
方法的逻辑整理出如下表格:
表中的parentSize是指父容器目前可以使用的大小,即父容器的specSize减去入参padding。
ViewGroup并没有定义onMeasure,需要其子类去实现,为什么ViewGroup不像View一样对其onMeasure做统一呢?因为不同的ViewGroup子类有不同的布局特征,导致测量细节各不相同,无法统一。
根据上面的表格,我们发现父容器的MeasureSpec属性为AT_MOST,子元素的LayoutParams为WRAP_CONTENT的时候,子元素的测量模式为AT_MOST,它的SpecSize为父容器的SpecSize减去padding(入参),也就是说子元素WRAP_CONTENT和MATCH_PARENT一样的。为了解决这个问题,需要在WRAP_CONTENT时指定一下默认的宽高。
1.2、measureChildren方法
measureChildren
中会循环调用measureChild方法,在measureChild中,首先会调用getChildMeasureSpec方法,入参和上面类似,区别在于padding入参仅仅为自身的padding,然后会调用子元素的measure方法(和measureChildWithMargins
非常类似)。
2、View的Measure过程
View的measure方法是一个final方法,会调用onMeasure方法,因此只需要关注onMeasure方法,入参为自己的measureSpec
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension
用于设置测量的宽高,测量好之后,必须调用。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
简单理解,getDefaultSize返回的就是measureSpec中的specSize,这就是View测量后的大小。在AT_MOST和EXACTLY模式下,都返回了specSize。也就是说对于一个直接继承View的自定义View,它的wrap_content和match_parent效果一样,因此如果要实现自定义View的wrap_content,则要重写onMeasure方法。解决问题:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
// 在 MeasureSpec.AT_MOST 模式下,给定一个默认值mWidth,mHeight。默认宽高灵活指定
//参考TextView、ImageView的处理方式
//其他情况下沿用系统测量规则即可
if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
getSuggestedMinimumWidth()
方法就是:如果View没有设置背景,就返回minWidth属性值(可以为0);如果设置了背景,就返回minWidth和背景的最小宽度之间的最大值。
View的measure过程是三大流程中最复杂的一个,measure完成以后,通过 getMeasuredWidth/Height 方法就可以正确获取到View的测量后宽/高。在某些情况下,系统可能需要多次measure才能确定最终的测量宽/高,所以在onMeasure中拿到的宽/高很可能不是准确的。一个较好的习惯是在onLayout方法中,去获取View测量宽高或最终宽高。
3、如何正确获得宽高
如果我们想要在Activity启动的时候就获取一个View的宽高,怎么操作呢?因为View的measure过程和Activity的生命周期并不是同步执行,无法保证在Activity的 onCreate、onStart、onResume 时某个View就已经测量完毕。所以有以下四种方式来获取View的宽高:
3.1、Activity/View#onWindowFocusChanged
onWindowFocusChanged这个方法的含义是:VieW已经初始化完毕了,宽高已经准备好了,需要注意:它会被调用多次,当Activity的窗口得到焦点和失去焦点均会被调用。
3.2、view.post(runnable)
通过post将一个runnable投递到消息队列的尾部,当Looper调用此runnable的时候,View也初始化好了。
3.3、ViewTreeObserver
使用 ViewTreeObserver 的众多回调可以完成这个功能,比如OnGlobalLayoutListener 这个接口,当View树的状态发送改变或View树内部的View的可见性发生改变时,onGlobalLayout 方法会被回调,这是获取View宽高的好时机。需要注意的是,伴随着View树状态的改变, onGlobalLayout 会被回调多次。
3.4、view.measure(int widthMeasureSpec,intheightMeasureSpec)
手动对view进行measure。需要根据View的layoutParams分情况处理:
- match_parent:直接放弃。根据上表所示,需要知道parentSize,即父容器剩余空间,而此时无法知道这个值。
- 具体的数值( dp/px):
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
- wrap_content:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
// View的尺寸使用30位二进制表示,最大值30个1,在AT_MOST模式下,我们用View理论上能支持的最大值去构造MeasureSpec是合理的
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);
四、layout过程
layout
方法确定View本身的位置,会调用onLayout
方法。onLayout
确定所有子元素的位置,通过遍历所有的子View并调用其layout
方法。
View#layout中,setFrame
确定View的四个顶点位置,即初始化mLeft,mRight,mTop,mBottom这四个值(确定了最终的宽高),也就确定了View在父容器中的位置。接着调用onLayout方法,确定所有子View的位置,和onMeasure一样,onLayout的具体实现和布局有关,因此View和ViewGroup均没有真正实现onLayout方法。
View的测量宽高和最终宽高的区别:
在View的默认实现中,View的测量宽高和最终宽高相等,只不过测量宽高形成于measure过程,最终宽高形成于layout过程。即便View需要多次测量才能确定自己的测量宽高,但最终来说,测量宽高和最终宽高还是一致。
五、draw过程
View的绘制过程遵循如下几步:
- 绘制背景 drawBackground(canvas)
- 绘制自己 onDraw
- 绘制children dispatchDraw 遍历所有子View的 draw 方法
- 绘制装饰 onDrawScrollBars
View#setWillNotDraw,如果一个View不需要绘制任何内容,那么置为ture,系统会进行相应的优化。默认情况下,View为false,ViewGroup为true。所以自定义ViewGroup需要通过onDraw来绘制内容时,必须显式的关闭 WILL_NOT_DRAW 这个优化标记位,即调用 setWillNotDraw(false)。