一种实现极简番茄时钟的思路

概述

最近跟着扔物线的自定义View教程重新复习了一波基础,但是API这种东西如果不用很容易就忘了,趁大脑还没触发GC之前,最好的记忆方式就是撸个Demo出来。iOS上有一款个人很喜欢的简约TODO应用叫极简待办,其中它的番茄时钟交互很适合用来练手。

分析

先看下iOS的效果图长啥样

功能

  • 一个默认黑色的圆,一个灰色的圆,随着倒计时减少,灰色圆的弧度越来越大
  • 手指在圆圈内滑动可以调整时间

心路历程

一步步来,先画一个黑圆以及显示时间文本,传入一个时间值后,可以实现倒计时功能。实现倒计时需要用到CountDownTimer,因此只要在该类的onTick中不断重绘就行了

public void start(){
        new CountDownTimer(time * 1000 + 1000, 1000) {
            @Override
            public void onTick(long millisUntilFinished) {
                time = (time * 1000 - 1000) / 1000;
                textTime = time + "";
                invalidate();
            }
            @Override
            public void onFinish() {
            }
        }.start();
    }

通过setTime方法提前设置一分钟,运行后可以看到界面开始倒计时,

接着,开始倒计时后,画灰色的圆弧,这里需要用到动画ValueAnimator,通过ValueAnimator的getAnimatedValue()方法获取实时的动画值,计算灰色圆弧扫过的区域,修改start()方法如下:

public void start(){
    ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1.0f);
    valueAnimator.setDuration(time * 1000);
    valueAnimator.setInterpolator(new LinearInterpolator());
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            sweepVelocity = (float) animation.getAnimatedValue();
            invalidate();
        }
    });
    valueAnimator.start();
    new CountDownTimer(time * 1000 + 1000, 1000) {
        @Override
        public void onTick(long millisUntilFinished) {
            time = (time * 1000 - 1000) / 1000;
            textTime = time + "";
            invalidate();
        }
        @Override
        public void onFinish() {
        }
    }.start();
}

onDraw中:

mRectF.set(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
canvas.drawArc(mRectF, START_ANGLE, 360 * sweepVelocity, false, mPaint);

效果如下:

时间显示的有点挫,格式化下:

private String formatCountdownTime(int countdownTime) {
    StringBuilder sb = new StringBuilder();
    int minute = countdownTime / 60;
    int second = countdownTime - 60 * minute;
    if (minute < 10) {
        sb.append("0" + minute + ":");
    } else {
        sb.append(minute + ":");
    }
    if (second < 10) {
        sb.append("0" + second);
    } else {
        sb.append(second);
    }
    return sb.toString();
}

接下来就是实现上下滑动调整时间了,思路如下:

先确定一个最大值,这里取60分钟,我们可以获取到手指按下和滑动的坐标,如果这两个点的纵坐标差值刚好等于直径,那么显示的时间取最大值,其余的就在0-60的区间内,在滑动过程中执行重绘。

重写onTouchEvent方法:

@Override
public boolean onTouchEvent(MotionEvent event) {
    float x = event.getX();
    float y = event.getY();
    switch (event.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            touchX = x;
            touchY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            offsetX = x - touchX;
            offsetY = y - touchY;
            time = (int) (offsetY / 2 / radius * MAX_TIME);
            if (time <= 0) {
                time = 0;
            }
            textTime = formatTime(time);
            countdownTime = time * 60;
            invalidate();
            break;
    }
    return true;
}

运行效果如下:

时间是可以调整了,但是当前没有对滑动范围限制,看到最大时间超出60分钟,处理的思路很简单,就是判断点是否在圆内,那怎么判断触摸点是否在圆内呢?平面内两点间距离公式和半径比较即可。

private boolean isContained(float x, float y) {
    if (Math.sqrt((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY)) > radius) {
        return false;
    } else {
        return true;
    }
}

最终效果:

github传送门