学习往往是枯燥的,如果能用一个有趣 Demo 来学习和练习技术,那对知识的掌握就会更牢固。我在学习 Canvas 绘制 API 的时候就是这样做的。

截图镇楼

效果图

我觉得这个绘制小黄人的自定义 View 就很有意思,也为我后来工作中的自定义 View 实现打下了良好的基础。虽然这是 4 年半以前写的文章,但是大部分关注我们的同学应该没看过,今天咱们一起来拷古翻新一下代码(程序员的事,怎么能叫炒冷饭呢,这明明是温故而知新)。以后有机会还会分享项目实用自定义 View,敬请关注。

实现步骤

其实很简单

  1. 首先找到一张小黄人的图
  2. 然后调用 canvas.drawBitmap() 后画到画布上 好吧,一点都不好笑  - -。

正文 ↓

准备工作

自定义MinionView extends View,定义以下成员变量,备用(可以先不看,后面的代码看到莫名其妙出来的变量再上来看下)

private float bodyWidth;private float bodyHeight;private static final float BODY_SCALE = 0.6f; // 身体主干占整个view的比重private static final float BODY_WIDTH_HEIGHT_SCALE = 0.6f; // 身体的比例设定为 w:h = 3:5

private float mStrokeWidth = 4; // 描边宽度private float offset; // 计算时,部分需要 考虑描边偏移private float radius; // 身体上下半圆的半径private int colorClothes = Color.rgb(32, 116, 160); // 衣服的颜色private int colorBody = Color.rgb(249, 217, 70); // 身体的颜色private int colorStroke = Color.BLACK;private RectF bodyRect = new RectF();private float handsHeight;// 计算出吊带的高度时,可以用来做手的高度private float footHeight; // 脚的高度,用来画脚部阴影时用

初始化参数

重写 onSizeChanged 方法,尺寸变化时初始化一下绘制的参数(会经常看到一些奇怪的数字,用做比例换算,别问我怎么来的,目测 + 一点点微调得来的- -。)

private void initParams() {    bodyWidth = Math.min(getWidth(), getHeight() * BODY_WIDTH_HEIGHT_SCALE) * BODY_SCALE;    bodyHeight = Math.min(getWidth(), getHeight() * BODY_WIDTH_HEIGHT_SCALE) / BODY_WIDTH_HEIGHT_SCALE * BODY_SCALE;

    mStrokeWidth = Math.max(bodyWidth / 50, mStrokeWidth);    offset = mStrokeWidth / 2;

    bodyRect.left = (getWidth() - bodyWidth) / 2;    bodyRect.top = (getHeight() - bodyHeight) / 2;    bodyRect.right = bodyRect.left + bodyWidth;    bodyRect.bottom = bodyRect.top + bodyHeight;

    radius = bodyWidth / 2;    footHeight = radius * 0.4333f; 

    handsHeight =  (getHeight() + bodyHeight) / 2   + offset - radius * 1.65f;}

绘制参数好了,接下来就是一步步绘制几何图形了

画身体

显然身体是一个矩形加上,上下半圆,这边只要用一个圆角矩形,然后圆角的弧度半径用身体宽度的一半就可以达到这个效果了。把身体的矩形外存起来,后面经常要用到其相对位置进行对其它部位的定位,代码如下:

protected void onDraw(Canvas canvas) {    ...    drawBody(canvas);       // 身体    drawBodyStroke(canvas); // 最后画身体的描边,可以摭住一些过渡的棱角}

private void drawBody(Canvas canvas) {    mPaint.setColor(colorBody);    mPaint.setStyle(Paint.Style.FILL);

    canvas.drawRoundRect(bodyRect, radius, radius, mPaint);}

private void drawBodyStroke(Canvas canvas) {    mPaint.setColor(colorStroke);    mPaint.setStrokeWidth(mStrokeWidth);    mPaint.setStyle(Paint.Style.STROKE);    canvas.drawRoundRect(bodyRect, radius, radius, mPaint);}

画衣服

这是穿上裤子的样子
  • 首先画 底下的半圆
rect.left = (getWidth() - bodyWidth) / 2 + offset;rect.top = (getHeight() + bodyHeight) / 2 - radius * 2 + offset;rect.right = rect.left + bodyWidth - offset * 2;rect.bottom = rect.top + radius * 2 - offset * 2;

mPaint.setColor(colorClothes);mPaint.setStyle(Paint.Style.FILL);mPaint.setStrokeWidth(mStrokeWidth);canvas.drawArc(rect, 0, 180, true, mPaint);
  • 再画半圆上方的矩形, w 表示矩形离左边身体的距离,h 矩形的高
int h = (int) (radius * 0.5);int w = (int) (radius * 0.3);

rect.left += w;rect.top = rect.top + radius - h;rect.right -= w;rect.bottom = rect.top + h;

canvas.drawRect(rect, mPaint);
  • 上面的画完之后,要在衣服上面描一层黑色的边,用canvas.drawLines把线一条条画出来吧,这边要同时考虑画笔的描边宽度,否则会出现连接点有锯齿的感觉。( 2020 注:这是当时最直接的想法,现在来看用 Path 来绘制,每个点用 rLineTo 去连接,代码会简单得多。)
mPaint.setColor(colorStroke);mPaint.setStyle(Paint.Style.FILL);mPaint.setStrokeWidth(mStrokeWidth);float[] pts = new float[20];// 5 条线

pts[0] = rect.left - w;pts[1] = rect.top + h;pts[2] = pts[0] + w;pts[3] = pts[1];

pts[4] = pts[2];pts[5] = pts[3] + offset;pts[6] = pts[4];pts[7] = pts[3] - h;

pts[8] = pts[6] - offset;pts[9] = pts[7];pts[10] = pts[8] + (radius - w) * 2;pts[11] = pts[9];

pts[12] = pts[10];pts[13] = pts[11] - offset;pts[14] = pts[12];pts[15] = pts[13] + h;

pts[16] = pts[14] - offset;pts[17] = pts[15];pts[18] = pts[16] + w;pts[19] = pts[17];canvas.drawLines(pts, mPaint);
  • 画吊带 就是一个直角梯形,把梯形的四个顶点计算出来,使用canvas.drawPath将其画上去,然后纽扣用一个实心的小圆表示
// 画左吊带path.reset();path.moveTo(rect.left - w - offset, handsHeight);path.lineTo(rect.left + h / 4f, rect.top + h / 2f);final float smallW = w / 2f * (float) Math.sin(Math.PI / 4);path.lineTo(rect.left + h / 4f + smallW, rect.top + h / 2f - smallW);final float smallW2 = w / (float) Math.sin(Math.PI / 4) / 2;path.lineTo(rect.left - w - offset, handsHeight - smallW2);canvas.drawPath(path, mPaint);

mPaint.setColor(colorStroke);mPaint.setStrokeWidth(mStrokeWidth);mPaint.setStyle(Paint.Style.STROKE);canvas.drawPath(path, mPaint);mPaint.setStyle(Paint.Style.FILL_AND_STROKE);canvas.drawCircle(rect.left + h / 5f, rect.top + h / 4f, mStrokeWidth * 0.7f, mPaint);

// 画右吊带,代码差不多省略了,坐标对称
  • 画中间的口袋 是一个下面两边是圆角的圆角矩形,但是貌似不能直接画这样的圆角矩形,所以我就用土办法,不就是一个多边形吗,用canvas.drawPath来画,在圆角的地方添加圆弧过渡path.addArc
path.reset();float radiusBigPocket = w / 2.0f;path.moveTo(rect.left + 1.5f * w, rect.bottom - h / 4f);path.lineTo(rect.right - 1.5f * w, rect.bottom - h / 4f);path.lineTo(rect.right - 1.5f * w, rect.bottom + h / 4f);path.addArc(rect.right - 1.5f * w - radiusBigPocket * 2, rect.bottom + h / 4f - radiusBigPocket,        rect.right - 1.5f * w, rect.bottom + h / 4f + radiusBigPocket, 0, 90);path.lineTo(rect.left + 1.5f * w + radiusBigPocket, rect.bottom + h / 4f + radiusBigPocket);

path.addArc(rect.left + 1.5f * w, rect.bottom + h / 4f - radiusBigPocket,        rect.left + 1.5f * w + 2 * radiusBigPocket, rect.bottom + h / 4f + radiusBigPocket, 90, 90);path.lineTo(rect.left + 1.5f * w, rect.bottom - h / 4f - offset);canvas.drawPath(path, mPaint);    
  • 左右两个小口袋也直接用一个小弧来解决掉
// 下边一竖,分开裤子canvas.drawLine(bodyRect.left + bodyWidth / 2, bodyRect.bottom - h * 0.8f, bodyRect.left + bodyWidth / 2, bodyRect.bottom, mPaint);// 左边的小口袋float radiusSmallPocket = w * 1.2f;canvas.drawArc(bodyRect.left - radiusSmallPocket, bodyRect.bottom - radius - radiusSmallPocket,       bodyRect.left + radiusSmallPocket, bodyRect.bottom - radius + radiusSmallPocket, 80, -60, false, mPaint);// 右边小口袋canvas.drawArc(bodyRect.right - radiusSmallPocket, bodyRect.bottom - radius - radiusSmallPocket,        bodyRect.right + radiusSmallPocket, bodyRect.bottom - radius + radiusSmallPocket, 100, 60, false, mPaint);
  • 嗯,衣服画完了。
protected void onDraw(Canvas canvas) {    ...    drawClothes(canvas);//衣服}

private void drawClothes(Canvas canvas) {    //就是上面那一堆代码按顺序合起来啦。。。。。}

画脚

脚这部分比较简单,从身体的下方,一个竖直的矩形下来,再加上一个左边圆角的圆角矩形,还是通过画Path来实现。

private void drawFeet(Canvas canvas) {    mPaint.setStrokeWidth(mStrokeWidth);    mPaint.setColor(colorStroke);    mPaint.setStyle(Paint.Style.FILL_AND_STROKE);

    float radiusFoot = radius / 3 * 0.4f;    float leftFootStartX = bodyRect.left + radius - offset * 2;    float leftFootStartY = bodyRect.bottom - offset;    float footWidthA = radius * 0.5f;//脚宽度大-到半圆结束    float footWidthB = footWidthA / 3;//脚宽度-比较细的部分

    // 左脚    path.reset();    path.moveTo(leftFootStartX, leftFootStartY);    path.lineTo(leftFootStartX, leftFootStartY + footHeight);    path.lineTo(leftFootStartX - footWidthA + radiusFoot, leftFootStartY + footHeight);

    rect.left = leftFootStartX - footWidthA;    rect.top = leftFootStartY + footHeight - radiusFoot * 2;    rect.right = rect.left + radiusFoot * 2;    rect.bottom = rect.top + radiusFoot * 2;    path.addArc(rect, 90, 180);    path.lineTo(rect.left + radiusFoot + footWidthB, rect.top);    path.lineTo(rect.left + radiusFoot + footWidthB, leftFootStartY);    path.lineTo(leftFootStartX, leftFootStartY);    canvas.drawPath(path, mPaint);

  // 右脚与左脚实现一致,坐标对称,代码略}

画手

这里是双手放在后背的样子

手我用的是一个等腰直角三角形来实现,斜边就是吊带到裤子,从直角顶点作高到斜边,通过小直角三角形的直角边相等就可以算出顶点的坐标。这个时候还是有个圆角,刚开始我实现的时候是像上面那些通过path.addArc加上圆角,但是这边计算好之后和原来的衔接一直有问题,在调了半天之后,偶然发现mPaint.setPathEffect(new CornerPathEffect(radiusHand));这个方法,可以使path的拐角用圆角来过渡,一下子就简单到爆了,果然科学技术是第一生产力。

private void drawHands(Canvas canvas) {    ...           // 左手    path.moveTo(bodyRect.left, handsHeight);    path.lineTo(bodyRect.left - hypotenuse / 2, handsHeight + hypotenuse / 2);    path.lineTo(bodyRect.left +offset, bodyRect.bottom - radius +offset);    path.lineTo(bodyRect.left, handsHeight);    canvas.drawPath(path, mPaint);

    mPaint.setStrokeWidth(mStrokeWidth);    mPaint.setStyle(Paint.Style.STROKE);    mPaint.setColor(colorStroke);    canvas.drawPath(path, mPaint);

    // 右手略 ...    // 手臂内侧拐点    path.reset();    mPaint.setStyle(Paint.Style.FILL);    path.moveTo(bodyRect.left, handsHeight + hypotenuse / 2 - mStrokeWidth);    path.lineTo(bodyRect.left - mStrokeWidth * 2, handsHeight + hypotenuse / 2 + mStrokeWidth * 2);    path.lineTo(bodyRect.left, handsHeight + hypotenuse / 2 + mStrokeWidth);    canvas.drawPath(path, mPaint);    ... }

画眼睛,嘴巴

三个字,圆圆圆

反正就是各种画圆,或者弧形,嘴巴部分偷懒也就一条小弧一笔带过了,哈哈

private void drawEyesMouth(Canvas canvas) {    // 眼睛中心处于上半圆直径 往上的高度偏移    float eyesOffset = radius * 0.1f;    mPaint.setStrokeWidth(mStrokeWidth * 5);

    // 计算眼镜带弧行的半径 分两段,以便眼睛中间有隔开的效果    float radiusGlassesRibbon = (float) (radius / Math.sin(Math.PI / 20));    rect.left = bodyRect.left + radius - radiusGlassesRibbon;    rect.top = bodyRect.top + radius - (float) (radius / Math.tan(Math.PI / 20)) - radiusGlassesRibbon - eyesOffset;    rect.right = rect.left + radiusGlassesRibbon * 2;    rect.bottom = rect.top + radiusGlassesRibbon * 2;    canvas.drawArc(rect, 81, 3, false, mPaint);    canvas.drawArc(rect, 99, -3, false, mPaint);

    // 眼睛半径    float radiusEyes = radius / 3;    mPaint.setColor(Color.WHITE);    mPaint.setStrokeWidth(mStrokeWidth);    mPaint.setStyle(Paint.Style.FILL);

    canvas.drawCircle(bodyRect.left + bodyWidth / 2 - radiusEyes - offset, bodyRect.top + radius - eyesOffset, radiusEyes, mPaint);    canvas.drawCircle(bodyRect.left + bodyWidth / 2 + radiusEyes + offset, bodyRect.top + radius - eyesOffset, radiusEyes, mPaint);

    mPaint.setColor(colorStroke);    mPaint.setStyle(Paint.Style.STROKE);    canvas.drawCircle(bodyRect.left + bodyWidth / 2 - radiusEyes - offset, bodyRect.top + radius - eyesOffset, radiusEyes, mPaint);    canvas.drawCircle(bodyRect.left + bodyWidth / 2 + radiusEyes + offset, bodyRect.top + radius - eyesOffset, radiusEyes, mPaint);

    final float radiusEyeballBlack = radiusEyes / 3;    mPaint.setStyle(Paint.Style.FILL);    canvas.drawCircle(bodyRect.left + bodyWidth / 2 - radiusEyes - offset, bodyRect.top + radius - eyesOffset, radiusEyeballBlack, mPaint);    canvas.drawCircle(bodyRect.left + bodyWidth / 2 + radiusEyes + offset, bodyRect.top + radius - eyesOffset, radiusEyeballBlack, mPaint);

    mPaint.setColor(Color.WHITE);    final float radiusEyeballWhite = radiusEyeballBlack / 2;    canvas.drawCircle(bodyRect.left + bodyWidth / 2 - radiusEyes + radiusEyeballWhite - offset * 2,            bodyRect.top + radius - radiusEyeballWhite + offset - eyesOffset,            radiusEyeballWhite, mPaint);    canvas.drawCircle(bodyRect.left + bodyWidth / 2 + radiusEyes + radiusEyeballWhite,            bodyRect.top + radius - radiusEyeballWhite + offset - eyesOffset,            radiusEyeballWhite, mPaint);

    // 画嘴巴,因为位置和眼睛有相对关系,所以写在一块    mPaint.setColor(colorStroke);    mPaint.setStyle(Paint.Style.STROKE);    mPaint.setStrokeWidth(mStrokeWidth);    float radiusMonth = radius;    rect.left = bodyRect.left;    rect.top = bodyRect.top - radiusMonth / 2.5f;    rect.right = rect.left + radiusMonth * 2;    rect.bottom = rect.top + radiusMonth * 2;    canvas.drawArc(rect, 95, -20, false, mPaint);}

脚下的阴影

这是最后一步了,直接画一个非常扁的椭圆放在脚下面就可以了

不科学啊,长这么胖,为毛影子这么瘦(别在意这些细节)
private void drawFeetShadow(Canvas canvas) {    mPaint.setColor(getResources().getColor(android.R.color.darker_gray));    canvas.drawOval(bodyRect.left + bodyWidth * 0.15f,            bodyRect.bottom - offset + footHeight,            bodyRect.right - bodyWidth * 0.15f,            bodyRect.bottom - offset + footHeight + mStrokeWidth * 1.3f, mPaint);}

重写 onDraw 方法

按层级依次调用上述的各种方法,画完收工。

@Overrideprotected void onDraw(Canvas canvas) {    drawFeetShadow(canvas); // 脚下的阴影    drawFeet(canvas);       // 脚    drawHands(canvas);      // 手    drawBody(canvas);       // 身体    drawClothes(canvas);    // 衣服    drawEyesMouth(canvas);  // 眼睛,嘴巴    drawBodyStroke(canvas); // 最后画身体的描边,可以摭住一些过渡的棱角}

少了点什么?

画完了,好像少了点什么。。。。。对了,头发。好吧,我画的是程序猿,哪来的头发 - -

至此,正常画风的小黄人已经画完了,但是吧,好不容易画好,好像没啥意思,脑洞大开一下吧。电影中的小黄人中病毒后是会变成紫色的,那我们用代码画,换个颜色还不是分分钟,不但要紫色,还要各种颜色。

三行代码搞定脑洞

public void randomBodyColor() {    Random random = new Random();    colorBody = Color.rgb(random.nextInt(255), random.nextInt(255), random.nextInt(255));    invalidate();}

然后效果就变成了这样。

看起来还有点小酷炫
希望大家喜欢,完整源码在 github ,可点击“阅读原文”查看

推荐阅读

设计模式概览:六大设计原则

RecyclerView 的缓存复用机制

ServiceManager 的工作原理

关注我

助你升职加薪

Android 面试官

原创不易,在看支持!

绘制半圆_Android Canvas 绘制小黄人相关推荐

  1. html5用canvas画小黄人

    初学者,用canvas画小黄人 <script>window.onload=function(){var c=document.getElementById("people&qu ...

  2. canvas画小黄人

    效果 1.准备画布 <canvas id="canvas" width="1200" height="1000"></ca ...

  3. IOS绘制小黄人,对绘图知识的一个练习

    IOS绘制小黄人,对绘图知识的一个练习 #define kTopX rect.size.width * 0.5 #define kTopRadius 80 #define kTopY 260// // ...

  4. Python小黄人绘制

    Python小黄人绘制 使用python turtle库绘制小黄人 ​ 附上各坐标点的坐标图 完整代码: import turtle as t # 初始化 t.setup(800,800) t.pen ...

  5. 【python绘图库turtle实战】使用python绘图库turtle绘制:太阳花、彩虹线与小黄人【含完整源码】

    本文使用python的turtle绘制3幅图代码示例: 1.绘制太阳花 import turtle as t import time t.color("red","yel ...

  6. MATLAB绘制小黄人

    最近因为疫情的原因,在家比较无聊,顺便学习用matlab画画. 首先,在网上找了一张小黄人的简笔画. 然后,选好坐标原点,计算出要画的图形位置坐标. 最后,用代码绘制出图形,并填充上颜色. 下面是我自 ...

  7. 用python实现小黄人自动绘制

    用python实现小黄人自动绘制 学了python的画笔之后用python画笔实现了小黄人的绘制 就是分部位的绘制小黄人的身体,引用系统小海龟函数 下面展示一些 全部代码. import turtle ...

  8. Python库 turtlede的使用(绘制小黄人、樱花树、小猪佩奇、皮卡丘)

    最近在学习Python库turtle的用法,顺便也整理了几个用turtle库画的图形,具体如下. 一  turtle的基本操作 1. 引用 turtle库 2. 常用的基础函数 (1)绘制状态的函数 ...

  9. HTML5之canvas画布教你绘画小黄人

    HTML5之canvas画布教你绘画小黄人 Canvas画布:顾名思义绘画的基础,也是一幅画作的根.同样,我们学习技术也需要一步步往上走立好自己根,才能更好的成长.有一些东西总不能完美,但我们都有追求 ...

最新文章

  1. R语言诊断试验数据处理与ROC分析实战案例:联合诊断ROC
  2. Win32 Edit 文本框追加字符串
  3. java字符排序规则_java 重写排序规则,用于代码层级排序
  4. 【语义分割】OCRNet:Object-Context Representations for Semantic Segmentation
  5. asp.net 之高速缓存
  6. oracle pga建议值,Oracle PGA作用
  7. python exceptions怎么用_Python基础介绍 | Exceptions异常
  8. iOS开发总结——项目目录结构
  9. (转)asp.net c#如何采集需要登录的页面?
  10. Elasticsearch中的嵌套查询介绍及实例
  11. 汽车故障检测仪计算机教程,如何使用汽车故障诊断仪进行汽车维修
  12. DSIS多媒体信息发布系统液晶广告机管理软件
  13. android强制开启深色模式bug,强制深色模式软件
  14. 为什么16位int的取值范围是-32768~32767?
  15. MS发起的PDP上下文激活过程
  16. 快速数论变换与多项式常用运算
  17. DSG-01-2B3B-A110-51T、DSG-01-2B3B-A100-70电磁控制换向阀
  18. 章泽天卸任刘强东旗下公司董事 官方回应:正常商业调整
  19. 双离合档把上按钮作用_大众车自动档档把上的按钮是干什么用的?
  20. java在线编译网站

热门文章

  1. spring学习(4):spring管理对象之间的关联关系
  2. 玩转oracle 11g(40):Oracle11g 不区分大小写设定
  3. java学习(101):arraylist的遍历和增加
  4. java学习(5):全局变量和局部变量
  5. 实例65:python
  6. 计算机win7内容已满,Win7旗舰版电脑C盘满了怎么清理
  7. js 循环拆词_javascript forEach通用循环遍历方法
  8. linux扩容根目录空间_Linux系统扩容根目录磁盘空间的操作方法
  9. python selenium 处理弹窗_python+selenium 抓取弹出对话框信息
  10. .NETFramework-Web.Mvc:ViewResult