View树结构
每一个视图的绘制过程都必须经历三个最主要的阶段:onMeasure()、onLayout()、onDraw()
系统内部会依次调用DecorView的measure(),layout()和draw()三大流程方法。
measure()方法又会调用onMeasure()方法对它所有的子元素进行测量,如此反复调用下去就能完成整个View树的遍历测量。
同样的,layout()和draw()两个方法里也会调用相似的方法去对整个View树进行遍历布局和绘制。
测量规格MeasureSpec
MeasureSpec类:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
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);
}
}
widthMeasureSpec和heightMeasureSpec是32位的int类型,,它的高2位代表测量模式Mode,低30位代表测量大小Size。
测量模式三类:
unspecified | 父容器不对 view 有任何限制,要多大给多大 |
---|---|
exactly | 父容器已经检测出 view 所需要的大小 |
at_most | 父容器指定了一个大小, view 的大小不能大于这个值 |
绘制过程
1、measure()
/*
* final 标识符 , 不能被重载
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
//回调onMeasure()方法
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
//回调View视图里的onMeasure过程
private void onMeasure(int height , int width){
//设置该view的实际宽(mMeasuredWidth)高(mMeasuredHeight)
//1、该方法必须在onMeasure调用,否者报异常。
setMeasuredDimension(h , l) ;
//2、如果该View是ViewGroup类型,则对它的每个子View进行measure()过程
int childCount = getChildCount() ;
for(int i=0 ;i<childCount ;i++){
//获得每个子View对象引用
View child = getChildAt(i) ;
//整个measure()过程就是个递归过程
//该方法只是一个过滤器,最后会调用measure()过程 ;或者 measureChild(child , h, i)方法
measureChildWithMargins(child , h, i) ;
//其实,对于我们自己写的应用来说,最好的办法是去掉框架里的该方法,直接调用view.measure(),如下:
//child.measure(h, l)
}
}
//该方法具体实现在ViewGroup.java里 。
protected void measureChildWithMargins(View v, int height , int width){
v.measure(h,l)
}
2、layout()
/* final 标识符 , 不能被重载 , 参数为每个视图位于父视图的坐标轴
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
public final void layout(int l, int t, int r, int b) {
//设置每个视图位于父视图的坐标轴
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
//回调onLayout函数 ,设置每个子视图的布局
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~LAYOUT_REQUIRED;
}
mPrivateFlags &= ~FORCE_LAYOUT;
}
//回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现
private void onLayout(int left , int top , right , bottom){
//如果该View不是ViewGroup类型
//调用setFrame()方法设置该控件的在父视图上的坐标轴
setFrame(l ,t , r ,b) ;
//--------------------------
//如果该View是ViewGroup类型,则对它的每个子View进行layout()过程
int childCount = getChildCount() ;
for(int i=0 ;i<childCount ;i++){
//获得每个子View对象引用
View child = getChildAt(i) ;
//整个layout()过程就是个递归过程
child.layout(l, t, r, b) ;
}
}
3、draw()
private void draw(Canvas canvas){
//1、绘制该View的背景
//2、为绘制渐变框做一些准备操作
//3、调用onDraw()方法绘制视图本身
//4、调用dispatchDraw()方法绘制每个子视图,dispatchDraw()已经在Android框架中实现了,在ViewGroup方法中。应用程序程序一般不需要重写该方法,但可以捕获该方法的发生,做一些特别的事情。
//5、绘制渐变框
}
//ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法
@Override
protected void dispatchDraw(Canvas canvas) {
//其实现方法类似如下:
int childCount = getChildCount() ;
for(int i=0 ;i<childCount ;i++){
View child = getChildAt(i) ;
//调用drawChild完成
drawChild(child,canvas) ;
}
}
//ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法
protected void drawChild(View child,Canvas canvas) {
...
//简单的回调View对象的draw()方法,递归就这么产生了。
child.draw(canvas) ;
...
}
1)绘制背景 – drawBackground()
2)绘制自己 – onDraw()
3)绘制孩子 – dispatchDraw()
4)绘制装饰 – onDrawScrollbars()
dispatchDraw()方法内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法(注意,这个 地方“需要重绘”的视图才会调用draw()方法)。
值得说明的是,ViewGroup类已经为我们重写了dispatchDraw()的功能实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。
示例demo
1、自定义View
public class MyView extends View {
public MyView(Context context) {
super(context);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(800, 800);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画笔1:绘制圆,椭圆
Paint paint1 = new Paint();
paint1.setStyle(Paint.Style.FILL_AND_STROKE);
paint1.setColor(Color.BLACK);
canvas.drawCircle(120, 100, 50, paint1);
canvas.drawOval(250, 75, 400, 125, paint1);
//画笔2:绘制三角形,弧线
Path path_triangle = new Path();
path_triangle.moveTo(210, 175);//起始位置
path_triangle.lineTo(120, 260);
path_triangle.lineTo(300, 260);
path_triangle.lineTo(210, 175);
//三角形
Paint paint2 = new Paint();
paint2.setStyle(Paint.Style.STROKE);
paint2.setStrokeWidth(5);//画笔宽度
paint2.setColor(Color.BLUE);
canvas.drawPath(path_triangle, paint2);
//弧线
RectF rectF = new RectF(40, 150, 300, 325);//外切矩形范围
rectF.offset(50, 10);//矩形向右偏移100像素,向下偏移20像素
canvas.drawArc(rectF, 40, 100, false, paint2);
//画笔3:绘制矩形
Paint paint3 = new Paint();
paint3.setStyle(Paint.Style.FILL_AND_STROKE);
paint3.setColor(Color.RED);
canvas.drawRect(100, 400, 350, 500, paint3);
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_orange_light">
<com.example.viewmeasure_zlz.MyView
android:id="@+id/id_my_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
findViewById(R.id.id_my_view).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "------ My view is clicked !");
}
});
2、自定义ViewGroup
public class MyViewGroup extends ViewGroup {
private Context mContext;
public Button button;
public ImageView imageView;
public TextView textView;
public MyView myView;
public MyViewGroup(Context context) {
super(context);
mContext = context;
init();
}
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
init();
}
public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
init();
}
private void init() {
//child view 1 : Button
button = new Button(mContext);
button.setText("Button");
this.addView(button);
//child view 2 : ImageView
imageView = new ImageView(mContext);
imageView.setBackgroundResource(R.mipmap.ic_launcher);
this.addView(imageView);
//child view 3 : TextView
textView = new TextView(mContext);
textView.setText("text");
this.addView(textView);
//child view 4 : 自定义View
myView = new MyView(mContext);
this.addView(myView);
}
/**
* 对每个子View进行measure():设置每子View的大小,即实际宽和高
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取该ViewGroup的实际长和宽 涉及到MeasureSpec类的使用
int specSizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int specSizeHeight = MeasureSpec.getSize(heightMeasureSpec);
//设置本ViewGroup的宽高
setMeasuredDimension(specSizeWidth, specSizeHeight);
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
childView.measure(300, 300);
// this.measureChild(childView, widthMeasureSpec, heightMeasureSpec);
// this.measureChildWithMargins(childView, widthMeasureSpec, heightMeasureSpec);
}
}
/**
* 对每个子View视图进行布局
*
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//每个子View的起始横坐标
int startLeft = 0;
//每个子View距离父视图的位置,可以理解为 margin = 10px
int startTop = 10;
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
childView.layout(startLeft, startTop, startLeft + childView.getMeasuredWidth(), startTop + childView.getMeasuredHeight());
//校准startLeft值,View之间的间距设为10px
startLeft = startLeft + childView.getMeasuredWidth() + 10;
//校准startTop值,View之间的间距设为20px
startTop = startTop + childView.getMeasuredHeight() + 20;
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
Log.i("TAG_ZLZ", "------ dispatchDraw()");
super.dispatchDraw(canvas);
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
Log.i("TAG_ZLZ", "------ drawChild()");
return super.drawChild(canvas, child, drawingTime);
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_orange_light">
<com.example.viewmeasure_zlz.MyViewGroup
android:id="@+id/id_my_view_group"
android:layout_width="300dp"
android:layout_height="600dp"
android:background="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
MyViewGroup myViewGroup = findViewById(R.id.id_my_view_group);
myViewGroup.button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i("TAG_ZLZ", "------ myViewGroup button click !");
}
});
myViewGroup.imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i("TAG_ZLZ", "------ myViewGroup imageView click !");
}
});
myViewGroup.textView.setText("test");
myViewGroup.myView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i("TAG_ZLZ", "------ myViewGroup myView click !");
}
});