android中如果要绘制2d图形需要有四个基本组件:
1,Bitmap :相当于画布。基于它之上画图。还可以是:Rect、RectF、text。
2,Canvas:相当于画家。在画布上管理绘制过程,提供绘图方法。
3,Paint:相当于画笔,可以设置画笔的颜色,粗细,类型等。
4,Drawable:包含绘制要素。,如形状,路径,文本,图像等。用于展示图像。
可以绘制图像的对象有三个:
1,自定义View,重写onDraw()方法,获取到其中的Canvas实例。
public class AddView extends View {
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//使用canvas绘图
}
}
2,定义一个Bitmap对象绘图。new一个Cancas对象,并传入bitmap实例。
Bitmap bitmap=Bitmap.createBitmap(100,100, Bitmap.Config.ARGB_8888);
Canvas canvas=new Canvas(bitmap);
3,使用SuefaceView对象绘制动态图像。
//SurfaceView surfaceView=new SurfaceView(getContext());
//surfaceView是SurfaceView已存在的对象
SurfaceHolder surfaceHolder= surfaceView.getHolder();
Canvas canvas=surfaceHolder.lockCanvas();//获得canvas对象
//使用canvas绘图
//……
surfaceHolder.unlockCanvasAndPost(canvas);//释放canvas
总结:
不管是哪一种对象都是获得其中的Canvas(画家)对象。
那么,2d绘图中需要注意的有哪些呢?通过我自己亲身试险和同行朋友的经验总结了以下几个方面:
1,自定义view继承的对象不光view
当我们需要画一个图的时候,一般情况下会继承view,然后重写onDraw()方法。
但是有些时候,我们并不能继承这个没有任何特征的view。有时候我们需要继承layout或者textview等等。
那么这个时候问题就来了。例如,我们如果要在grouplayout上画图,就有一个问题。grouplayout的视图属性是依赖于子view。
也就是说,我给grouplayout设置了背景色,其实是没有作用的,而依赖于它之上的子view设置的背景色才被用户所看到。
所以,如果我们要在grouplayout上画一个图,那么就算画出来了它也是不会给你显示的。
那么这个时候我们应该怎么办呢?
这里要提到一个属性:
//设置不重绘为false
setWillNotDraw(false);
只要你的自定义view继承view或者以view为父类的控件,都是可以设置这条属性的。
顾名思义,这条属性的意思就是视图不重新绘制。
android系统默认的是true。也就是默认不重新绘制,这导致了如果你要在类似grouplayout这样的视图上绘图的时候,它是不会重新绘制的,也就是显示不出来。
所以我们要设置这么一条属性,告诉系统,这个view需要重新绘制。
2,只有onDraw需要重写吗?
如小标题,答案是no!
继承了view后重写onDraw会得到Canvas对象,用于重绘。但是问题是重绘的一切位置依据是根据坐标来的。你如何获得应该在哪里重绘的依据呢?
不要告诉我说默认坐标(0,0)。这明显是不符合要求的,也满足不了要求的。
也不要告诉我根据getWidth()和getHeight()来获得,如果xml中配置的是自适应高度或者根据父类显示怎么办呢?而且,最重要的是在view还没有显示出来的时候,它们的值是null,而我们需要根据它们的值来绘图。所以不可能先出图再获得值。
那么,还要重写第二个view中的方法:protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
这个方法会返回给你一个宽高的测量规范。不是宽高。
那么它有什么用呢?它怎么获得控件的宽高或者父类的宽高呢?
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
在onMeasure方法中做以上代码的处理,就可以得到宽度。同理,高度也一样。
不过当在xml中设置根据内容显示的时候(wrap_content),该值是无效的,也就是说当自适应宽高的时候,获取不到有效的宽高值。
这时候应该怎么办呢?
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int width;
if (widthMode == MeasureSpec.EXACTLY) {
// MeasureSpec.EXACTLY 的意思是明确的。
width = widthSize;
} else {
width = 60;//默认值
}
通过MeasureSpec.getMode来获取当前view在xml中设置的值是否是有效的。
如果该值有效,说明xml中设置了明确的大小,或者设置了根据父类设置宽高(match_parent)。
如果该值无效则我们给一个默认值。
获得了view的宽高,绘图就有可靠的依据了。
另外,在这个方法中,获得到了宽度或者高度,如果绘制的图是正方形的话可以用:
//这样做是因为加号宽高是相等的,手动设置宽高
setMeasuredDimension(width, width);
这个属性来将宽高设置成一样。达到正方形的目的。
3,重载onMeasure()方法后抛出异常了
当我们重写onMeasure()方法后发现报了这样一个错误:
java.lang.IllegalStateException: onMeasure() did not set the measured dimension by calling setMeasuredDimension()
暴力的翻译过来就是告诉你没有调用setMeasuredDimension()方法。
所以:
只要重载onMeasure()方法就必须调用setMeasuredDimension()方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
//MeasureSpec.EXACTLY表示该view设置的确切的数值
width = widthSize;
height = heightSize;
}
setMeasuredDimension(width, height);
//处理逻辑
}
4,自定义view的坐标是四个象限吗?
先来看个图:
如果刚刚接触2d绘图的程序员会天真的以为android屏幕内的坐标系还和我们数学课本上的一样,那你就错了。
正确的坐标系是这样的:
没错,android屏幕上的坐标系就只这样的,以左上角为原点(0,0),向右向下正数无线增大。
也就是说,android坐标系没有负象限。如果你将绘图的坐标强行设置成负数它只会跑出父类去。
弄清了坐标系,那么计算位置啥的都比较好说了。
4,自定义view依附于屏幕吗?
自定义view的宽高起始点和被控制的范围大小很多不理解的程序员以为是以屏幕为依据的。例如,view的宽设置成了match_parent,这个时候理论上来说,它的宽度的确是屏幕的宽度。
但是如果,这个view外面有一个父布局,那么它的宽只能和父类一样打。(这tm废话)需要注意的是,这时候,自定义view的移动就要以父类作为基准了,计算高度或者获取坐标都要以父类的左上角为起点来计算。
就算你在自定义view中看到的绘图室全屏幕显示的,但是只要依赖了非屏幕的父布局,它就不再是全屏幕显示的了。
所有以父类为依据的数值都要做相应的改变了。
通俗点说,就是在自定义view中绘图的时候,我们不能莽撞的去获取屏幕的宽高来计算,而是以获取父布局的宽高来作为依据。