Android 用Canvas 畫幾何圖形,畫出小黃人
看了大神JR93的這篇文章: 純CSS3畫出小黃人并實現動畫效果 ,萌生了我在安卓上畫小黃人的想法,同時他在文章中的步驟分解,在我的具體實現中起到了很大的幫助。話不多說,先上效果圖
實現步驟
其實很簡單
- 首先找到一張小黃人的圖
- 然后調用 canvas.drawBitmap() 后畫到畫布上 - -。
好吧,一點都不好笑
正文
準備工作
自定義 MinionView extends View ,定義以下成員變量,備用(可以先不看,后面的代碼看到莫名其妙出來的變量再上來看下)
private Paint mPaint; private float bodyWidth; private float bodyHeigh; 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; private float handsHeight;//計算出吊帶的高度時,可以用來做手的高度 private float footHeigh;//腳的高度,用來畫腳部陰影時用
初始化參數
重寫 protected void onDraw(Canvas canvas) 方法,首先調用 如下 (會經常看到一些奇怪的數字,做比例,別問我怎么來的,目測+一點點微調得來的- -。)
private void initParams() { bodyWidth = Math.min(getWidth(), getHeight() * BODY_WIDTH_HEIGHT_SCALE) * BODY_SCALE; bodyHeigh = Math.min(getWidth(), getHeight() * BODY_WIDTH_HEIGHT_SCALE) / BODY_WIDTH_HEIGHT_SCALE * BODY_SCALE; mStrokeWidth = Math.max(bodyWidth / 50, mStrokeWidth); offset = mStrokeWidth / 2; bodyRect = new RectF(); bodyRect.left = (getWidth() - bodyWidth) / 2; bodyRect.top = (getHeight() - bodyHeigh) / 2; bodyRect.right = bodyRect.left + bodyWidth; bodyRect.bottom = bodyRect.top + bodyHeigh; radius = bodyWidth / 2; footHeigh = radius * 0.4333f; handsHeight = (getHeight() + bodyHeigh) / 2 + offset - radius * 1.65f ; }
畫身體

顯然身體是一個矩形加上,上下半圓,這邊只要用一個圓角矩形,然后圓角的弧度半徑用身體寬度的一半就可以達到這個效果了。
把身體的矩形外存起來,后面經常要用到其相對位置進行對其它部位的定位,代碼如下。
drawBody(canvas);//身體 drawBodyStroke(canvas);//最后畫身體的描邊,可以摭住一些過渡的棱角 private void drawBody(Canvas canvas) { initPaint(); mPaint.setColor(colorBody); mPaint.setStyle(Paint.Style.FILL); canvas.drawRoundRect(bodyRect, radius, radius, mPaint); } private void drawBodyStroke(Canvas canvas) { initPaint(); mPaint.setColor(colorStroke); mPaint.setStrokeWidth(mStrokeWidth); mPaint.setStyle(Paint.Style.STROKE); canvas.drawRoundRect(bodyRect, radius, radius, mPaint); }
畫衣服

這是穿上褲子的樣子
-
首先先畫上底下的半圓
RectF rect = new RectF(); rect.left = (getWidth() - bodyWidth) / 2 + offset; rect.top = (getHeight() + bodyHeigh) / 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 把線一條條畫出來吧,這邊要同時考慮畫筆的描邊寬度,否則會出現連接點有鋸齒的感覺。
//畫橫線 initPaint(); 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 將其畫上去,然后紐扣用一個實心的小圓表示
//畫左吊帶 initPaint(); mPaint.setColor(colorClothes); mPaint.setStrokeWidth(mStrokeWidth); mPaint.setStyle(Paint.Style.FILL); Path path = new Path(); path.moveTo(rect.left - w - offset, handsHeight); path.lineTo(rect.left + h / 4, rect.top + h / 2); final float smallW = w / 2 * (float) Math.sin(Math.PI / 4); path.lineTo(rect.left + h / 4 + smallW, rect.top + h / 2 - smallW); final float smallW2 = w / (float) Math.sin(Math.PI / 4) / 2; path.lineTo(rect.left - w - offset, handsHeight - smallW2); canvas.drawPath(path, mPaint); initPaint(); mPaint.setColor(colorStroke); mPaint.setStrokeWidth(mStrokeWidth); mPaint.setStyle(Paint.Style.STROKE); canvas.drawPath(path, mPaint); initPaint(); mPaint.setStyle(Paint.Style.FILL_AND_STROKE); canvas.drawCircle(rect.left + h / 4, rect.top + h / 4, mStrokeWidth, mPaint); //畫右吊帶 initPaint(); mPaint.setColor(colorClothes); mPaint.setStrokeWidth(mStrokeWidth); mPaint.setStyle(Paint.Style.FILL); path.reset(); path.moveTo(rect.left - w + 2 * radius - offset, handsHeight); path.lineTo(rect.right - h / 4, rect.top + h / 2); path.lineTo(rect.right - h / 4 - smallW, rect.top + h / 2 - smallW); path.lineTo(rect.left - w + 2 * radius - offset, handsHeight- smallW2); canvas.drawPath(path, mPaint); initPaint(); mPaint.setColor(colorStroke); mPaint.setStrokeWidth(mStrokeWidth); mPaint.setStyle(Paint.Style.STROKE); canvas.drawPath(path, mPaint); initPaint(); mPaint.setStyle(Paint.Style.FILL_AND_STROKE); canvas.drawCircle(rect.right - h / 4, rect.top + h / 4, mStrokeWidth, mPaint);
-
畫中間的口袋 是一個下面兩邊是圓角的圓角矩形,但是貌似不能直接畫這樣的圓角矩形,所以我就用土辦法,不就是一個多邊形嗎,用 canvas.drawPath 來畫,在圓角的地方添加圓弧過渡 path.addArc
//中間口袋 initPaint(); mPaint.setColor(colorStroke); mPaint.setStrokeWidth(mStrokeWidth); mPaint.setStyle(Paint.Style.STROKE); path.reset(); float radiusBigPokect = w / 2.0f; path.moveTo(rect.left + 1.5f * w, rect.bottom - h / 4); path.lineTo(rect.right - 1.5f * w, rect.bottom - h / 4); path.lineTo(rect.right - 1.5f * w, rect.bottom + h / 4); path.addArc(rect.right - 1.5f * w - radiusBigPokect * 2, rect.bottom + h / 4 - radiusBigPokect, rect.right - 1.5f * w, rect.bottom + h / 4 + radiusBigPokect, 0, 90); path.lineTo(rect.left + 1.5f * w + radiusBigPokect, rect.bottom + h / 4 + radiusBigPokect); path.addArc(rect.left + 1.5f * w, rect.bottom + h / 4 - radiusBigPokect, rect.left + 1.5f * w + 2 * radiusBigPokect, rect.bottom + h / 4 + radiusBigPokect, 90, 90); path.lineTo(rect.left + 1.5f * w, rect.bottom - h / 4 - offset); canvas.drawPath(path, mPaint);
-
左右兩個小口袋也直接用一個小弧來解決掉
// 下邊一豎,分開褲子 canvas.drawLine(bodyRect.left + bodyWidth / 2, bodyRect.bottom - h * 0.8f, bodyRect.left + bodyWidth / 2, bodyRect.bottom, mPaint); // 左邊的小口袋 float radiusSamllPokect = w * 1.2f; canvas.drawArc(bodyRect.left - radiusSamllPokect, bodyRect.bottom - radius - radiusSamllPokect, bodyRect.left + radiusSamllPokect, bodyRect.bottom - radius + radiusSamllPokect, 80, -60, false, mPaint); // 右邊小口袋 canvas.drawArc(bodyRect.right - radiusSamllPokect, bodyRect.bottom - radius - radiusSamllPokect, bodyRect.right + radiusSamllPokect, bodyRect.bottom - radius + radiusSamllPokect, 100, 60, false, mPaint); // canvas.drawArc(left + w/5,);
-
嗯,衣服畫完了。
drawClothes(canvas);//衣服 private void drawClothes(Canvas canvas) { initPaint(); //就是上面那一堆代碼按順序合起來啦。。。。。 }
畫腳

腳這部分比較簡單,從身體的下方,一個豎直的矩形下來,再加上一個左邊圓角的圓角矩形,還是通過畫Path來實現。
drawFeet(canvas);//腳 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 path = new Path(); path.moveTo(leftFootStartX, leftFootStartY); path.lineTo(leftFootStartX, leftFootStartY + footHeigh); path.lineTo(leftFootStartX - footWidthA + radiusFoot, leftFootStartY + footHeigh); RectF rectF = new RectF(); rectF.left = leftFootStartX - footWidthA; rectF.top = leftFootStartY + footHeigh - radiusFoot * 2; rectF.right = rectF.left + radiusFoot * 2; rectF.bottom = rectF.top + radiusFoot * 2; path.addArc(rectF, 90, 180); path.lineTo(rectF.left + radiusFoot + footWidthB, rectF.top); path.lineTo(rectF.left + radiusFoot + footWidthB, leftFootStartY); path.lineTo(leftFootStartX, leftFootStartY); canvas.drawPath(path, mPaint); // 右腳 float rightFootStartX = bodyRect.left + radius + offset * 2; float rightFootStartY = leftFootStartY; path.reset(); path.moveTo(rightFootStartX, rightFootStartY); path.lineTo(rightFootStartX, rightFootStartY + footHeigh); path.lineTo(rightFootStartX + footWidthA - radiusFoot, rightFootStartY + footHeigh); rectF.left = rightFootStartX + footWidthA - radiusFoot * 2; rectF.top = rightFootStartY + footHeigh - radiusFoot * 2; rectF.right = rectF.left + radiusFoot * 2; rectF.bottom = rectF.top + radiusFoot * 2; path.addArc(rectF, 90, -180); path.lineTo(rectF.right - radiusFoot - footWidthB, rectF.top); path.lineTo(rectF.right - radiusFoot - footWidthB, rightFootStartY); path.lineTo(rightFootStartX, rightFootStartY); canvas.drawPath(path, mPaint); }
畫手

這里是雙手放在后背的樣子
手我用的是一個等腰直角三角形來實現,斜邊就是吊帶到褲子,通過sin(45度)就可以算出頂點的坐標。這個時候還是有個圓角,剛開始我實現的時候是像上面那些通過 path.addArc 加上圓角,但是這邊計算好之后和原來的銜接一直有問題,在調了半天之后,偶然發現 mPaint.setPathEffect(new CornerPathEffect(radiusHand)); 這個方法,可以使path的拐角用圓角來過渡,一下子就簡單到爆了,果然科學技術是第一生產力。
drawHands(canvas);//手 private void drawHands(Canvas canvas) { mPaint.setStrokeWidth(mStrokeWidth); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(colorBody); // 左手 Path path = new Path(); float hypotenuse = bodyRect.bottom - radius - handsHeight; float radiusHand = hypotenuse / 6; mPaint.setPathEffect(new CornerPathEffect(radiusHand)); path.moveTo(bodyRect.left, handsHeight); path.lineTo(bodyRect.left - hypotenuse / 2, handsHeight + hypotenuse / 2); path.lineTo(bodyRect.left, bodyRect.bottom - radius); 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); mPaint.setColor(colorBody); path.moveTo(bodyRect.right, handsHeight); path.lineTo(bodyRect.right + hypotenuse / 2, handsHeight + hypotenuse / 2); path.lineTo(bodyRect.right, bodyRect.bottom - radius); 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); path.reset(); path.moveTo(bodyRect.right, handsHeight + hypotenuse / 2 - mStrokeWidth); path.lineTo(bodyRect.right + mStrokeWidth * 2, handsHeight + hypotenuse / 2 + mStrokeWidth * 2); path.lineTo(bodyRect.right, handsHeight + hypotenuse / 2 + mStrokeWidth); canvas.drawPath(path, mPaint); }
畫眼睛,嘴巴

反正就是各種畫圓,或者弧形,嘴巴部分偷懶也就一條小弧一筆帶過了,哈哈
drawEyesMouth(canvas);//眼睛,嘴巴 private void drawEyesMouth(Canvas canvas) { float eyesOffset = radius * 0.1f;//眼睛中心處于上半圓直徑 往上的高度偏移 mPaint.setStrokeWidth(mStrokeWidth * 5); // 計算眼鏡帶弧行的半徑 分兩段,以便眼睛中間有隔開的效果 float radiusGlassesRibbon = (float) (radius / Math.sin(Math.PI / 20)); RectF rect = new RectF(); 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; initPaint(); 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); }
腳下的陰影
這是最后一步了,直接畫一個非常扁的橢圓放在腳下面就可以了

不科學啊,長這么胖,為毛影子這么瘦(別在意這些細節)
drawFeetShadow(canvas);//腳下的陰影 private void drawFeetShadow(Canvas canvas) { mPaint.setColor(getResources().getColor(android.R.color.darker_gray)); canvas.drawOval(bodyRect.left + bodyWidth * 0.15f, bodyRect.bottom - offset + footHeigh, bodyRect.right - bodyWidth * 0.15f, bodyRect.bottom - offset + footHeigh + mStrokeWidth * 1.3f, mPaint); }
在 onDraw 方法,依次調用上述的各種方法,畫完收工。
@Override protected void onDraw(Canvas canvas) { initParams(); initPaint(); 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