View在我们日常开发中十分常见,常见的控件如Button,TextView等就是一个View,而LinearLayout,RelativeLayout等就是ViewGroup,而ViewGroup又是继承View的,所以可以说View是所有控件的基类,由此可见View的地位和作用十分重要,所以我们有必要了解一下View的基础知识。
一个View要能被我们肉眼所见,那么该View必然得先经过绘制,而View的绘制主要包括三大流程,分别为measure,layout和draw。
· measure: 测量View的宽高,其中ViewGroup除了测量自己的宽高之外还要调用measureChildren测量子元素。
在自定义View时要重写onMeasure,否则当设置View的大小为wrap_content时,会发现该属性不起作用。这是为什么呢?要回答这个问题,还得引出参与测量过程的一个重要角色----MeasureSpec,MeasureSpec是一个32为的int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(测量大小)。在测量过程中,View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽高。也就是说MeasureSpec由LayoutParams和父容器所决定,而View的测量宽高由MeasureSpec决定。
虽然说了MeasureSpec在测量过程中的重要性,但是还没回答为什么在自定义View中直接使用wrap_content属性会不起作用。别着急,接下来马上说。刚才说了MeasureSpec的高2位是SpecMode,即测量模式。SpecMode有三种模式:UNSPECIFIED,EXACTLY,AT_MOST。
UNSPECIFIED:父容器对View没有任何限制,View需要多少空间就给多少,不过该模式一般用于系统内部,所以我们不用关注。
EXACTLY:父容器已经检测出了View所需的精确大小,这时View的值就是SpecSize所指定的值。对应于LayoutParams中的match_parent和具体的数值两种模式。
AT_MOST:父容器指定了SpecSize,View的大小不能超过该值,SpecSize的具体值由具体的View实现,对应于LayoutParams中的wrap_content。
上面说了MeasureSpec由父容器和LayoutParams所决定,无论当父容器的模式是EXACTLY还是AT_MOST,如果View的宽或高为wrap_content,此时View的大小是父容器剩余可用空间的大小,这也就是为什么自定义View时wrap_content属性不起作用。此时只要重写onMeasure方法即可。例如下面做法通过重写onMeasure方法,给View指定一个默认宽或高,然后在View使用wrap_content属性时使用该默认值,从而解决了wrap_content不起作用的问题。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//默认宽/高
int mWidth = 200;
int mHeigh = 200;
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heighSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heighSize = MeasureSpec.getSize(heightMeasureSpec);
if(widthSpecMode == MeasureSpec.AT_MOST && heighSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth, mHeigh);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth, heighSize);
}else if(heighSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSize, mHeigh);
}
}
·layout:确定View的位置,同时也确定了View的最终大小。可能看到这会觉得有点奇怪,measure不是测量View的宽高吗?为什么在layout方法中确定了最终宽高?其实不矛盾,measure方法执行完之后得到的是测量宽高,layout确定的是最终宽高,两者在绝大多数情况下是相等的。
该方法的流程大致分为两步:1.调用setFrame方法来确定View的四个顶点的位置;2.调用onLayout方法,用于父容器确定子元素的位置。
·draw:将View绘制到屏幕上去,一般遵循下面流程:1.绘制背景;2.绘制自己;3.绘制children;4.绘制装饰。
经过上面三大流程,View就绘制出来了,而一个View不同于Drawable的地方在于View不仅能显示在屏幕上,还能响应外部事件。假设我们点击一个设置了监听的Button,一般情况下引起UI的变化,在我们按下手指之后Button内部发生了一系列什么事呢?这就是接下来要说的View的事件分发机制。
当一个点击事件产生后,它的传递顺序如下:Activity --> Window --> View;当所有元素都不处理该点击事件时,该点击事件又会一层一层往上传回去,最终将会传到Activity,由Activity处理。假设有View处理了点击事件,那么其将会按以下三部曲处理该点击事件。
当点击事件传到了ViewGroup之后,dispatchTouchEvent将会被调用,在该方法中又会调用onInterceptTouchEvent来判断是否拦截该事件,如果拦截,将会调用onTouchEvent处理该点击事件,如果不拦截,将会交给下一层View按照相同的方法去处理。其中,只要有点击事件传到该ViewGroup,其dispatchTouchEvent就会被调用,而一旦该View拦截了点击事件,那么之后的点击事件将会都交给其处理,不再调用onInterceptTouchEvent,当ViewGroup处理某事件序列时,该事件序列都会交给它处理,不会把事件序列再拆分出来,分给不同View处理;至于什么是事件序列?事件序列就是以down事件开始,包含n个move事件,以up事件结束。ViewGroup的onTouchEvent方法可能被屏蔽,例如当该ViewGroup设置了onTouchListener,则onTouch方法将会被调用,当onTouch方法返回true时,onTouchEvent方法就被屏蔽,当其返回false,onTouchEvent方法被调用,当onTouchEvent方法被调用时,如果当前设置了
onClickListener,那么onClick方法将被调用,由此可见我们平时设置的onClickListener优先级最低。
当点击事件往下传被某个View处理时,如果该View不消耗ACTION_DOWN事件(onTouchEvent返回false),则该事件将会上传到上一层由其父元素去处理,即父元素的onTouchEvent将会被调用。如果该View不消耗的时间不是ACTION_DOWN事件,那么该事件将不会往上传而是会消失。onTouchEvent默认返回值是true,那什么情况下View的onTouchEvent默认值返回false?即什么情况下View不消耗事件呢?只有当View的clickable和longClickable同时为false时,View的onTouchEvent才会返回false,而enable属性则不会影响onTouchEvent的默认返回值。
点击事件往下传时,最终总会传递到一个类似button之类的View中,当点击事件传到这些View中时,和上面一样,dispatchTouchEvent会被调用,但这些View里面已经没有子元素了,所以其内部没有onInterceptTouchEvent方法,而是会调用onTouchEvent方法处理事件,但在调用onTouchEvent之前会上面讲的那样先判断该View是否有设置onTouchListener,之后的处理与上面类似。
View的事件分发机制大致说完了,了解了View的绘制和事件分发之后,我们可能会尝试自定义一些View,自定义View第一步会遇到的是写构造器,下面说说自定义View的三种构造器,本来应该是有四种,但是有一些是类似的,所以只说三种。先看看这三种构造器的写法。
public CircleView(Context context){
super(context);
}
public CircleView(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
}
这几个构造器第一眼看起来只是参数不同,其实它们还是有挺大区别的。其中第一种构造器是在代码中动态创建自定义View时调用的。第二种是在XML中使用自定义属性时会被调用,这个方法实际上是调用下面的构造器,所以要用关键字“this”,不能用“super”。至于第三个构造器,可以在该方法中对我们自定义View的属性进行统一初始化,然后在XML使用自定义View时可以使用相应的自定义属性。