Android原生繪圖之炫酷倒計時
零、前言
1.本篇原型是慕課網的教程,但 是用JavaScript實現在瀏覽器上
的, ofollow,noindex">詳見
2.最近感覺安卓Canvas不比html5的canvas差,使用想 復刻一下到Android上
3.本篇並不止於教程,而是以知其所以然來運用字元點陣及動效,這也是從JavaScript移植到安卓的必要條件
4.本篇會深入地分析整體思路與邏輯,為大家提供一種canvas繪製的思路,相信會給你帶來收穫。
最終效果

最終效果.gif
一、字元的點陣顯示
第一個問題是如何字元的點陣顯示,以及點陣形狀的自定義:效果如下:

字元的點陣顯示.png
1.從 1
開始分析:先看一個數組:
/** * 用來顯示點陣的二維陣列 */ public static final int[][] digit_test = new int[][] { {0, 0, 0, 1, 1, 0, 0}, {0, 1, 1, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {1, 1, 1, 1, 1, 1, 1} };//1
/** * 顏色陣列 */ public static final int[] colors = new int[]{ 0xff33B5E5, 0xff0099CC, 0xffAA66CC, 0xff9933CC, 0xff99CC00, 0xff669900, 0xffFFBB33, 0xffFF8800, 0xffFF4444, 0xffCC0000};
如果你高度散光,估計能一眼看出來些什麼,看不出來的話,看下圖:
你也許會說:暈,這麼簡單粗暴。----沒錯,就是這麼粗暴

2.其實分析出來也不麻煩
就是二維陣列一行一行遍歷,遇到1就畫圖形,這裡是圓形,你可以自己畫任意的形狀
補充一下, 這裡的1和0都是自定義的標示符
,你可以任意,但繪製的邏輯也要同步修改

點陣分析.png
private void renderDigitTest(Canvas canvas) { for (int i = 0; i < Cons.digit_test.length; i++) { for (int j = 0; j < Cons.digit_test[j].length; j++) {//一行一行遍歷,遇到1就畫 if (Cons.digit_test[i][j] == 1) { canvas.save(); float rX = (j * 2 + 1) * (mRadius + 1);//第(i,j)個點圓心橫座標 float rY = (i * 2 + 1) * (mRadius + 1);//第(i,j)個點圓心縱座標 canvas.translate(rX, rY); canvas.drawCircle(rX, rY, mRadius, mPaint);//畫圓 int color = Cons.colors[(int) (Math.random() * Cons.colors.length)]; mPaint.setColor(color); canvas.restore(); } } } }
3.如何多個數字呢?
上古有道,道生一,一生二
,會一個自然第二個也就不遠了
既然一個二維陣列可以表示一個數,那麼用一個三維陣列再成放10個二維陣列不就表示數字了嗎!
三維陣列太長了,也沒有什麼技術含量,貼這裡影響市容,放在文尾,自行拷貝。
4.數字0~9的渲染函式:(這裡就畫五角星吧,路徑繪製可詳見Path篇)
/** * * @param pos 偏移量 x,y * @param num 要顯示的數字 * @param canvas 畫布 */ private void renderDigit(PointF pos, int num, Canvas canvas) { for (int i = 0; i < Cons.digit[num].length; i++) { for (int j = 0; j < Cons.digit[num][j].length; j++) { if (Cons.digit[num][i][j] == 1) { canvas.save(); float rX = pos.x + j * 2 * (mRadius + 1) + (mRadius + 1);//第(i,j)個點圓心橫座標 float rY = pos.y + i * 2 * (mRadius + 1) + (mRadius + 1);//第(i,j)個點圓心縱座標 canvas.translate(rX, rY); Path path = CommonPath.nStarPath(5, mRadius, mRadius / 2); int color = Cons.colors[(int) (Math.random() * Cons.colors.length)]; mPaint.setColor(color); canvas.drawPath(path, mPaint); canvas.restore(); } } } }
OnDraw()中:
canvas.save(); renderDigit( 1, canvas); canvas.translate(150, 0); renderDigit( 9, canvas); canvas.translate(150, 0); renderDigit(9, canvas); canvas.translate(150, 0); renderDigit(4, canvas); canvas.restore();

字元的點陣顯示.png
OK,第一步完成,萬事開頭難,理解之後,後面就很簡單了
二、靜態時間顯示與倒計時:
1.靜態時間顯示
只要動態獲取時間即可,在onDraw中將時間解析成響應數字繪製出來,是不是很簡單
mCalendar = Calendar.getInstance(); int hour = mCalendar.get(Calendar.HOUR_OF_DAY); int min = mCalendar.get(Calendar.MINUTE); int sec = mCalendar.get(Calendar.SECOND); canvas.save(); //繪製小時 renderDigit(hour / 10, canvas); canvas.translate(17*mRadius, 0); renderDigit(hour % 10, canvas); //繪製冒號: canvas.translate(17*mRadius, 0); renderDigit(10, canvas); //繪製分鐘 canvas.translate(11*mRadius, 0); renderDigit(min / 10, canvas); canvas.translate(17*mRadius, 0); renderDigit(min % 10, canvas); //繪製冒號: canvas.translate(17*mRadius, 0); renderDigit(10, canvas); //繪製秒: canvas.translate(11*mRadius, 0); renderDigit(sec / 10, canvas); canvas.translate(17*mRadius, 0); renderDigit(sec % 10, canvas); canvas.restore();

時鐘靜態效果.png
通過Handler定時傳送訊息更新檢視:

時鐘動態效果.gif
倒計時處理
//初始時設定截止日期 mCalendar = Calendar.getInstance(); mCalendar.set(2018, 11-1, 11, 21, 21, 21); mDeadTime = mCalendar.getTimeInMillis(); //OnDraw中獲取之間的毫秒數,處理 mCalendar = Calendar.getInstance(); long lastSec = (mDeadTime - mCalendar.getTimeInMillis()) / 1000;//持續時間 int hour = (int) (lastSec / 3600); int min = (int) (lastSec - hour * 3600) / 60; int sec = (int) (lastSec % 60); Log.d(TAG, "render: "+ hour+","+min+","+sec); //接下來的和上面一樣

倒計時處理.gif
4.重點:小球的運動
有點小複雜,簡單地畫了一個流程圖

分析.png
先把小球封裝一下:
/** * 作者:張風捷特烈<br/> * 時間:2018/11/11 0011:6:13<br/> * 郵箱:[email protected]<br/> * 說明:小球封裝類 */ public class Ball { public float a;//加速度 public float vX;//速度X public float vY;//速度Y public float pX;//點位X public float pY;//點位Y public int color;//顏色 }
準備工作
//成員變數 private int mRadius = 12;//半徑 private int mLimitY;//Y邊界 private final static int BALLS_VY = 15;//y方向速度 private final static int BALLS_ABS_VX = 20;//x方向速度數值 private final static float BALLS_A = 0.98f;//小球的加速度 private List<Ball> mBalls = new ArrayList<>();//小球集合 private long mDeadTime;//剩餘時間 private int mCurDeadTime;//當前剩餘時間 private Paint mPaint; private Path mStarPath; //初始化:init() mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setStyle(Paint.Style.FILL); mLimitY = winSize.y - 200; Calendar calendar = Calendar.getInstance(); calendar.set(2018, 11 - 1, 11, 21, 21, 21); mDeadTime = calendar.getTimeInMillis(); mCurDeadTime = getLifeSec();
1.使用ValueAnimator不斷重繪介面
ValueAnimator animator = ValueAnimator.ofFloat(0, 1); animator.setRepeatCount(-1); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { update(); invalidate(); } }); animator.start();
2.小球集合的更新總函式
/** * 小球集合的更新總函式 */ private void update() { addBalls();//新增小球 updateBalls();//更新小球 }
3.新增倒計時中改動的點
核心是:判斷當前時間是否改變,再將點位放到集合中
這是要注意我繪製小球放在繪製秒的前面,所以根據繪製小球時的畫布原點,在加入是對小球的點位進行偏移
/** * 新增倒計時中改動的點 */ private void addBalls() { int nextSec = getLifeSec(); int nextHours = nextSec / 3600; int nextMinutes = (nextSec - nextHours * 3600) / 60; int nextSeconds = nextSec % 60; int curHours = mCurDeadTime / 3600; int curMinutes = (mCurDeadTime - curHours * 3600) / 60; int curSeconds = mCurDeadTime % 60; if (nextSeconds != curSeconds) {//判斷當前時間是否改變,再將點位放到集合中 if ((curHours / 10) != (nextHours / 10)) { addBalls((-17 * 5 - 11 * 2) * mRadius, curHours / 10); } if ((curHours % 10) != (nextHours % 10)) { addBalls((-17 * 4 - 11 * 2) * mRadius, curHours % 10); } if ((curMinutes / 10) != (nextMinutes / 10)) { addBalls((-17 * 3 - 11) * mRadius, curMinutes / 10); } if ((curMinutes % 10) != (nextMinutes % 10)) { addBalls((-17 * 2 - 11) * mRadius, curMinutes % 10); } if ((curSeconds / 10) != (nextSeconds / 10)) { addBalls(-17 * mRadius, curSeconds / 10); } if ((curSeconds % 10) != (nextSeconds % 10)) { addBalls(0, nextSeconds % 10); } mCurDeadTime = nextSec; } } /** * 新增小球的核心邏輯 * * @param offsetX x方向偏移 * @param num改變的時間數字 */ private void addBalls(int offsetX, int num) { for (int i = 0; i < Cons.digit[num].length; i++) { for (int j = 0; j < Cons.digit[num][i].length; j++) { if (Cons.digit[num][i][j] == 1) { Ball ball = new Ball(); ball.a = BALLS_A; ball.vX = (float) (Math.pow(-1, Math.ceil(Math.random() * 1000)) * BALLS_ABS_VX * Math.random()); ball.vY = (float) (BALLS_VY * Math.random()); ball.pX = offsetX + j * 2 * (mRadius + 1) + (mRadius + 1);//第(i,j)個點圓心橫座標 ball.pY = i * 2 * (mRadius + 1) + (mRadius + 1);//第(i,j)個點圓心縱座標 ball.color = Cons.colors[(int) (Math.random() * Cons.colors.length)]; mBalls.add(ball); } } } }
4.這裡是讓小球運動的核心:
更新所有球的位置---讓球運動,並且越界移除
根據速度和加速度的公式,每次重新整理的時間作為單位時間
安卓的ValueAnimator是0.167秒,由於程式碼的執行效率,會有一點偏差,大太多就會造成不流暢,即視覺卡頓
/** * 更新所有球的位置---讓球運動 * 並且越界移除 */ private void updateBalls() { int minX = (-17 * 5 - 11 * 2) * mRadius;//限定x範圍最小值 int maxX = 400;//限定x範圍大值 for (Ball ball : mBalls) { ball.pX += ball.vX;//x=xo+v*t ball.pY += ball.vY; ball.pY += ball.a;//v=vo+a*t if (ball.pY >= mLimitY - mRadius) {//超過Y底線,反彈 ball.pY = mLimitY - mRadius; ball.vY = -ball.vY * 0.99f; } if (ball.pX > maxX) {//超過X最大值,反彈 ball.pX = maxX - mRadius; ball.vX = -ball.vX * 0.99f; } } for (int i = 0; i < mBalls.size(); i++) {//刪除越界的點 if (mBalls.get(i).pX + mRadius < minX || mBalls.get(i).pY + mRadius < 0 || mBalls.get(i).pX - mRadius > maxX) { mBalls.remove(i); } } }
5.獲取倒計時剩餘秒數
/** * 獲取倒計時剩餘秒數 * * @return 倒計時剩餘秒數 */ private int getLifeSec() { int life = Math.round((mDeadTime - System.currentTimeMillis()) / 1000); life = life > 0 ? life : 0; return life; }
6.繪製的方法:
和上面比就多了個畫圓球的方法,把化數字的方法抽取了一下。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int hour = getLifeSec() / 3600; int min = (getLifeSec() - hour * 3600) / 60; int sec = getLifeSec() % 60; canvas.save(); //繪製小時 drawHour(canvas, hour); //繪製冒號: drawDot(canvas); //繪製分鐘 drawMin(canvas, min); //繪製冒號: drawDot(canvas); //繪製秒: drawSec(canvas, sec); canvas.restore(); } /** * 繪製秒 * * @param canvas * @param sec */ private void drawSec(Canvas canvas, int sec) { canvas.translate(11 * mRadius, 0); drawBall(canvas);//繪製小球 renderDigit(sec / 10, canvas); canvas.translate(17 * mRadius, 0); renderDigit(sec % 10, canvas); } /** * 繪製小球 * * @param canvas */ private void drawBall(Canvas canvas) { canvas.save(); canvas.translate(17 * mRadius, 0); for (Ball ball : mBalls) { mPaint.setColor(ball.color); mPaint.setStyle(Paint.Style.FILL); canvas.drawCircle(ball.pX, ball.pY, mRadius, mPaint); } canvas.restore(); } /** * 繪製分鐘 * @param canvas * @param min */ private void drawMin(Canvas canvas, int min) { canvas.translate(11 * mRadius, 0); renderDigit(min / 10, canvas); canvas.translate(17 * mRadius, 0); renderDigit(min % 10, canvas); } /** *繪製冒號 * @param canvas */ private void drawDot(Canvas canvas) { canvas.translate(17 * mRadius, 0); renderDigit(10, canvas); } /** * 繪製小時 * @param canvas * @param hour */ private void drawHour(Canvas canvas, int hour) { renderDigit(hour / 10, canvas); canvas.translate(17 * mRadius, 0); renderDigit(hour % 10, canvas); } /** * 渲染數字 * @param num要顯示的數字 * @param canvas 畫布 */ private void renderDigit(int num, Canvas canvas) { for (int i = 0; i < Cons.digit[num].length; i++) { for (int j = 0; j < Cons.digit[num][j].length; j++) { if (Cons.digit[num][i][j] == 1) { canvas.save(); float rX = j * 2 * (mRadius + 1) + (mRadius + 1);//第(i,j)個點圓心橫座標 float rY = i * 2 * (mRadius + 1) + (mRadius + 1);//第(i,j)個點圓心縱座標 canvas.translate(rX, rY); mPaint.setColor(Color.BLUE); canvas.drawPath(mStarPath, mPaint); canvas.restore(); } } } }

最終效果.gif
到這裡就OK了,是不是比想象中的要簡單
後記:捷文規範
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--無 | 2018-11-11 | Android原生繪圖之炫酷倒計時 |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的CSDN | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援
附錄、靜態常量
/** * 作者:張風捷特烈<br/> * 時間:2018/11/11 0011:6:02<br/> * 郵箱:[email protected]<br/> * 說明:常量 */ public class Cons { /** * 顏色陣列 */ public static final int[] colors = new int[]{ 0xff33B5E5, 0xff0099CC, 0xffAA66CC, 0xff9933CC, 0xff99CC00, 0xff669900, 0xffFFBB33, 0xffFF8800, 0xffFF4444, 0xffCC0000}; /** * 用來顯示點陣的三維陣列 */ public static final int[][] digit_test = new int[][] { {0, 0, 0, 1, 1, 0, 0}, {0, 1, 1, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {1, 1, 1, 1, 1, 1, 1} };//1 /** * 用來顯示點陣的三維陣列 */ public static final int[][][] digit = new int[][][]{ { {0, 0, 1, 1, 1, 0, 0}, {0, 1, 1, 0, 1, 1, 0}, {1, 1, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {0, 1, 1, 0, 1, 1, 0}, {0, 0, 1, 1, 1, 0, 0} },//0 { {0, 0, 0, 1, 1, 0, 0}, {0, 1, 1, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {1, 1, 1, 1, 1, 1, 1} },//1 { {0, 1, 1, 1, 1, 1, 0}, {1, 1, 0, 0, 0, 1, 1}, {0, 0, 0, 0, 0, 1, 1}, {0, 0, 0, 0, 1, 1, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 1, 1, 0, 0, 0}, {0, 1, 1, 0, 0, 0, 0}, {1, 1, 0, 0, 0, 0, 0}, {1, 1, 0, 0, 0, 1, 1}, {1, 1, 1, 1, 1, 1, 1} },//2 { {1, 1, 1, 1, 1, 1, 1}, {0, 0, 0, 0, 0, 1, 1}, {0, 0, 0, 0, 1, 1, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 1, 1, 1, 0, 0}, {0, 0, 0, 0, 1, 1, 0}, {0, 0, 0, 0, 0, 1, 1}, {0, 0, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {0, 1, 1, 1, 1, 1, 0} },//3 { {0, 0, 0, 0, 1, 1, 0}, {0, 0, 0, 1, 1, 1, 0}, {0, 0, 1, 1, 1, 1, 0}, {0, 1, 1, 0, 1, 1, 0}, {1, 1, 0, 0, 1, 1, 0}, {1, 1, 1, 1, 1, 1, 1}, {0, 0, 0, 0, 1, 1, 0}, {0, 0, 0, 0, 1, 1, 0}, {0, 0, 0, 0, 1, 1, 0}, {0, 0, 0, 1, 1, 1, 1} },//4 { {1, 1, 1, 1, 1, 1, 1}, {1, 1, 0, 0, 0, 0, 0}, {1, 1, 0, 0, 0, 0, 0}, {1, 1, 1, 1, 1, 1, 0}, {0, 0, 0, 0, 0, 1, 1}, {0, 0, 0, 0, 0, 1, 1}, {0, 0, 0, 0, 0, 1, 1}, {0, 0, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {0, 1, 1, 1, 1, 1, 0} },//5 { {0, 0, 0, 0, 1, 1, 0}, {0, 0, 1, 1, 0, 0, 0}, {0, 1, 1, 0, 0, 0, 0}, {1, 1, 0, 0, 0, 0, 0}, {1, 1, 0, 1, 1, 1, 0}, {1, 1, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {0, 1, 1, 1, 1, 1, 0} },//6 { {1, 1, 1, 1, 1, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {0, 0, 0, 0, 1, 1, 0}, {0, 0, 0, 0, 1, 1, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 0, 1, 1, 0, 0, 0}, {0, 0, 1, 1, 0, 0, 0}, {0, 0, 1, 1, 0, 0, 0}, {0, 0, 1, 1, 0, 0, 0} },//7 { {0, 1, 1, 1, 1, 1, 0}, {1, 1, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {0, 1, 1, 1, 1, 1, 0}, {1, 1, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {0, 1, 1, 1, 1, 1, 0} },//8 { {0, 1, 1, 1, 1, 1, 0}, {1, 1, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {1, 1, 0, 0, 0, 1, 1}, {0, 1, 1, 1, 0, 1, 1}, {0, 0, 0, 0, 0, 1, 1}, {0, 0, 0, 0, 0, 1, 1}, {0, 0, 0, 0, 1, 1, 0}, {0, 0, 0, 1, 1, 0, 0}, {0, 1, 1, 0, 0, 0, 0} },//9 { {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0} }//: }; }