新手学自定义View系列(一)之canvas绘制API

简述:

  1. Android中自定义View开发的遇到的问题?

Android开发中自定义View可以说是常见的技术,不管是新手还是老司机都非常了解。但是目前Android自定义View的现状是很多人对自定义View都能说得出来一些,但是实际上根据需求开发起来却比较难。老实说这也是我之前的想法,我也一直在思考这个原因,个人觉得原因是:
第一 对自定义View中的API不熟悉;
第二 对自定义View没有一个系统认识和深入了解;
第三 对掌握的自定义View的掌握没有一个分类管理思想;

  1. canvas绘制的API的细节深入的学习能解决什么问题?
    此篇博客canvas的API的学习可以系统掌握canvas.drawxxx()系列的方法
    以及绘制原理。

  2. 本系列博客能给你带来什么?
    个人认为Android中的自定义View API的学习主要分为几个方面:
    第一 Canvas绘制系列的API的学习;
    第二 Paint 画笔系列的API的学习;
    第三 辅助绘制(clipxxx系列方法)canvas的几何变换(位移、旋转、错切)API的学习;
    第四 自定义View的测量、绘制(onMeasure、onSizeChange、onDraw);
    第五 自定义ViewGroup的测量、布局、绘制(onMeasure、onSizeChange、onLayout、onDraw);
    第六 事件分发、事件拦截、滑动冲突以及触摸反馈的接口的回调设计;


1、canvas绘制颜色

一般用于在绘制之前设置底色,或者在绘制之后为界面设置半透明蒙版
canvas中绘制颜色主要几种方法:

  • canvas.drawColor(int color)
  • canvas.drawRGB(int r, int g, int b)
  • canvas.drawARGB(int a, int r, int g, int b)
canvas.drawColor(Color.parseColor("#00bfa5"));//int color
canvas.drawRGB(100, 200, 100);//设置red值(0~255),green值(0~255),blue值(0~255)
canvas.drawARGB(100, 100, 200, 100);//设置alpha值(0~255),设置red值(0~255),green值(0~255),blue值(0~255)

2、canvas绘制形状

  • 绘制圆形:
  • canvas.drawCircle(float dx, float dy, float radius, Paint paint)
mPaint.setStyle(Paint.Style.FILL);//设置填充style为FILL
mPaint.setStyle(Paint.Style.STROKE);//设置填充style为STROKE
canvas.drawCircle(mWidth / 2, mHeight / 2, 200, mPaint);//(dx:圆心横坐标 dy:圆心纵坐标 radius:半径 paint 画笔) 

  • 绘制矩形:
    canvas中绘制矩形主要几种方法:
    Rect和RectF的细微区别是Rect的绘制的单位类型是int,而RectF的绘制单位类型是float
  • canvas.drawRect(int left, int top, int right, int bottom, Paint paint);
  • canvas.drawRect(Rect rect, Paint paint);
  • canvas.drawRect(RectF rectF, Paint paint);
mPaint.setStyle(Paint.Style.FILL);//设置填充style为FILL
mPaint.setStyle(Paint.Style.STROKE);//设置填充style为STROKE
mPaint.setColor(Color.parseColor("#e91e63"));
canvas.drawRect(mWidth / 2 - 200, 200, mWidth / 2 + 200, 600, mPaint);//left:矩形的左边离Y轴的距离;top:矩形的顶边离X轴的距离;right的右边离Y轴的距离;bottom:矩形的底部边离X轴的距离

  • 绘制圆角矩形:
    canvas绘制圆角矩形主要有两个方法:
    但是这两个方法是等价的,绘制出来的效果都是一样的。
  • drawRoundRect(RectF rect, float rx, float ry, Paint paint)
  • drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint)
int rx = 80;//圆角矩形的圆角所处的椭圆的横向半径
int ry = 40;//圆角矩形的圆角所处的椭圆的纵向半径
int left = mWidth / 2 - 200;//left:矩形的左边离Y轴的距离;
int top = 200;//top:矩形的顶边离X轴的距离;
int right = mWidth / 2 + 200;//right的右边离Y轴的距离;
int bottom = 400;//bottom:矩形的底部边离X轴的距离

mPaint.setStyle(Paint.Style.FILL);//设置填充style为FILL
mPaint.setStyle(Paint.Style.STROKE);//设置填充style为STROKE
mPaint.setStrokeWidth(5f);//设置stroke线形的宽度
mPaint.setColor(Color.parseColor("#e91e63"));

canvas.drawRoundRect(left, top, right, bottom, rx, ry, mPaint);//绘制圆角矩形

提出问题:圆角矩形绘制的原理是怎样的?为什么会有rx,ry两个半径?

圆角矩形的四个圆角实际上对应着四个椭圆弧的一部分,注意是椭圆弧不是正圆弧。那么rx也就是椭圆弧的横向半径,ry是椭圆弧的纵向半径,正好对应着椭圆的短半径和长半径。

这里写图片描述

(图片来源于GcsSloop大神,地址:http://ww3.sinaimg.cn/large/005Xtdi2jw1f2748fjw2bj308c0dwmx8.jpg)
结论:

当rx等于ry时,此时的四个椭圆弧也就变成四个正圆弧,那么每个圆角即为正圆弧的1/4;

当rx等于矩形宽度的一半,ry等于矩形高度的一半时,此时四个圆角所处的椭圆的重合,正好为矩形的内接椭圆;

当rx大于矩形宽度的一半,ry大于矩形高度的一半时,此时四个圆角所处椭圆实际上是无法计算出圆弧的,所以drawRoundRect对大于该数值的参数进行了修改,如果大于rx大于矩形宽度一半,ry大于矩形高度一半的参数均按照一半来处理。

结论论证:
可以针对圆角矩形绘制四个半径rx,ry椭圆来验证,若椭圆中有一部分圆弧与之重合,正好就证实我们的原理。

int rx = 80;//圆角矩形的圆角所处的椭圆的横向半径
int ry = 40;//圆角矩形的圆角所处的椭圆的纵向半径
int left = mWidth / 2 - 200;//left:矩形的左边离Y轴的距离;
int top = 200;//top:矩形的顶边离X轴的距离;
int right = mWidth / 2 + 200;//right的右边离Y轴的距离;
int bottom = 400;//bottom:矩形的底部边离X轴的距离

mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5f);
mPaint.setColor(Color.parseColor("#e91e63"));

canvas.drawRoundRect(left, top, right, bottom, rx, ry, mPaint);//绘制圆角矩形

canvas.drawOval(left, top, left + 2 * rx, top + 2 * ry, mPaint);//绘制左上角圆角的所处的椭圆
canvas.drawOval(right - 2 * rx, top, right, top + 2 * ry, mPaint);//绘制右上角圆角的所处的椭圆
canvas.drawOval(left, bottom - 2 * ry, left + 2 * rx, bottom, mPaint);//绘制左下角圆角的所处的椭圆
canvas.drawOval(right - 2 * rx, bottom - 2 * ry, right, bottom, mPaint);//绘制右下角圆角的所处的椭圆

当rx等于ry时,此时的四个椭圆弧也就变成四个正圆弧,那么每个圆角即为正圆弧的1/4;

当rx接近矩形宽度的一半,ry接近矩形高度的一半时,此时四个圆角所处的椭圆接近重合

int rx = 195;//矩形宽度的一半为200
int ry = 95;//矩形高度的一半为100
int left = mWidth / 2 - 200;//left:矩形的左边离Y轴的距离;
int top = 200;//top:矩形的顶边离X轴的距离;
int right = mWidth / 2 + 200;//right的右边离Y轴的距离;
int bottom = 400;//bottom:矩形的底部边离X轴的距离

mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5f);
mPaint.setColor(Color.parseColor("#e91e63"));

canvas.drawRoundRect(left, top, right, bottom, rx, ry, mPaint);//绘制圆角矩形

canvas.drawOval(left, top, left + 2 * rx, top + 2 * ry, mPaint);//绘制左上角圆角的所处的椭圆
canvas.drawOval(right - 2 * rx, top, right, top + 2 * ry, mPaint);//绘制右上角圆角的所处的椭圆
canvas.drawOval(left, bottom - 2 * ry, left + 2 * rx, bottom, mPaint);//绘制左下角圆角的所处的椭圆
canvas.drawOval(right - 2 * rx, bottom - 2 * ry, right, bottom, mPaint);//绘制右下角圆角的所处的椭圆
image.png

当rx等于矩形宽度的一半,ry等于矩形高度的一半时,此时四个圆角所处的椭圆的重合,正好为矩形的内接椭圆;

image.png

当rx大于矩形宽度的一半,ry大于矩形高度的一半时,此时四个圆角所处椭圆实际上是无法计算出圆弧的,所以drawRoundRect对大于该数值的参数进行了修改,如果大于rx大于矩形宽度一半,ry大于矩形高度一半的参数均按照一半来处理。(为了便于区别把椭圆的颜色绘制为grey)

image.png
  • 绘制点:
    canvas绘制点主要有几种方法:
  • canvas.drawPoint(float x, float y, Paint paint)//绘制单个点
  • canvas.drawPoints(float[] pts, int offset, int count, Paint paint)//有选择性绘制多个点
  • canvas.drawPoints(float[] pts, Paint paint)//绘制多个点
mPaint.setColor(Color.parseColor("#dd2c00"));
mPaint.setStrokeWidth(80f);
mPaint.setStrokeCap(Paint.Cap.ROUND);//绘制点Point,canvas.drawPoint默认绘制方形点,设置画笔的setStrokeCap可以绘制圆点
canvas.drawPoint(mWidth / 2, mHeight / 2, mPaint);


//绘制多个点:drawPoints(float[] opts, Paint paint)
//opts 点的一对(x,y)坐标的数组。(相邻的两个数分为一个点的x,y)

mPaint.setColor(Color.parseColor("#304ffe"));
float[] points = {mWidth / 2 - 200, 200, mWidth / 2 + 200, 200, mWidth / 2 - 200, 600, mWidth / 2 + 200, 600};
canvas.drawPoints(points, mPaint);

//有选择绘制多个点: drawPoints(float[] opts, offset, count, Paint paint)
//opts 点的一对(x,y)坐标的数组。(相邻的两个数分为一个点的x,y)
//offset:是相对于对points数组的元素下标做偏移
//count: 设置points数组从offset偏移算起元素的个数
//经过offset和count操作产生新的点的数组

mPaint.setColor(Color.RED);
canvas.drawPoints(points, 4, 4, mPaint);//在前两个点后画两个点
image.png
  • 绘制椭圆:
    canvas绘制椭圆主要有两种方法:
    但是这两个方法是等价的,绘制出来的效果都是一样的。
  • drawOval(RectF oval, Paint paint)
  • drawOval(float left, float top, float right, float bottom, Paint paint)
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.parseColor("#FF9800"));

canvas.drawOval(mWidth / 2 - 400, 200, mWidth / 2 + 400, 600, mPaint);//外接矩形的left,top,right,bottom
image.png
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5f);
mPaint.setColor(Color.parseColor("#0091EA"));
RectF rectF = new RectF(mWidth / 2 - 200, 0, mWidth / 2 + 200, 800);//外接矩形的left,top,right,bottom

canvas.drawOval(rectF, mPaint);
image.png
  • 绘制线段:
    canvas绘制线段主要有几种方法:
  • drawLine(float startX, float startY, float stopX, float stopY, Paint paint)
  • drawLines(float[] pts, int offset, int count, Paint paint)
  • drawLines(float[] pts, Paint paint)
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5f);
mPaint.setColor(Color.GREEN);

canvas.drawLine(mWidth / 2 - 200, 200, mWidth / 2 + 200, 600, mPaint);//startX线的起点X坐标,startY线的起点Y坐标,endX线的终点X坐标,endY线的终点Y坐标
image.png
float[] points2 = {
                mWidth / 2 - 200, 200,
                mWidth / 2 + 200, 600,
                mWidth / 2 + 200, 200,
                mWidth / 2 - 200, 600,
                mWidth / 2, 200,
                mWidth / 2, 600,
                mWidth / 2 - 200, 400,
                mWidth / 2 + 200, 400
        };//points2 点的一对(startX,startY,endX,endY)坐标的数组。(相邻的四个数为一条线的起点和终点的x,y坐标)
        
canvas.drawLines(points2, mPaint);//绘制多条线段
image.png
float[] points2 = {
                mWidth / 2 - 200, 200,
                mWidth / 2 + 200, 600,
                mWidth / 2 + 200, 200,
                mWidth / 2 - 200, 600,
                mWidth / 2, 200,
                mWidth / 2, 600,
                mWidth / 2 - 200, 400,
                mWidth / 2 + 200, 400
        };//points2 点的一对(startX,startY,endX,endY)坐标的数组。(相邻的四个数为一条线的起点和终点的x,y坐标)

//offset:原理和drawPonits一样,是相对于对points2数组的元素下标做偏移
//count: 设置points2数组从offset偏移算起元素的个数
//经过offset和count产生新的点的数组

canvas.drawLines(points2, 4, 8, mPaint);
image.png
  • 绘制弧形或者扇形(是针对所在椭圆来绘制弧形和扇形):
    canvas绘制弧形或者扇形(是针对所在椭圆来绘制弧形和扇形)主要有两种方法:但是这两个方法是等价的,绘制出来的效果都是一样的。
  • drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
  • drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
//canvas.drawArc(left,top,right,bottom,startAngle,sweepAngle,userCenter,paint)
//left,top,right,bottom: 固定出椭圆
//startAngle起始角度(0度为圆心横坐标向左,正数为顺时针,负数为逆时针)
//sweepAngle扫过的角度
//userCenter是否连接圆心(true:连接圆心,画扇形;false:不连接圆心,画弧形)

mPaint.setStyle(Paint.Style.STROKE);//设置椭圆填充style为STROKE
RectF rectF1 = new RectF(mWidth / 2 - 400, 200, mWidth / 2 + 400, 600);

mPaint.setStyle(Paint.Style.FILL);//设置扇形填充style为FILL
mPaint.setStyle(Paint.Style.STROKE);//设置扇形填充style为STROKE
canvas.drawOval(rectF1,mPaint);//绘制扇形所处的椭圆
canvas.drawArc(rectF1, 0, 120, true, mPaint);//扇形
image.png
image.png
//canvas.drawArc(left,top,right,bottom,startAngle,sweepAngle,userCenter,paint)
//left,top,right,bottom: 固定出椭圆
//startAngle起始角度(0度为圆心横坐标向左,正数为顺时针,负数为逆时针)
//sweepAngle扫过的角度
//userCenter是否连接圆心(true:连接圆心,画扇形;false:不连接圆心,画弧形)

mPaint.setStyle(Paint.Style.STROKE);//设置椭圆填充style为STROKE
RectF rectF1 = new RectF(mWidth / 2 - 400, 200, mWidth / 2 + 400, 600);

mPaint.setStyle(Paint.Style.FILL);//设置扇形填充style为FILL
mPaint.setStyle(Paint.Style.STROKE);//设置弧形填充style为STROKE
canvas.drawOval(rectF1,mPaint);//绘制弧形所处的椭圆(注意:绘制STROKE的弧形的时候不要绘制椭圆,否则会重合不便于观察结果)
canvas.drawArc(rectF1, 0, 120, false, mPaint);//弧形
image.png
image.png
  • 绘制图片Bitmap:
    canvas图片Bitmap主要有几种方法:
  • drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
  • drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)
  • drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)
canvas.translate(mWidth / 2, mHeight / 2);//移动坐标原点到View组件的正中间位置,这个操作会很方便我们绘制时算坐标。(涉及到canvas的几何变换后期会说到)
float[] points3 = {-mWidth / 2, 0, mWidth / 2, 0, 0, -mHeight / 2, 0, mHeight / 2};
canvas.drawLines(points3, mPaint);//绘制出坐标系线
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon);
canvas.drawBitmap(bitmap, -bitmap.getWidth() / 2, -bitmap.getHeight() / 2, mPaint);//图片左上角x,y坐标对应left,top
image.png
  • 绘制文字:
    canvas中涉及到文本的绘制的细节有很多,后期会专门说下关于文本的绘制,这里先了解下。
  • canvas.drawText(char[] text, int index, int count, float x, float y, Paint paint)
  • canvas.drawText(String text, float x, float y, Paint paint)
  • canvas.drawText(String text, int start, int end, float x, float y, Paint paint)
  • canvas.drawText(CharSequence text, int start, int end, float x, float y, Paint paint)
canvas.translate(mWidth / 2, mHeight / 2);//移动坐标原点到View组件的正中间位置
mPaint.setTextSize(50);
mPaint.setColor(Color.GREEN);
mPaint.setFakeBoldText(true);
String text = "今天好像是情人节,单身狗还是撸撸代码吧";
mPaint.getTextBounds(text, 0, text.length(), mTextBound);//通过传入mTextBound(Rect对象),将文字的尺寸固定住,作用相当于测量文本尺寸。

canvas.drawText(text, -mTextBound.width() / 2, mTextBound.height() / 2, mPaint);//x,y指的是文本绘制起点坐标,注意文本的绘制起点是第一个字左下角还要向外偏移一点,至于为什么是这样后期会说到.
image.png
  • 绘制Path类型自定义图型:
    Path的知识在Canvas中绘制很重要,合理使用Path可以做出很多炫酷的组件以及提高绘制的效率。所以这里先给出个例子,下一期博客将会深入讲解canvas绘制中的Path。情人节了,这张图或许适合你,哈哈哈
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.RED);
mPath.arcTo(200, 200, 400, 400, -225, 225, true);
mPath.arcTo(400, 200, 600, 400, -180, 225, false);
mPath.lineTo(400, 542);

canvas.drawPath(mPath, mPaint);
image.png

结束:
到这里,这期博客就结束了,该博客旨在新手学习沉淀。感谢GcsSloop和扔物线大神对自定义View的博客的无私奉献。本系列博客均为自己学大神们自定义View总结。

小例子:本来是扔物线大神第一期的直方图的例子,在其基础上扩充几点:第一就是点击每个直方图会有监听回调的触摸返回,并把当前点击的直方图数据信息显示出来;第二就是在直方图基础上绘制了折线图。
抛出一个思考的问题:对于一个自定义View如何给某个绘制区域设置监听的点击事件触摸回调?

image.png
image.png
image.png

例子源码下篇博客给出。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容