Android 画一个 iPhone 样式的小时钟

本文介绍了如何在Android中模仿iPhone主界面的小时钟样式,通过自绘控件实现。内容包括起因、构思、所需API、指针绘制原理、绘制步骤以及最终效果展示。开发者详细讲解了利用sin和cos函数计算指针坐标的方法,以及优化绘制过程的思考。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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点钟的终点坐标,其实情况以此类推。

步骤

  1. 测量自身大小

    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;
    }
    
  2. 计算中心坐标点

    //计算圆心坐标
    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;
    }
    
  3. 画黑色圆角矩形背景

    //画表外边框
    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);
    }
    
  4. 画半径稍小于背景圆角矩形宽一半的白色正圆

    //画表盘
    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);
    }
    
  5. 画指针的轴,即黑色小圆

    //画表盘中心轴
    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);
    }
    
  6. 画表盘数字

    //画表盘数字
    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);
    }
    }
    
  7. 画指针

    //画时分秒指针
    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;
    }
    
  8. 让时钟开始工作

    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的时候如何只单独绘制三根指针,避免绘制不会实时更改的区域,还在研究当中。

Demo下载地址

如有描述不当、错误请指正,谢谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值