View的工作流程
measure、layout、draw三大流程
1.View的measure过程
View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在View的measure方法中会去调用View的onMeasure方法
setMeasuredDimension方法会设置View宽高的测量值,我们看一下getDefaultSize方法
getDefaultSize
getDefaultSize返回的大小就是measureSpec中的specSize就是View测量后的大小,这里多次提到测量后的大小,是因为View最终的大小是在layout阶段确定的,但是几乎所有情况下View的测量大小和最终大小是相等的。至于UNSPECIFIED情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize的第一个参数size,即宽高分别为getSuggestedMinimumWidth和getSuggestedMinimumHeight这两个方法的返回值。
分析上图第二个,如果View没有设置背景,那么View的宽度为mMinWidth,而mMinWidth对应于android:minWidth这个属性所指定的值,因此View的宽度即为android:minWidth属性所指定的值。如果这个属性不指定,那么mMinWidth则默认为0,如果View指定了背景,则View的宽度为max(mMinWidth,mBackground.getMinimumWidth()).
getMinimumWidth方法
该方法返回的就是Drawable的原始宽度,前提是这个View有原始宽度,否则返回0。
总结getSuggestedMinimum
如果View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0,如果View设置了背景,则返回android:minWidth和背景这两者中的最大值。getSuggestedMinimum的返回值就是View在UNSPECIFIED情况下的测量宽高。从getDefaultSize方法来看,View的宽高由specSize决定,所以直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身的大小,否则在布局中使用wrap_content就相当于使用match_parent。从上述代码中我们可以知道如果View在布局中使用了wrap_content,那么他的specmode是AT_MOST模式,在这种模式下它的宽高等于specSize。查表可得此时specSize为parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器当前剩余空间大小,显然,View的宽高就等于父容器当前剩余的空间大小,这种效果和布局中使用match_parent完全一致。
解决wrap_content失效问题
我们只需要给View指定一个默认的内部宽高(mWidth mHeight),并在wrap_content时设置此宽高即可,对于非wrap_content情形,我们沿用系统的测量值。这个默认的内部宽高大小如何指定根据需求。
ViewGroup的measure过程
对于ViewGroup来说,除了完成自己的measure过程,还会去遍历调用所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,所以它没有重写View的onMeasure方法,但提供了一个measureChildren的方法。
measureChildren
上图中ViewGroup对每一个子元素进行measure
measureChild
measureChild取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传给View的measure方法处理。
ViewGroup并没有定义其测量的具体过程,因为不同的ViewGroup子类有不同的布局特性,导致它们的细节各不相同。
分析LinearLayout的onMeasure
LineaarLayout的onMeasure
measureVertical的源码
由于太长只展示部分
系统会遍历每个子元素并执行measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法,这样各个子元素就开始依次进入measure过程,并且系统会通过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度。每测量一个子元素,mTotalLength就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的margin等。当子元素测量完毕后,LinearLayout会测量自己的大小,如下图。 当子元素测量完毕后,LinearLayout会根据子元素的情况来测量自己的大小。针对竖直的LinearLayout而言,它在水平方向的测量过程遵循View的测量过程,在竖直方向的测量过程和View有所不同。具体指如果它的布局中高度采用match_parent或者具体数值,那么它的的量过程与View一致,即高度为SpecSize,如果布局中高度采用wrap_content,那么它的高度是所有子元素所占用的高度总和,但是仍然不能超过它的父容器的剩余空间,最终高度还要考虑其在竖直方向的padding。
上图中resolveSizeAndState源码
setMeasuredDimension方法源码
measure完成后,通过fetMeasuredWidth/Height方法就可以正确获取到View的测量宽高。需要注意在某些极端情况下系统可能需要多次measure才能确定最终的测量宽高,在这种情况下在onMeasure拿到的测量宽高可能是不准确的。一个比较好的习惯是从onLayout方法中去获取View的测量宽高或者最终宽高。
问题
当我们想在Activity已启动的时候就做一件任务,但这件任务需要获取某个View的宽高。但是在onCreate、onStart、onResume中均无法正确得到某个View的宽高信息,这是因为View的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证。
解决办法
1.Activity/View#onWindowFocusChanged
这个方法的含义是:View已经初始化完毕了,宽高已经准备好了,这个时候去获取宽高是没有问题的。但是onWindowFocusChanged会被调用多次,当Activity的窗口得到焦点和失去焦点时都会被调用一次。也就是当Activity继续执行和暂停执行时,它都会被调用,如果频繁的进行onResume和onPause那么它也会被频繁调用。
所以可以通过重写该方法在其中获取宽高
2.view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候View也初始化好了。 使用方法
3.ViewTreeObserver
使用Observer的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当View树的状态发生改变或者View树内部的View的客家逆行发生改变时,onGlobalLayout方法将被回调,因此这是获取View的宽高一个很好的时机。伴随着View树的状态改变等,onGlobalLayout会被调用多次。
使用方法
4.view.measure(int widthMeasureSpec,int heightMeasureSpec)
通过手动对View进行measure来得到view的宽高,比较复杂,
要根据View的LayoutParams来分
1.match_parent
无法measure出具体的宽高,因为构造此种MeasureSpec需要parentSize而这个时候我们无法知道parentSize的大小。
2.具体数值
3.wrap_content
注意(1<<30)-1,View吃醋还能使用30位二进制表示,也就是说最大是30个1(2^30-1)也就是(1<<30)-1,在最大化模式下我们用View理论上能支持的最大值去构造MeasureSpec是合理的
measure的两种错误用法
2.layout过程
ViewGroup用于确定子元素的位置,当ViewGroup位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。Layout过程和measure过程相比较简单。layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置。
下图为View的layout方法。
layout大致流程
先通过setFrame方法来设定View四个顶点的位置,即初始化mLeft,mRight,mTop,mBottom,View这四个顶点一旦确定,那么View在父容器中的位置也确定了,接着会调用onLayout方法,用于父容器决定子元素的位置,它的具体实现和具体的布局有关,所以View和ViewGroup都没有具体实现。 下图是LinearLayout的onLayout方法
选择layoutVertical讲解
该方法会遍历所有子元素并调用setChildFrame方法指定子元素的位置,其中childTop会逐渐增大,意味着后面的子元素会被放置在靠下的位置,正好符合竖直方向的LinearLayout的特性。setChildFrame只是调用子元素的layout方法而已,这样父容器在layout方法中完成自己的定位之后,就通过onLayout方法会调用子元素的layout方法,子元素有会通过自己的layout方法来确定自己的位置,这样一层一层完成了整个View树的layout过程。
以下是setChildFrame的源码
setChildFrame中的width和height实际上就是子元素的测量宽高
而在layout方法中会通过setFrame去设置子元素的四个顶点位置,在setFrame中有几句赋值语句来确定子元素的位置
View的测量宽高和最终宽高有什么区别?
这个问题可以具体为View的getMeasuredWidth和getWidth有什么区别 首先看一下getWidth的实现
结合mLeft等四个变量的赋值过程来看,getWidth方法的返回值刚好就是View的的测量宽度。在View的默认实现中,View的测量宽高和最终宽高是相等的,只不过测量宽高形成于View的measure过程,而最终宽高形成于View的layout过程,即两者的赋值时机不同,测量宽高的赋值时机要早一点。在日常开发中,我们可以认为View的测量宽高等于最终宽高,但是的确存在情况会导致两者不一致。
重写View的layout代码
上述代码会导致在任何情况下View的最终宽高总是比测量宽高大100px,虽然这么做会导致View显示不正常并且没有实际意义,但这证明了测量宽高的确可以不等于最终宽高。另外一种情况是在某些情况下View需要多次measure才能确定自己的测量宽高,在前几次的测量过程中,其得出的测量宽高有可能和最终宽高不一致,但最终来说两者相同。
draw过程
draw步骤
1.绘制背景 background.draw(canvas) 2.绘制自己 onDraw 3.绘制children dispatchDraw 4.绘制装饰 onDrawScrollBars //在新版本中已经改为onDrawForeground方法,在该方法中调用了onDrawScrollBars
draw源码
View的绘制过程传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层一层的传递下去
View的一个特殊方法setWillNotDraw
如果一个view不需要绘制任何内容那么设置这个标记位为true以后,系统会进行相关优化。ViewGroup默认启动该标记位,View默认不启动。当我们的自定义控件继承于ViewGroup并且自身并不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当明确知道一个ViewGroup需要通过onDraw来绘制内容时我们需要显式关闭这个标记位。
自定义View
自定义View的分类
1.继承View重写onDraw方法
主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态地现实一些不规则的图形。显然这需要通过绘制的方式来实现,即重写onDraw方法。采用这种方式需要自己直至wrap_content并且padding也要自己处理。
2.继承ViewGroup派生特殊的Layout
主要用于实现自定义布局,当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法来实现。需要合适地处理ViewGroup的测量、布局两个过程,并同时处理子元素的测量和布局过程
3.继承特定的View
比较常见,一般是用于扩展某种已有的View的功能,比较容易,不需要自己支持wrap_content和padding
4.继承特定的ViewGroup
比较常见,不需要自己处理ViewGroup的测量和布局,理论上2能实现的效果方法4也能,但2更接近View的底层
自定义View须知
1.让View支持wrap_content
如果不对其做特殊处理那么当外界在布局使用它时就无法达到预期效果
2.若有必要让你的view支持padding
因为直接继承View的控件如果不在draw方法中处理padding,那么padding属性是无法起作用的。直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然会导致padding和子元素的margin失效。
3.尽量不要在View中使用Handler
因为View内部本身就提供了post方法完全可以替代Handler
4.View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
如果有线程或者动画需要停止时那么onDetachedFromWindow是一个很好的时机。当包含此View的Activity退出或当前View被remove时,这个方法会被调用,和此方法对应的是onAttachedToWindow,它会在包含此View的Activity启动时调用。当View变得不可见时我们也需要停止,否则有可能造成内存泄漏
5.View带有滑动嵌套情形时,处理好滑动冲突
自定义View示例
自定义的Circle代码1.0
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
public class Circle extends View {
private int mColor= Color.BLUE;
private Paint mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
private void init(){
mPaint.setColor(mColor);
}
public Circle(Context context) {
super(context);
init();
}
public Circle(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public Circle(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public Circle(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width=getWidth();
int height=getHeight();
int radius=Math.min(width,height)/2;
canvas.drawCircle(width/2,height/2,radius,mPaint);
}
}
上面的代码定义了一个具有圆形效果的自定义View,它会在自己的中心点以宽高的最小值为直径绘制一个蓝色的实心圆
Activity_main.xml代码
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.rdc.testforlearn.Circle
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/circle"
android:background="@color/black"
android:layout_width="match_parent"
android:layout_height="100dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
展示效果
再设置20dp的margin
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/circle"
android:background="@color/black"
android:layout_margin="20dp"
android:layout_width="match_parent"
android:layout_height="100dp"/>
显示效果
设置20dp的padding
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/circle"
android:background="@color/black"
android:layout_margin="20dp"
android:padding="20dp"
android:layout_width="match_parent"
android:layout_height="100dp"/>
显示效果
padding没有生效,因为我们前面提到过直接继承自View和ViewGroup的控件,padding是默认无法生效的,需要自己处理
修改width的模式(顺便修改了一下布局)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.rdc.testforlearn.Circle
android:id="@+id/circle"
android:background="@color/black"
android:layout_margin="20dp"
android:padding="20dp"
android:layout_width="wrap_content"
android:layout_height="100dp"/>
</LinearLayout>
显示效果
wrap_content并没有起到预期效果,通过对比我们可以发现使用match_parent和wrap_content没有任何区别。这一点在前面也已经提到过:对于直接继承自View的控件,如果不对wrap_content做特殊处理就相当于使用match_parent。
解决办法
1.wrap_content
为其指定一个wrap_content模式下的默认宽高即可
在Circle类中增加如下代码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode=MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec);
if (widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){//当宽高都为wrap_content时
setMeasuredDimension(200,200);//设置默认值
}else if(widthSpecMode==MeasureSpec.AT_MOST){//当宽为wrap_content时
setMeasuredDimension(200,200);
}else if(heightSpecMode==MeasureSpec.AT_MOST){//当高为wrap_content时
setMeasuredDimension(200,200);
}
}
显示效果
2.padding
修改onDraw如下
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingLeft=getPaddingLeft();
int paddingRight=getPaddingLeft();
int paddingTop=getPaddingLeft();
int paddingBottom=getPaddingLeft();
int width=getWidth()-paddingLeft-paddingRight;
int height=getHeight()-paddingTop-paddingBottom;
int radius=Math.min(width,height)/2;
canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,radius,mPaint);
}
中心思想就是绘制时考虑到View四周的空白即可,其中圆心和半径都会考虑到View四周的padding
显示效果
自定义属性
添加步骤 1.在values目录下创建自定义属性的XML,比如attrs.xml,也可以选择类似attrs_circle_view.xml等这种以attrs_开头的文件名,文件名并没有什么限制,可以随便取。 创建attrs.xml
在上面的xml中声明了一个自定义属性集合Circle,在这个集合里面可以有很多自定义属性,这里的格式color指的是颜色。除了颜色格式,自定义属性还有其他格式,比如reference指的是资源id,dimension指尺寸,而像string integer boolean这种指基本数据类型等等。
2.在View的构造方法中解析自定义属性的值并做相应处理
首先加载自定义属性集合Circle,接着解析Circle属性集合中的circle_color。在这一步骤中,如果在使用时没有指定circle_color这个属性,那么就会选择红色作为默认的颜色,解析完属性后,通过recycle方法来实现资源。
上图是书中的构造方法,注意第二个构造函数调用了本类的第三个构造方法,在正常情况下,第一个构造函数在动态创建一个view的时候调用,第二个构造函数在xml静态调用的时候调用,第三个调用方法要在第一或者第二个构造函数显式调用才会执行。
xml修改如下
需要注意为了使用自定义属性,必须在布局文件中添加schemas声明:xmlns:app=“http://schemas.android.com/apk/res-auto” app是自定义属性的前缀,也可以换成其他名字,相应的下方属性也得更改名字。
显示效果
|