需要了解的
- 先来张图说明一下它们的关系
你还要知道ViewGroup之间是可以嵌套的.
View的绘制流程
- 不知道大家有没有这种疑惑, 为什么我们在写布局文件的时候, 一定要写layout_width和layout_height呢, 难道就没有默认值吗? 颜色, 背景, 等等其他的都有默认值, 为什么宽高就一定要我们手动写呢? 接下来就让我们一起来解答这个疑惑吧.
- 绘制流程的源码就不贴出来了, 有兴趣的可以打开View的源码对照着来看, 印象会更深刻, 当然, 不看源码, 理解以下的实例代码, 也不会影响你对整个流程的理解.
- 首先看一下View的绘制流程示例:
public class MyView extends View {
private static final String TAG = "MyView";
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* measure - > onMeasure ,view的源码中,measure会调用onMeasure
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //测量, 表示这个view的大小, 在View的源码中, Measure是final修饰的, 我们只能重写onMeasure方法
// TODO Auto-generated method stub
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, "measure");
}
/**
* layout - > setFrame 和 onLayout
*/
@Override
public void layout(int l, int t, int r, int b) { //布局,决定了摆放在父容器中的哪个位置
// TODO Auto-generated method stub
super.layout(l, t, r, b);
Log.d(TAG, "layout");
}
/**
* draw - > onDraw
*/
@Override
public void draw(Canvas canvas) { //绘制
// TODO Auto-generated method stub
super.draw(canvas);
Log.d(TAG, "draw");
}
}
- 以上就是View绘制显示在屏幕上必定会调用的三个方法, 可能你还不太理解, 不过没关系, 先混个脸熟, 有个印象先, 下面我们一个个分析.
View的onMeasure
- 来看一段Demo
public class MyView extends View {
private static final String TAG = "MyView";
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* measure -> onMeasure
*
* 1.父容器拿到孩子的申请的宽高layout_width, layout_height封装成宽高的期望widthMeasureSpec和heightMeasureSpec
* 父容器Relativelayout(或者其他Linearlyout)
* 调用MyView的 measure(int widthMeasureSpec, int heightMeasureSpec)传入对孩子宽高的期望
* measure -> onMeasure(widthMeasureSpec, heightMeasureSpec)
*
* @param widthMeasureSpec 父容器(RelativeLaoyut)对孩子MyView的宽度期望, 跟layout_width相关
* @param heightMeasureSpec 父容器(RelativeLaoyut)对孩子MyView的高度期望, 跟layout_height相关
* 这是我们为什么一定要指定layout_width和layout_height的原因.
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/**
* int widthMeasureSpec
* 32位二进制
* 前两位 是测量模式 mode
* public static final int UNSPECIFIED = 0 << MODE_SHIFT; 父容器对孩子没有任何的限制,孩子想多大多大
* public static final int EXACTLY = 1 << MODE_SHIFT; 父容器对孩子有确切的大小要求,大小就会后30位
* public static final int AT_MOST = 2 << MODE_SHIFT; 父容器对孩子的最大值有要求,大小就会后30位
* 后30位表示大小
*/
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
Log.d(TAG, "onMeasure mode " + (mode>>30) + " " + size);
//super方法默认使用父容器对我的期望的宽高
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//我们也可以不调用super直接调用setMeasuredDimension(50, 50)来指定宽高
}
}
- 注释里的setMeasuredDimension(int measuredWidth, int measuredHeight)是一个重要的方法, 它是整个测量结束的标志, 只有这个方法调用了, 我们才能调用getMeasuredWidth()或者getMeasuredHeight()方法获得测量宽高(注意, 不是实际宽高, 实际宽高要在布局完成之后)。
- 自定义View如果要使用wrap_content属性的话,则需重写onMeasure方法。
View的layout
- layout方法里面调用setFrame(),给View的上下左右四个位置mLeft, mTop,mRight, mBottom赋值,完成布局工作.
- onLayout()是一个空的方法,说明具体的布局不应该由view来决定
- 我们的view并不需要关心layout方法, 布局的事应该交由父容器去处理, 让它决定它的孩子应该摆放在哪个地方.
- 在布局完成后, 我们才能调用getWidth()和getHeight获得实际的宽高.
- getWidth()和getMeasuredWidth()的区别: getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。
View的draw
view的绘制分为6步:
- 对视图的背景进行绘制
- If necessary, save the canvas’ layers to prepare for fading (暂时忽略它)
- 对视图的内容进行绘制, 在onDraw(canvas)方法中完成
- 对当前视图的所有子视图进行绘制 ,调用dispatchDraw。
- If necessary, draw the fading edges and restore layers (暂时忽略它)
- 绘制装饰品(如滚动条)任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已.
即我们关心四个步骤:
- 绘制背景
- 绘制内容
- 绘制孩子
- 绘制装饰
绘制需要两个类, 画布(Canvas)和画笔(Paint), 通过以下Demo通过onDraw方法利用画笔在画布上绘制我们的图案吧.
public class MyView extends View {
private Paint mPaint;
private Bitmap mBitmap;
private Path mPath;
private RectF mOval;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
//设置去锯齿
mPaint.setAntiAlias(true);
//配置画笔,画空心圆
mPaint.setStyle(Style.STROKE);
//设置画笔宽度
mPaint.setStrokeWidth(3);
//设置画笔颜色
mPaint.setColor(Color.BLUE);
//画图片时需要设置图片
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.haha);
//设置扇形的大小
mOval = new RectF(5, 5, 195, 195);
initPath();
}
private void initPath() {
mPath = new Path();
//确定三个点
int x1 = 100, y1 = 5;
int x2 = 195, y2 = 195;
int x3 = 5, y3 = 195;
//移动到第一个点
mPath.moveTo(x1, y1);
//链接第一个点和第二个点
mPath.lineTo(x2, y2);
//链接第二个点和第三个点
mPath.lineTo(x3, y3);
mPath.lineTo(x1, y1);
}
/**
* 不要在onDraw方法里面创建新的对象,因为onDraw方法可能会频繁调用
*/
@Override
protected void onDraw(Canvas canvas) {
// 6. 裁剪
// canvas.clipPath(mPath);
// 1. 画直线
// int startX =5, startY = 100;
// int stopX = 195, stopY = 100;
// canvas.drawLine(startX, startY, stopX, stopY, mPaint);
// 2. 画圆
// int cx = 100, cy = 100;
// int radius = 80;
// canvas.drawCircle(cx, cy, radius, mPaint);
// 3. 画空心圆
// 4. 画图片
// canvas.drawBitmap(mBitmap, 0, 0, mPaint);
// 5. 画三角形
// canvas.drawPath(mPath, mPaint);
//7.画扇形
int startAngle = -90; //开始的角度
int sweepAngle = 45; //扫过的角度
boolean useCenter = false;//是否画出扇形的两边
canvas.drawArc(mOval, startAngle, sweepAngle, useCenter, mPaint);
}
}
View的重新绘制
- invalidate(); //触发View的重新绘制 onDraw
- postInvalidate(); //请求在主线程重新绘制控件 onDraw
ViewGroup的绘制流程
- ViewGroup继承View,绘制流程跟View是一致
ViewGroup的测量
- 相同点:measure -> onMeasure
- 不同点:ViewGroup需要在onMeasure去测量孩子
- 自定义ViewGroup一定要重写onMeasure方法,如果不重写则子View获取不到宽和高。重写是在onMeasure方法中调用measureChildern()方法,遍历出所有子View并对其进行测量。
ViewGroup的布局
- 相同点:layout (父容器调用) -》 onLayout
- 不同点:ViewGroup需要实现onLayout方法去布局孩子,调用孩子的layout方法,指定孩子上下左右的位置
- requestLayout();//请求重新布局 onLayout
ViewGroup的绘制
- 相同点:draw -> onDraw
- 不同点:ViewGroup一般不绘制自己,ViewGroup默认实现dispatchDraw去绘制孩子