如何实现一个时钟控件

对于一个安卓开发者而言,在学习自定义View的过程中,肯定有尝试去实现一个时钟控件,本文就看看如何快速的实现一个靠谱的时钟控件。

准备工作

在开始编码之前,我们需要明确实现时钟控件过程中,涉及到一些要点和可能遇到的阻碍。这里从几个方面来说:

  1. 表盘,表盘由圆形和刻度组成,两者均需绘制
  2. 指针,主要是时针、分针、秒针。其中时针随着时间推移,会产生细微的位移;分针每分钟移动一格;秒针每秒钟移动一格。
  3. 时间的同步

设计

首先我们需要设计刻度的格式,主要包括当为整点时刻时,对应位置的刻度线最好长一点。

其次三个指针的长度也应该不一样,要保持时针最短,秒针最长。

最后,我们需要实现秒针每秒钟移动一格的动画效果

就以上要点中,我们需要关注的是如何将圆等分60份;如何在圆弧上向圆心方向画出刻度;如何实现秒针移动的动画效果。

接下来,我们就这些问题,来看下具体如何实现

实现

控件的实现其实很简单,采用继承View,实现onDraw方法的方式。先绘制表盘,再绘制指针。

表盘主要就是由一个圆组成,然后我们需要在圆弧上画出刻度。在获取圆弧上的刻度位置时,我们需要用到三角函数的知识,通过正余弦来计算每个刻度的起始坐标和结束坐标。确定了每个刻度在圆弧上的起始坐标和圆内的结束坐标,我们就可以绘制一条直线来代表指针

在计算起止坐标时,我们主要通过下面的公式来获取,其中mRadius是圆的半径,moveDegree是基于12点钟位置顺时针移动的角度

float offY = (float) (mRadius - mRadius * Math.cos(Math.toRadians(moveDegree)));

当确定刻度线的起始位置后,我们就可以根据刻度线预设的长度来计算它的结束位置了(HOUR_SCALE_LENGTH是默认时针长度)。

float endY = (float) (offY + HOUR_SCALE_LENGTH * Math.cos(Math.toRadians(moveDegree)));

当获取刻度位置逻辑确定后,我们就可以来循环绘制60个刻度了,绘制代码可以参考如下片段:

  /**
   * 绘制刻度盘,主要包括:
   * 1、表盘圆形
   * 2、60个刻度
   */
  private void drawPanel(Canvas canvas) {
    if (!isPanelDraw) {
      // 获取对应的参数属性:控件宽高,矩形内切圆半径,内切圆圆心位置,三个指针轨迹圆的半径等。
      int measuredHeight = getMeasuredHeight();
      int measuredWidth = getMeasuredWidth();
      // 表盘内切圆半径
      mRadius = Math.min(measuredHeight, measuredWidth) / 2;
      // 表盘圆心x y 坐标
      mCenterX = measuredWidth / 2;
      mCenterY = measuredHeight / 2;
      // 时针轨迹圆半径:内切圆的一半
      mHourHandRadius = (int) (mRadius * 0.50);
      // 分针轨迹圆半径:内切圆的6/10
      mMinuteHandRadius = (int) (mRadius * 0.60);
      // 秒针轨迹圆半径:内切圆的8/10
      mSecondHandRadius = (int) (mRadius * 0.80);
    }
    // 画笔设置
    mPanelPaint.setStyle(Paint.Style.STROKE);
    mPanelPaint.setStrokeWidth(2);
    // 绘制表盘内切圆
    canvas.drawCircle(mCenterX, mCenterY, mRadius, mPanelPaint);

    // 绘制圆心位置
    mPanelPaint.setStyle(Paint.Style.FILL);
    canvas.drawCircle(mCenterX, mCenterY, DOT_RADIUS, mPanelPaint);

    int moveDegree = 0;
    // 从0-360开始绘制刻度
    while (moveDegree < CIRCLE_DEGREE) {
      // 计算刻度在圆弧上的位置,也就是起始位置
      float offX = (float) (mCenterX + mRadius * Math.sin(Math.toRadians(moveDegree)));
      float offY = (float) (mRadius - mRadius * Math.cos(Math.toRadians(moveDegree)));
      if (moveDegree % OFF_HOUR_DEGREE == 0) {
        // 计算刻度在圆内的位置,也就是终点位置,整点位置刻度加长
        float endX = (float) (offX - HOUR_SCALE_LENGTH * Math.sin(Math.toRadians(moveDegree)));
        float endY = (float) (offY + HOUR_SCALE_LENGTH * Math.cos(Math.toRadians(moveDegree)));
        mPanelPaint.setStrokeWidth(HOUR_STROKE);
        canvas.drawLine(offX, offY, endX, endY, mPanelPaint);
      } else {
        // 计算刻度在圆内的位置,也就是终点位置
        float endX = (float) (offX - MINUTE_SCALE_LENGTH * Math.sin(Math.toRadians(moveDegree)));
        float endY = (float) (offY + MINUTE_SCALE_LENGTH * Math.cos(Math.toRadians(moveDegree)));
        mPanelPaint.setStrokeWidth(MINUTE_STROKE);
        // 绘制刻度
        canvas.drawLine(offX, offY, endX, endY, mPanelPaint);
      }
      moveDegree += OFF_MINUTE_DEGREE;
    }
    isPanelDraw = true;
  }

最终的绘制效果为:

绘制完表盘,我们接下来就需要绘制表针了。表针的绘制其实可以参考表盘的绘制,区别是,表针的起点是在圆心位置,终点落在圆内。而且时针、分针、秒针长短不一致,也即是需要绘制三条线段。代码可以参考如下:

/**
   * 绘制指针,主要包括:
   * 1、时针
   * 2、分针
   * 3、秒针
   */
  private void drawScale(Canvas canvas) {
    // 获取当前时间
    Calendar calendar = Calendar.getInstance();
    int hour = calendar.get(Calendar.HOUR);
    int minute = calendar.get(Calendar.MINUTE);
    int second = calendar.get(Calendar.SECOND);

    // 计算三个表针的行进角度
    float secondDegree = CIRCLE_DEGREE / 60 * second;
    float minuteDegree = CIRCLE_DEGREE / 60 * minute;
    float hourDegree = CIRCLE_DEGREE / 12 * hour + minute * 30 / 60;

    // 计算时针当前的终点位置
    float endHourX = (float) (mCenterX + mHourHandRadius * Math.sin(Math.toRadians(hourDegree)));
    float endHourY = (float) (mCenterY - mHourHandRadius * Math.cos(Math.toRadians(hourDegree)));

    // 计算分针当前的终点位置
    float endMinuteX =
        (float) (mCenterX + mMinuteHandRadius * Math.sin(Math.toRadians(minuteDegree)));
    float endMinuteY =
        (float) (mCenterY - mMinuteHandRadius * Math.cos(Math.toRadians(minuteDegree)));

    // 计算秒针当前的终点位置
    float endSecondX =
        (float) (mCenterX + mSecondHandRadius * Math.sin(Math.toRadians(secondDegree)));
    float endSecondY =
        (float) (mCenterY - mSecondHandRadius * Math.cos(Math.toRadians(secondDegree)));

    // 绘制时针
    mPointerPaint.setStrokeWidth(HOUR_STROKE);
    canvas.drawLine(mCenterX, mCenterY, endHourX, endHourY, mPointerPaint);
    // 绘制分针
    mPointerPaint.setStrokeWidth(MINUTE_STROKE);
    canvas.drawLine(mCenterX, mCenterY, endMinuteX, endMinuteY, mPointerPaint);
    // 绘制秒针
    mPointerPaint.setStrokeWidth(SECOND_STROKE);
    canvas.drawLine(mCenterX, mCenterY, endSecondX, endSecondY, mPointerPaint);
  }

为了实现秒针的动画效果,我们采用重复绘制的形式来处理的(当然也可以采用动画来处理)。方式就是每隔一秒向UI线程发送重新绘制时钟控件的消息,因为时间会更新,所以指针也会对应移动。调用顺序为:

  @Override protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    drawPanel(canvas);
    drawScale(canvas);
    // 实现动画
    postDelayed(runnable, 1000);
  }

我们可以看下最终的效果

最后

上述实现中,其实还有很多优化的地方,我们注意到:为了实现秒针的移动,我们每隔一秒重新绘制了整个View,事实上,表盘和刻度是没有必要一直绘制的。因此,就造成了系统资源的浪费。基于这一点,我们应该考虑进行优化,比如将表盘独立出来,使用动画来操作秒针的移动等。感兴趣的读者可以尝试一下,需要注意的是,在判断哪种方式更优秀时,我们还需要以实际的测试结果来说明。

自定义控件在安卓中的地位极为重要,想要成为一个合格的安卓应用开发工程师,就必须熟练掌握自定义控件这个技能。

文章涉及代码github地址

发布了46 篇原创文章 · 获赞 21 · 访问量 7073

猜你喜欢

转载自blog.csdn.net/lotty_wh/article/details/104953082