android view是大家实现各种漂亮ui的基础,因此对于它的重要性,就可想而知了;网上关于android view分析的文章也是非常的多,之所以还写这篇文章主要还是,通过看大家的分析和自己的理解做一个整理和记录,这样会有个更加深刻的印象。
android view 有几万行的代码,
本文主要针对view绘制流程的主要三个方法进行分析:测量(Measure)、布局(Layout)、绘制(draw)
想要搞懂一个知识,最好的办法是通过实例来分析,所以这里我写了个简单的例子来看下Activity 中setContentView()时view三个方法的流程:
(1)自定义的一个view,重写onMeasure、onLayout、onDraw方法:
public class CustomView extends View {
private static final String TAG = "CustomView";
public CustomView(Context context) {
super(context);
Log.e(TAG,"构造方法");
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.e(TAG,"onMeasure");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.e(TAG,"onLayout");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.e(TAG,"onDraw");
}
}
(2)activity 中使用:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final CustomView view = new CustomView(this);
view.setBackgroundResource(R.drawable.ic_launcher_background);
setContentView(view);
}
跑下程序看下log 跑的结果:
可以看到三个方法的执行顺序是:onMeasure->onLayout->onDraw,但是这里执行了三次onMeasure两次onLayout,
这是为什么呢?
我们知道把视图展示到屏幕上,是有一个树形结构的视图,而这个视图树的根节点是DecorView,而DecorView是FrameLayout的子类,我们看FrameLayout的源码,会调用两次onMeasure,而在onLayout后又执行onMeasure->onLayout这个过程,视图大小发生变化然后调用requestLayout()方法,而requestLayout()会导致调用measure()过程 和 layout()过程 。
知道了这三个方法的执行流程,我们来分别解释下这三个方法的作用:
1.onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
我们看到onMeasure源码中调用了setMeasuredDimension方法,setMeasuredDimension源码:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
可以看到setMeasuredDimension()就是真正的测量视图的大小,测量一个view实际上是给字段mMeasuredWidth,mMeasuredHeight设置值,最后执行mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET,将字段mPrivateFlags的EASURED_DIMENSION_SET位设置为1。
这时候我们实例中调用下setMeasuredDimension:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(1000,1000);
Log.e(TAG,"onMeasure");}
如果没重写setMeasuredDimension方法,默认是满屏的,调用后就设置了视图的大小.
接下来我们通过另一个方式来了解下onMeasure方法设置视图大小与父视图的关系:
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.linwenbing.demo.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#000000"
/></android.support.constraint.ConstraintLayout>
我这边自定义的view父视图设置的宽高都是match_parent 而view本事宽高是wrap_content,运行发现,子视图需然设置的wrap_content但是确布满整个屏幕,但是当自定义view设置具体的宽高时,就是根据具体的宽高显示所以得出结论:
当子视图没有设置具体宽高时,子视图采用的是父视图的宽高。这是什么原因呢?看源码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
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;
}
我们发现AT_MOST (相当于wrap_content )和EXACTLY (相当于match_parent )两种情况返回的测量宽高都是specSize,而这个specSize正是我们上面说的父控件剩余的宽高,所以默认onMeasure方法中wrap_content 和match_parent 的效果是一样的,都是填充剩余的空间。
要真正理解onMeasure我们还需要知道onMeasure两个参数int widthMeasureSpec, int heightMeasureSpec的意思:
这两个参数并不是真正的宽高,这两个参数中包含了两个意思:MeasureSpec类中的specMode和specSize
MeasureSpec类中specMode的三种模式:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
* 父控件不强加任何约束给子控件,它可以是它想要任何大小
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
* 父控件已为子控件确定了一个确切的大小,孩子将被给予这些界限,不管子控件自己希望的是多大
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
* 父控件会给子控件尽可能大的尺寸
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
specSize:父控件传过来的大小。
onMeasure方法总结:
(1)onMeasure (int widthMeasureSpec, int heightMeasureSpec)是view自己的方法
(2)onMeasure 方法简单的理解就是是用于测量视图的大小,主要是用来测量自己和内容的来确定宽度和高度
(3)onMeasure有两个参数( int widthMeasureSpec, int heightMeasureSpec),该参数表示控件可获得的空间以及关于这个空间描述的元数据.
(4)widthMeasureSpec和heightMeasureSpec这两个值通常情况下都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小。
2.onLayout方法:
(1)首页我们看onLayout的源码:
/**
* Called from layout when this view should
* assign a size and position to each of its children.
*
* Derived classes with children should override
* this method and call layout on each of
* their children.
* @param changed This is a new size or position for this view
* @param left Left position, relative to parent
* @param top Top position, relative to parent
* @param right Right position, relative to parent
* @param bottom Bottom position, relative to parent
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
会发现什么都没有实现,这是因为:
这个其实是android留给我们自己去实现的一个方法,也就是大家都知道的,去布局子View的位置,只有含有子View的容器,才需要重写这个方法,也就是ViewGroup。
而view是通过layout方法来确认自己在父容器中的位置。
viewgroup自定义的onLayout:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int hadUsedHorizontal = 0;//水平已经使用的距离
int hadUsedVertical = 0;//垂直已经使用的距离
int width = getMeasuredWidth();
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
if (view.getMeasuredWidth() + hadUsedHorizontal > width) {
hadUsedVertical = hadUsedVertical + view.getMeasuredHeight() + verticalSpace;
hadUsedHorizontal = 0;
}
view.layout(hadUsedHorizontal, hadUsedVertical, hadUsedHorizontal + view.getMeasuredWidth(), hadUsedVertical + view.getMeasuredHeight());
hadUsedHorizontal = hadUsedHorizontal + horizontalSpace + view.getMeasuredWidth();
}
}
onLayout总结:
onLayout主要是viewgroup用来确定子view在父容器中的位置,所以在自定义viewgroup时需要重写onLayout.
3.onDraw方法:
onDraw方法比较简单,主要就是把view绘制到屏幕上,看源码:
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {}
也是一个没有任何实现的方法,主要是提供给view自己绘制,onDraw怎么绘制,这里不做详细说,就写个简单绘制一个位于中心的圆:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.e(TAG,"onDraw");
canvas.drawCircle(getMeasuredWidth()/2,getMeasuredHeight()/2,150,mPaint);
}
android view是大家实现各种漂亮ui的基础,因此对于它的重要性,就可想而知了;网上关于android view分析的文章也是非常的多,之所以还写这篇文章主要还是,通过看大家的分析和自己的理解做一个整理和记录,这样会有个更加深刻的印象。
android view 有几万行的代码,
本文主要针对view绘制流程的主要三个方法进行分析:测量(Measure)、布局(Layout)、绘制(draw)
想要搞懂一个知识,最好的办法是通过实例来分析,所以这里我写了个简单的例子来看下Activity 中setContentView()时view三个方法的流程:
(1)自定义的一个view,重写onMeasure、onLayout、onDraw方法:
public class CustomView extends View {
private static final String TAG = "CustomView";
public CustomView(Context context) {
super(context);
Log.e(TAG,"构造方法");
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.e(TAG,"onMeasure");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.e(TAG,"onLayout");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.e(TAG,"onDraw");
}
}
(2)activity 中使用:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final CustomView view = new CustomView(this);
view.setBackgroundResource(R.drawable.ic_launcher_background);
setContentView(view);
}
跑下程序看下log 跑的结果:
可以看到三个方法的执行顺序是:onMeasure->onLayout->onDraw,但是这里执行了三次onMeasure两次onLayout,
这是为什么呢?
我们知道把视图展示到屏幕上,是有一个树形结构的视图,而这个视图树的根节点是DecorView,而DecorView是FrameLayout的子类,我们看FrameLayout的源码,会调用两次onMeasure,而在onLayout后又执行onMeasure->onLayout这个过程,视图大小发生变化然后调用requestLayout()方法,而requestLayout()会导致调用measure()过程 和 layout()过程 。
知道了这三个方法的执行流程,我们来分别解释下这三个方法的作用:
1.onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
我们看到onMeasure源码中调用了setMeasuredDimension方法,setMeasuredDimension源码:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
可以看到setMeasuredDimension()就是真正的测量视图的大小,测量一个view实际上是给字段mMeasuredWidth,mMeasuredHeight设置值,最后执行mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET,将字段mPrivateFlags的EASURED_DIMENSION_SET位设置为1。
这时候我们实例中调用下setMeasuredDimension:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(1000,1000);
Log.e(TAG,"onMeasure");}
如果没重写setMeasuredDimension方法,默认是满屏的,调用后就设置了视图的大小.
接下来我们通过另一个方式来了解下onMeasure方法设置视图大小与父视图的关系:
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.linwenbing.demo.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#000000"
/>
</android.support.constraint.ConstraintLayout>
我这边自定义的view父视图设置的宽高都是match_parent 而view本事宽高是wrap_content,运行发现,子视图需然设置的wrap_content但是确布满整个屏幕,但是当自定义view设置具体的宽高时,就是根据具体的宽高显示所以得出结论:
当子视图没有设置具体宽高时,子视图采用的是父视图的宽高。这是什么原因呢?看源码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
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;
}
我们发现AT_MOST (相当于wrap_content )和EXACTLY (相当于match_parent )两种情况返回的测量宽高都是specSize,而这个specSize正是我们上面说的父控件剩余的宽高,所以默认onMeasure方法中wrap_content 和match_parent 的效果是一样的,都是填充剩余的空间。
要真正理解onMeasure我们还需要知道onMeasure两个参数int widthMeasureSpec, int heightMeasureSpec的意思:
这两个参数并不是真正的宽高,这两个参数中包含了两个意思:MeasureSpec类中的specMode和specSize
MeasureSpec类中specMode的三种模式:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
* 父控件不强加任何约束给子控件,它可以是它想要任何大小
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
* 父控件已为子控件确定了一个确切的大小,孩子将被给予这些界限,不管子控件自己希望的是多大
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
* 父控件会给子控件尽可能大的尺寸
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
specSize:父控件传过来的大小。
onMeasure方法总结:
(1)onMeasure (int widthMeasureSpec, int heightMeasureSpec)是view自己的方法
(2)onMeasure 方法简单的理解就是是用于测量视图的大小,主要是用来测量自己和内容的来确定宽度和高度
(3)onMeasure有两个参数( int widthMeasureSpec, int heightMeasureSpec),该参数表示控件可获得的空间以及关于这个空间描述的元数据.
(4)widthMeasureSpec和heightMeasureSpec这两个值通常情况下都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小。
2.onLayout方法:
(1)首页我们看onLayout的源码:
/**
* Called from layout when this view should
* assign a size and position to each of its children.
*
* Derived classes with children should override
* this method and call layout on each of
* their children.
* @param changed This is a new size or position for this view
* @param left Left position, relative to parent
* @param top Top position, relative to parent
* @param right Right position, relative to parent
* @param bottom Bottom position, relative to parent
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
会发现什么都没有实现,这是因为:
这个其实是android留给我们自己去实现的一个方法,也就是大家都知道的,去布局子View的位置,只有含有子View的容器,才需要重写这个方法,也就是ViewGroup。
而view是通过layout方法来确认自己在父容器中的位置。
viewgroup自定义的onLayout:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int hadUsedHorizontal = 0;//水平已经使用的距离
int hadUsedVertical = 0;//垂直已经使用的距离
int width = getMeasuredWidth();
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
if (view.getMeasuredWidth() + hadUsedHorizontal > width) {
hadUsedVertical = hadUsedVertical + view.getMeasuredHeight() + verticalSpace;
hadUsedHorizontal = 0;
}
view.layout(hadUsedHorizontal, hadUsedVertical, hadUsedHorizontal + view.getMeasuredWidth(), hadUsedVertical + view.getMeasuredHeight());
hadUsedHorizontal = hadUsedHorizontal + horizontalSpace + view.getMeasuredWidth();
}
}
onLayout总结:
onLayout主要是viewgroup用来确定子view在父容器中的位置,所以在自定义viewgroup时需要重写onLayout.
3.onDraw方法:
onDraw方法比较简单,主要就是把view绘制到屏幕上,看源码:
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
}
也是一个没有任何实现的方法,主要是提供给view自己绘制,onDraw怎么绘制,这里不做详细说,就写个简单绘制一个位于中心的圆:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.e(TAG,"onDraw");
canvas.drawCircle(getMeasuredWidth()/2,getMeasuredHeight()/2,150,mPaint);
}