一、Canvas绘制图形的直接对象
1、两种绘制环境:
canvas主要是区别:(1)、使用普通View的canvas画图,
(2)、使用专门的SurfaceView的canvas来画图。
2.canvas四个常用方法:第一种适合处理量比较小,帧率比较小的动画,比如说象棋游戏之类的;
第二种主要用在游戏,高品质动画方面的画图。
因为SurfaceView中定义一个专门的线程来完成画图工作,应用程序不需要等待View的刷图,提高了性能。
View一般用于绘制静态页面或者界面元素跟随用户的操作(点击、拖拽等)而被动的改变位置、大小等
SurfaceView一般用于无需用户操作,界面元素就需要不断的刷新的情况(例如打飞机游戏不断移动的背景)
canvas.save(); 保存画布,将之前绘制的图像全部保存起来,后续的操作好像就在一个新图层一样
canvas.restore(); 合并图层,将save之前和save之后的图像合并起来
canvas.translate(x,y); 画布平移,,默认绘图坐标在零点,平移之后,原点由(0,0)移到(x,y),(x,y)为新原点
canvas.rotate(); 画布旋转
3.Canvas的常用的绘制方法:
(1)drawPoint(float x, float y, Paint paint)
画点,
x: 水平x轴坐标,
y: 垂直y轴坐标,
paint: Paint对象。
(2)drawLine(float startX, float startY, float stopX, float stopY, Paint paint)
画线,
startX: 起始点的x轴坐标位置,
startY: 始点的y轴坐标位置,
stopX: 终点的x轴水平坐标位置,
stopY: y轴垂直坐标位置,
paint: Paint 画刷对象。
(3)drawRect(RectF rect, Paint paint)
绘制区域,
rect: RectF为一个区域
(4)drawPath(Path path, Paint paint)
绘制一个路径,
path: 为Path路径对象
(5)drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)
贴图,
bitmap: 常规的Bitmap对象,
src: 源区域(这里是bitmap),
dst: 目标区域(应该在canvas的位置和大小),
paint: 画刷对象,
因为用到了缩放和拉伸的可能,当原始Rect不等于目标Rect时性能将会有大幅损失。
(6)drawText(String text, float x, float y, Paint paint)
渲染文本,
text:String类型的文本,
x: 参数二x轴坐标,
y: 参数三y轴坐标,
paint: 是Paint对象。
(7)drawOval(RectF oval, Paint paint)
画椭圆,
oval:参数一是扫描区域,参数二为paint对象;
(8)drawCircle(float cx, float cy, float radius,Paint paint)
绘制圆,
cx: 圆心的x坐标,
cy: 圆心的y轴坐标,
radius: 是半径
(9)drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
画弧,
oval: 是RectF对象,一个区域界限用于定义在形状、大小、圆弧,
startAngle: 起始角(度)在圆弧的开始位置,
sweepAngle: 描角(度)开始顺时针测量的,
useCenter: 如果这是真的话,将是一个扇形,如果它是假这将是一个弧线,
起始角度不理解的,请看下图
二、简单的画笔Paint
Paint 代表了Canvas上的画笔、画刷、颜料等等;
通过上面讲解我们画一个简单的钟表,加深理解
钟表分为四部分:外层大圆盘,四个长时间刻度线,短时间刻度线,两个指针
说白了也是一个自定义View
1、新建java类继承View,并添加构造方法
2、重写onMeasure,确定View大小,也可以跳过此步骤,此时自定义View会充满父布局
//第一步,我们需要知道绘制图形的大小,这个过程在OnMeasure中进行,如果不重写OnMeasure方法,自定义的View会默认充满父布局 //首先重写onMeasure,按住ctrl点击super.onMeasure查看,发现其实是调用的setMeasuredDimension(mWidth,mHeihgt);方法,此方法将测量的宽高穿进去从而完成测量工作 //所以重写onMeasure方法,就是把参数传给setMeasuredDimension //对宽高重新进行定义 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //我们调用自己的自定义方法measureSize对宽高重新定义,参数宽和高是MeasureSpec对象 //MeasureSpec对象有两个常用方法 //MeasureSpec.getMode(measureSpec) 得到测量模式 //MeasureSpec.getSize(measureSpec) 得到测量大小 mWidth = measureSize(widthMeasureSpec); mHeihgt = measureSize(heightMeasureSpec); setMeasuredDimension(mWidth,mHeihgt); //初始化图形 //initView(); } //我们通过测量模式,给出不同的测量值 //当specMode = EXACTLY时,直接指定specSize即可 //当specMode != EXACTLY时,需要指出默认大小 //当specMode = AT_MOST时,即指定了wrap_content属性时,需要取出我们指定大小和specSize中最小的一个为最后测量值 private int measureSize(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if (specMode == MeasureSpec.EXACTLY){ result = specSize; }else { result = 180; //指定默认大小 if (specMode == MeasureSpec.AT_MOST){ result = Math.min(result,specSize); } } return result; }
3.重写onDraw方法
(1)外层大圆
首先设置大圆的的画布属性
circlePaint = new Paint(); circlePaint.setStyle(Paint.Style.STROKE); circlePaint.setAntiAlias(true); circlePaint.setStrokeWidth(5); circlePaint.setColor(ContextCompat.getColor(this.getContext(), R.color.colorAccent));
画出大圆
radius = Math.min(mWidth,mHeihgt) / 2; radius = radius * 0.8f; //让大圆占父布局的4/5 canvas.drawCircle(mWidth/2,mHeihgt/2,radius,circlePaint);
(2)刻度线
通过drawLine方法设置起始和结束端点就可以了。
不过要注意有一部分刻度是斜着的,这样就难以计算坐标了,通过三角函数计算工作量大。
所以使用使用相对的概念,我们将画布以圆心为坐标原点,每画好一条线段,就把画布旋转一定角度,当当画布重新旋转到原点时,所有刻度就已经画好了,这样就避免了三角函数的计算
初始化刻度线画笔:
//刻度线 degreePaint = new Paint(); degreePaint.setStyle(Paint.Style.STROKE); degreePaint.setAntiAlias(true); degreePaint.setStrokeWidth(3); degreePaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.white));
绘制刻度线
//刻度线 //初始刻度12点的索引为0 //之后中间空四个小刻度,然后是一个大刻度,即每加5是一个大刻度 //所以5的0和5的倍数是一个大刻度,其他为小刻度 //每格的度数 int rotateAngel = 360 / 60; int txt; String time; for (int i = 0; i < 60; i++){ if (i / 5 == 0){ txt = 12; }else { txt = i / 5; } time = String.valueOf(txt); if (i % 5 == 0){ //大刻度 degreePaint.setStrokeWidth(5); degreePaint.setTextSize(30); canvas.drawLine( mWidth / 2,mHeihgt / 2-radius, mWidth / 2,mHeihgt / 2-radius+30, degreePaint ); canvas.drawText( time, mWidth / 2 - degreePaint.measureText(time) / 2, mHeihgt / 2-radius+60, degreePaint ); }else { //小刻度 degreePaint.setStrokeWidth(3); degreePaint.setTextSize(15); canvas.drawLine( mWidth / 2,mHeihgt / 2-radius, mWidth / 2,mHeihgt / 2-radius+15, degreePaint ); } /** * 参数 * degrees 旋转的角度 * (x,y)相对旋转点坐标 * px x坐标位置 * py y坐标位置 */ canvas.rotate(rotateAngel,mWidth/2,mHeihgt/2); }//for
如果需要把文字放正,还需要计算三角函数,此处略过
(2)时针/分针/秒针
注意三个指针的长度宽度均不一样
初始化指针画笔:
//指针 fingerPaint = new Paint(); fingerPaint.setStyle(Paint.Style.STROKE); fingerPaint.setAntiAlias(true);
绘制指针:
//时针 canvas.translate(mWidth/2,mHeihgt/2); fingerPaint.setStrokeWidth(15); fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.black)); canvas.drawLine(0,0,0,-mWidth / 6,fingerPaint); //分针 fingerPaint.setStrokeWidth(10); fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.holo_red_light)); canvas.drawLine(0,0,0,-mWidth / 5,fingerPaint); //秒针 fingerPaint.setStrokeWidth(5); fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.holo_blue_light)); canvas.drawLine(0,0,0,-mWidth / 4,fingerPaint);
//绘制一个圆心: canvas.drawCircle(0, 0, 5, degreePaint);
4.让钟表走起来:
View一般用于绘制静态页面或者界面元素跟随用户的操作(点击、拖拽等)而被动的改变位置、大小等
SurfaceView一般用于无需用户操作,界面元素就需要不断的刷新的情况(例如打飞机游戏不断移动的背景)
但并不是不能完成画面的更新,
只要在onDraw方法中调用this.invalidate();即可实时更新,因为invalidate会调用onDraw方法
此处我们使用View,之后在另一篇文章里我会用SurfaceView
要想和系统时间一样,首先要得到系统时间:
之后就通过算法,旋转对应的角度即可:如时针curTime.setToNow(); // 取得系统时间。 int hour = curTime.hour; // 0-23 int minute = curTime.minute; int second = curTime.second; //Log.e("Liang", "Time获取当前日期"+hour+":"+minute+":"+second );
canvas.rotate( hour % 12 / 12f * 360, 0, 0);
先对12取余是因为时间可能是24进制的,
最后时分秒指针画完后,只要在onDraw方法中调用this.invalidate()即可实时更新,因为invalidate会调用onDraw方法
时分针画法:
curTime.setToNow(); // 取得系统时间。 int hour = curTime.hour; // 0-23 int minute = curTime.minute + 1 ; //0-59 int second = curTime.second + 1; //0-59 //Log.e("Liang", "Time获取当前日期"+hour+":"+minute+":"+second ); float degress = 0; //画布旋转角度 //时针 canvas.rotate( -degress, 0, 0); //还原画布方向 degress = hour % 12 / 12f * 360 + minute / 60f * 30; //旋转的角度 canvas.rotate( degress , 0, 0); //旋转画布 fingerPaint.setStrokeWidth(15); fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.black)); //画指针,其中注意x轴正方向朝右,y轴正方向朝下 canvas.drawLine(0,0,0,-hourPointerLength,fingerPaint); //分针 canvas.rotate( -degress , 0, 0); //还原画布方向 degress = minute / 60f * 360 + second / 60f * 6 ; //旋转的角度 canvas.rotate( degress , 0, 0); //旋转画布 fingerPaint.setStrokeWidth(10); fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.holo_red_light)); //画指针,其中注意x轴正方向朝右,y轴正方向朝下 canvas.drawLine(0,0,0,-minutePointerLength,fingerPaint); //秒针 canvas.rotate( -degress, 0, 0); //还原画布方向 degress = second / 60f * 360; //旋转的角度 canvas.rotate( degress, 0, 0); //旋转画布 fingerPaint.setStrokeWidth(5); fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.holo_blue_light)); //画指针,其中注意x轴正方向朝右,y轴正方向朝下 canvas.drawLine(0,0,0,-secondPointerLength,fingerPaint); //绘制一个圆心: canvas.drawCircle(0, 0, 5, degreePaint); canvas.restore(); this.invalidate();//调用onDraw方法,这样就会一直更新onDraw
全部代码:
package myView; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.icu.util.Calendar; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.text.format.Time; import android.util.AttributeSet; import android.util.Log; import android.view.View; import com.liang.myview.R; /** * Created by Amarao on 2018/6/24. */ public class MyClock extends View { private int mWidth; private int mHeihgt; private Paint circlePaint,degreePaint,fingerPaint; private float radius; private Time curTime; //秒针长度 private float secondPointerLength; //分针长度 private float minutePointerLength; //时针长度 private float hourPointerLength; public MyClock(Context context) { super(context); } public MyClock(Context context, @Nullable AttributeSet attrs) { super(context, attrs); curTime = new Time(); } //第一步,我们需要知道绘制图形的大小,这个过程在OnMeasure中进行,如果不重写OnMeasure方法,自定义的View会默认充满父布局 //首先重写onMeasure,按住ctrl点击super.onMeasure查看,发现其实是调用的setMeasuredDimension(mWidth,mHeihgt);方法,此方法将测量的宽高穿进去从而完成测量工作 //所以重写onMeasure方法,就是把参数传给setMeasuredDimension //对宽高重新进行定义 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //我们调用自己的自定义方法measureSize对宽高重新定义,参数宽和高是MeasureSpec对象 //MeasureSpec对象有两个常用方法 //MeasureSpec.getMode(measureSpec) 得到测量模式 //MeasureSpec.getSize(measureSpec) 得到测量大小 mWidth = measureSize(widthMeasureSpec); mHeihgt = measureSize(heightMeasureSpec); setMeasuredDimension(mWidth,mHeihgt); //初始化图形 //initView(); } //我们通过测量模式,给出不同的测量值 //当specMode = EXACTLY时,直接指定specSize即可 //当specMode != EXACTLY时,需要指出默认大小 //当specMode = AT_MOST时,即指定了wrap_content属性时,需要取出我们指定大小和specSize中最小的一个为最后测量值 private int measureSize(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if (specMode == MeasureSpec.EXACTLY){ result = specSize; }else { result = 180; //指定默认大小 if (specMode == MeasureSpec.AT_MOST){ result = Math.min(result,specSize); } } return result; } private void initPaint(){ //外层大圆 circlePaint = new Paint(); circlePaint.setStyle(Paint.Style.STROKE); circlePaint.setAntiAlias(true); circlePaint.setStrokeWidth(5); circlePaint.setColor(ContextCompat.getColor(this.getContext(), R.color.colorAccent)); //刻度线 degreePaint = new Paint(); degreePaint.setStyle(Paint.Style.STROKE); degreePaint.setAntiAlias(true); degreePaint.setStrokeWidth(3); degreePaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.white)); //指针 fingerPaint = new Paint(); fingerPaint.setStyle(Paint.Style.STROKE); fingerPaint.setAntiAlias(true); } @Override protected void onDraw(Canvas canvas) { initPaint(); super.onDraw(canvas); //外层大圆 radius = Math.min(mWidth,mHeihgt) / 2; radius = radius * 0.8f; //让大圆占父布局的4/5 canvas.drawCircle(mWidth/2,mHeihgt/2,radius,circlePaint); //刻度线 //初始刻度12点的索引为0 //之后中间空四个小刻度,然后是一个大刻度,即每加5是一个大刻度 //所以5的0和5的倍数是一个大刻度,其他为小刻度 //每格的度数 int rotateAngel = 360 / 60; int txt; String time; for (int i = 0; i < 60; i++){ if (i / 5 == 0){ txt = 12; }else { txt = i / 5; } time = String.valueOf(txt); if (i % 5 == 0){ //大刻度 degreePaint.setStrokeWidth(5); degreePaint.setTextSize(30); canvas.drawLine( mWidth / 2,mHeihgt / 2-radius, mWidth / 2,mHeihgt / 2-radius+30, degreePaint ); degreePaint.setStrokeWidth(3); canvas.drawText( time, mWidth / 2 - degreePaint.measureText(time) / 2, mHeihgt / 2-radius+60, degreePaint ); }else { //小刻度 degreePaint.setStrokeWidth(3); degreePaint.setTextSize(15); canvas.drawLine( mWidth / 2,mHeihgt / 2-radius, mWidth / 2,mHeihgt / 2-radius+15, degreePaint ); } /** * 参数 * degrees 旋转的角度 * (x,y)相对旋转点坐标 * px x坐标位置 * py y坐标位置 */ canvas.rotate(rotateAngel,mWidth/2,mHeihgt/2); }//for //将坐标中心移到圆心 canvas.translate(mWidth/2,mHeihgt/2); hourPointerLength = mWidth * 0.3f; minutePointerLength = mWidth * 0.35f; secondPointerLength = mWidth * 0.4f; curTime.setToNow(); // 取得系统时间。 int hour = curTime.hour; // 0-23 int minute = curTime.minute ; //0-59 int second = curTime.second ; //0-59 //Log.e("Liang", "Time获取当前日期"+hour+":"+minute+":"+second ); float degress = 0; //画布旋转角度 //时针 canvas.rotate( -degress, 0, 0); //还原画布方向 degress = hour % 12 / 12f * 360 + minute / 60f * 30; //旋转的角度 canvas.rotate( degress , 0, 0); //旋转画布 fingerPaint.setStrokeWidth(15); fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.black)); //画指针,其中注意x轴正方向朝右,y轴正方向朝下 canvas.drawLine(0,0,0,-hourPointerLength,fingerPaint); //分针 canvas.rotate( -degress , 0, 0); //还原画布方向 degress = minute / 60f * 360 + second / 60f * 6 ; //旋转的角度 canvas.rotate( degress , 0, 0); //旋转画布 fingerPaint.setStrokeWidth(10); fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.holo_red_light)); //画指针,其中注意x轴正方向朝右,y轴正方向朝下 canvas.drawLine(0,0,0,-minutePointerLength,fingerPaint); //秒针 canvas.rotate( -degress, 0, 0); //还原画布方向 degress = second / 60f * 360; //旋转的角度 canvas.rotate( degress, 0, 0); //旋转画布 fingerPaint.setStrokeWidth(5); fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.holo_blue_light)); //画指针,其中注意x轴正方向朝右,y轴正方向朝下 canvas.drawLine(0,0,0,-secondPointerLength,fingerPaint); //绘制一个圆心: canvas.drawCircle(0, 0, 5, degreePaint); canvas.restore(); this.invalidate();//调用onDraw方法,这样就会一直更新onDraw }//onDraw private void runClock(){ } }