Android 画一个 iPhone 样式的小时钟
起因
iPhone主界面的时钟几乎每天都会看到,某天突发奇想,用Android是否也可以画一个类似的呢?于是决定也尝试着画一个,顺便巩固下自绘控件的知识,请看图片
构思
做一件事情先要在脑海中想清楚,仔细观察了下iPhone的这个时钟(以画图的方式来考虑),其实很简单,大概分析如下:
背景是一个黑色的圆角矩形
表盘是一个白色的正圆,且半径稍小于背景圆角矩形宽一半
表盘中间是一个小正圆来显示指针的轴
表盘内圈为1-12的数字,且每个数字的角度间隔为30度(360/12=30)
时针、分钟、秒针分别是粗细、长度、颜色不同的线段,且从中心点放射出
相关Api
既然大概的思路有了,那么就要去思考接下来可能需要用到的Api
重写onMeasure函数,说明请移步测量初步
绘图Api的使用(画圆角矩形、圆、线段、文本)函数的说明请移步2D绘图初步
canvas.drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint)
canvas.drawCircle(float cx, float cy, float radius, @NonNull Paint paint)
canvas.drawLine(float startX, float startY, float stopX, float stopY, @NonNull Paint paint)
canvas.drawText(@NonNull String text, float x, float y, @NonNull Paint paint)
正弦和余弦(计算指针、表盘数字的终点坐标)
Math.sin(double d)
Math.cos(double d)
角度与弧度的换算(由于sin和cos函数需要的参数为“弧度”而不是“角度”,所以需要将角度转换为弧度)
Math.toRadians(double angdeg)
角度转换为弧度该怎么转换呢,我们知道1弧度的弧的长度=半径长度,并且圆周长(这里解释成全部弧长和360度更好理解)为2πr,也就是说360度的弧长为2πr,180度的弧长为πr,那弧度呢?,当然是πr/r=π啦,那假设60度呢?也就是π/(180/60),可以转换为π*60/180,也就是转换公式啦,当然系统为我们提供了直接转换的函数,我们就偷下懒啦,其实该函数内部算法也是用了这个公式,有兴趣的朋友可以点开看下。
指针原理
绘制指针起点坐标为表盘中心点,但是根据时间不一样,终点坐标是会变化的,那么这个终点坐标该怎么求呢?请先看下图:
假设我们的指针正好指向2点钟,此时半径R与垂直中心线的夹角为60度,那么此时指针与圆的切点P(x,y)也就是我们所要的终点坐标,这个终点坐标便是(Px,Py),只要我们求出,Px的水平距离+a(圆心水平距离),Py的垂直距离+b(圆心垂直距离)便得到了终点坐标,这时候就去要用到sin函数(对边与斜边的比)和cos函数(邻边与斜边的比)啦,以半径R为斜边,Px的距离便是sin60*R,Py的距离cos60*R,再加上圆心距离,便得到2点钟的终点坐标,其实情况以此类推。
步骤
测量自身大小
private int measureWidth(int widthMeasureSpec) { int result = DEFAULT_MIN_WIDTH_HEIGHT; int mode = MeasureSpec.getMode(widthMeasureSpec); int size = MeasureSpec.getSize(widthMeasureSpec); if (mode == MeasureSpec.EXACTLY) { result = size; } else if (mode == MeasureSpec.AT_MOST) { result = Math.min(result, size); } return result; } private int measureHeight(int heightMeasureSpec) { int result = DEFAULT_MIN_WIDTH_HEIGHT; int mode = MeasureSpec.getMode(heightMeasureSpec); int size = MeasureSpec.getSize(heightMeasureSpec); if (mode == MeasureSpec.EXACTLY) { result = size; } else if (mode == MeasureSpec.AT_MOST) { result = Math.min(result, size); } return result; }
计算中心坐标点
//计算圆心坐标 private float[] computeCenterPoint() { float[] centerPoint = new float[2]; int width = getMeasuredWidth(); int height = getMeasuredHeight(); int wh = Math.min(width, height); int ct = wh / 2; centerPoint[0] = ct; centerPoint[1] = ct; return centerPoint; }
画黑色圆角矩形背景
//画表外边框 private void drawClockBoundBorder(Canvas canvas) { mPaint.setColor(Color.BLACK); int width = getMeasuredWidth(); int height = getMeasuredHeight(); //根据长宽最小决定相同宽高,防止变形以及超出屏幕范围 float wh = Math.min(width, height); //根据宽算出大概的圆角 mDefaultBorderRadius = wh / 5F; canvas.drawRoundRect(new RectF(0, 0, wh, wh), mDefaultBorderRadius, mDefaultBorderRadius, mPaint); }
画半径稍小于背景圆角矩形宽一半的白色正圆
//画表盘 private void drawClockRoundBorder(Canvas canvas) { mPaint.setColor(Color.WHITE); int width = getMeasuredWidth(); int height = getMeasuredHeight(); //根据长宽最小决定相同宽高,防止变形以及超出屏幕范围 float wh = Math.min(width, height); //根据宽算出大概的圆角 mDefaultRoundRadiusSpace = wh / 20F; canvas.drawCircle(wh / 2, wh / 2, wh / 2 - mDefaultRoundRadiusSpace, mPaint); }
画指针的轴,即黑色小圆
//画表盘中心轴 private void drawClockCenterPoint(Canvas canvas) { mPaint.setColor(Color.BLACK); int width = getMeasuredWidth(); int height = getMeasuredHeight(); //根据长宽最小决定相同宽高,防止变形以及超出屏幕范围 float wh = Math.min(width, height); //根据宽算出大概的圆角 mDefaultCenterPointRadius = wh / 50F; canvas.drawCircle(wh / 2, wh / 2, mDefaultCenterPointRadius, mPaint); }
画表盘数字
//画表盘数字 private void drawClockNumber(Canvas canvas) { float degreeMaxOffset = mDefaultRoundRadiusSpace + mDefaultRoundRadiusSpace; mPaint.setTextAlign(Paint.Align.CENTER); mPaint.setTextSize(mDefaultRoundRadiusSpace * 1.6F); mPaint.setFakeBoldText(true); //12个大刻度,一个大刻度30度 for (int i = 0; i < 12; i++) { float[] points = computePointerPoint((i + 1) * 30, computeCenterPoint()[0] - degreeMaxOffset - mDefaultRoundRadiusSpace); String text = (i + 1) + ""; Rect rect = new Rect(); mPaint.getTextBounds(text, 0, text.length(), rect); float textHeight = rect.height(); float textWidth = rect.width(); canvas.drawText(text, points[2], points[3] + textHeight / 2, mPaint); } }
画指针
//画时分秒指针 private void drawClockAllPointer(Canvas canvas) { float ha; float ma; float sa; Calendar calendar = Calendar.getInstance(); float hour = calendar.get(Calendar.HOUR); float minute = calendar.get(Calendar.MINUTE); float second = calendar.get(Calendar.SECOND); sa = second * 6F; ma = (minute + second / 60) * 6F; ha = (hour + minute / 60 + second / 3600) * 30F; drawClockHourPointer(ha, canvas); drawClockMinutePointer(ma, canvas); drawClockSecondPointer(sa, canvas); } //画时针指针 private void drawClockHourPointer(float angle, Canvas canvas) { mPaint.setColor(Color.BLACK); int width = getMeasuredWidth(); int height = getMeasuredHeight(); //根据长宽最小决定相同宽高,防止变形以及超出屏幕范围 float wh = Math.min(width, height); mDefaultHourPointerWidth = wh / 50F; //时针长度取表盘半径的一半 mDefaultHourPointerLength = (wh / 2 - mDefaultRoundRadiusSpace) * 0.5F; mPaint.setStrokeWidth(mDefaultHourPointerWidth); float[] points = computePointerPoint(angle, mDefaultHourPointerLength); canvas.drawLine(points[0], points[1], points[2], points[3], mPaint); } //画分针指针 private void drawClockMinutePointer(float angle, Canvas canvas) { mPaint.setColor(Color.BLACK); int width = getMeasuredWidth(); int height = getMeasuredHeight(); //根据长宽最小决定相同宽高,防止变形以及超出屏幕范围 float wh = Math.min(width, height); mDefaultMinutePointerWidth = wh / 75F; //分针长度取表盘半径的一半 mDefaultMinutePointerLength = (wh / 2 - mDefaultRoundRadiusSpace) * 0.8F; mPaint.setStrokeWidth(mDefaultMinutePointerWidth); float[] points = computePointerPoint(angle, mDefaultMinutePointerLength); canvas.drawLine(points[0], points[1], points[2], points[3], mPaint); } //画秒针指针 private void drawClockSecondPointer(float angle, Canvas canvas) { mPaint.setColor(Color.RED); int width = getMeasuredWidth(); int height = getMeasuredHeight(); //根据长宽最小决定相同宽高,防止变形以及超出屏幕范围 float wh = Math.min(width, height); mDefaultSecondPointerWidth = wh / 100F; //秒针长度取表盘半径的一半 mDefaultSecondPointerLength = (wh / 2 - mDefaultRoundRadiusSpace) * 0.8F; mPaint.setStrokeWidth(mDefaultSecondPointerWidth); float[] points = computePointerPoint(angle, mDefaultSecondPointerLength); canvas.drawLine(points[0], points[1], points[2], points[3], mPaint); } //根据角度、指针长度 计算出起始坐标 private float[] computePointerPoint(float angle, float pointerLenght) { float[] linePoints = new float[4]; float[] centerPoint = computeCenterPoint(); linePoints[0] = centerPoint[0];//startX linePoints[1] = centerPoint[1];//startY if (angle <= 90F) { linePoints[2] = linePoints[0] + (float) Math.sin(Math.toRadians(angle)) * pointerLenght;//endX linePoints[3] = linePoints[1] - (float) Math.cos(Math.toRadians(angle)) * pointerLenght;//endY } else if (angle <= 180F) { linePoints[2] = linePoints[0] + (float) Math.cos(Math.toRadians(angle - 90F)) * pointerLenght;//endX linePoints[3] = linePoints[1] + (float) Math.sin(Math.toRadians(angle - 90F)) * pointerLenght;//endY } else if (angle <= 270F) { linePoints[2] = linePoints[0] - (float) Math.sin(Math.toRadians(angle - 180F)) * pointerLenght;//endX linePoints[3] = linePoints[1] + (float) Math.cos(Math.toRadians(angle - 180F)) * pointerLenght;//endY } else if (angle <= 360F) { linePoints[2] = linePoints[0] - (float) Math.cos(Math.toRadians(angle - 270F)) * pointerLenght;//endX linePoints[3] = linePoints[1] - (float) Math.sin(Math.toRadians(angle - 270F)) * pointerLenght;//endY } return linePoints; }
让时钟开始工作
public void startClockWork() { if (mIsWorking) return; mIsWorking = true; new Thread(new Runnable() { @Override public void run() { while (mIsWorking) { postInvalidate(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } public void stopClockWork() { mIsWorking = false; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); startClockWork(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); stopClockWork(); }
最运行效果如下:
总结
以上代码没有做太多的优化,请见谅,其中一些重复的计算可以优化的,还有就是在想在onDraw的时候如何只单独绘制三根指针,避免绘制不会实时更改的区域,还在研究当中。
如有描述不当、错误请指正,谢谢。