SurfaceView打造自定义时钟ClockView

从事Android开发也一段时间了,一直有作云笔记的习惯,可是博客不怎么写。最近给本身定了个计划,坚持每周至少写三个自定义控件,所谓熟能生巧呀。做为第一篇写的博客,给你们带来用SurfaceView打造的自定义时钟。PS:最近看见洋神的一篇推送,有人写了这个,本身看着效果图就写了哈哈,至少效果感受还阔以O(∩_∩)Ojava


先看下这炫酷的效果哈哈(时钟上的横线是录制的缘由,效果是没有那根线的,不影响哈):git



先简单介绍一下surfaceView吧,surfaceView区别于普通的View,其拥有如下特色:github

①拥有独立的绘图表面。canvas

②surfaceView须要在宿主上挖一个洞来显示本身。框架

③其的UI绘制能够在独立的进程中进行,使用双缓冲机制,不会阻塞主线程的UI操做。ide


那么,喜欢刨根问底的朋友们又会有疑问了,到底什么是双缓冲机制?
函数

在surfaceView里面,所谓的双缓冲机制其实就是说SurfaceView分front和back两块画布,这两块画布会相互交替来显示,每post一次交替一次,交替后显示在surfaceView上面。(这只是查阅资料以后的粗略理解),有兴趣的朋友能够本身深刻研究哈O(∩_∩)Opost


那么为何要使用双缓冲机制?学习

那是为了解决某种状况下(例如游戏场景)UI反复的局部刷新带来的闪烁问题。this


好了,基本知识点已经介绍完毕,我们开始撸码咯\(^o^)/~


首先,要使用surfaceView,一些基本的代码使有套路的。一般一个surfaceView的基本框架以下:

public class ClockSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
    private Canvas mCanvas;
    private Paint mPaint;
    private boolean isDrawing;
    private SurfaceHolder mHolder;


    public ClockSurfaceView(Context context) {
        super(context);
        init();
    }

    public ClockSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mHolder = getHolder();
        mHolder.addCallback(this);
        setFocusable(true);
        setFocusableInTouchMode(true);
        setKeepScreenOn(true);

        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.STROKE);
    }


    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {

    }

    private void draw() {
        try {
            mCanvas = mHolder.lockCanvas();
            //draw sth
        } catch (Exception e) {

        } finally {
            if (mCanvas != null) mHolder.unlockCanvasAndPost(mCanvas);//这里确保每次都能提交
        }
    }

    @Override
    public void run() {
        if (isDrawing) {
            draw();
        }
    }
}
说时迟,那时快,基本框架的代码已经撸完。毕竟做为LOL青铜段的大神,这手速,不谈了微笑

那么如今就能够开始画啦:

首先,咱们先把时钟的圆盘画出来,代码很简单:

mCanvas.drawColor(Color.WHITE);//注意必定要刷屏
mPaint.setColor(Color.BLACK);
mCanvas.drawCircle(getWidth() / 2, getHeight() / 2, mRadius, mPaint);

而后是画刻度和数字:

注意:这里画刻度和数字就须要捡回来高中的三角函数的数学了,做为曾经的数学课表明,so easy啦偷笑

弧度的图形表示:


求坐标点并修正坐标系的三角函数公式:
 
 
private float calculateX(int radius, double radian) {
    return (float) (radius * Math.cos(radian)) + mCenterX;
}

private float calculateY(int radius, double radian) {
    return -(float) (radius * Math.sin(radian)) + mCenterY;
}

大致思想,咱们先考虑,一个圆盘有60个小格,那么每一个小格天然占的弧度是
 
 
public static final double= 2 * Math.PI / 60;

又因为我打算从12这个数字开始顺时针画一周,所以起始弧度为HALF_PI=Math.PI / 2;
须要注意的是,咱们的整点的格子会有数字而且刻度线会加粗处理,所以,咱们用currentPosition来记录当前所画的是第几个刻度,每5的整倍数刻度线就会加粗加长处理(好羞涩)
 
 
/**
     * 画刻度和数字
     */
    private void drawScaleAndNum() {
        for (double i = HALF_PI; i > -1.5 * Math.PI; i -= intervalRadian) {
            currentPosition += 1;
            float outCircleX = calculateX(mRadius, i);
            float outCircleY = calculateY(mRadius, i);//弧度的Y轴和Android的Y轴方向相反,所以取反
            float inCircleX;//从圆盘上的点画起,肯定终点的位置圆,从而画刻度线
            float intCircleY;
            if ((currentPosition - 1) / 5 < 12 && (currentPosition - 1) % 5 == 0) {//整点
                inCircleX = calculateX(mRadius - DensityUtil.dip2px(mContext, 12.5f), i);
                intCircleY = calculateY(mRadius - DensityUtil.dip2px(mContext, 12.5f), i);
                int hourNum = (currentPosition - 1) / 5 == 0 ? 12 : (currentPosition - 1) / 5;
                drawNum(mCanvas, hourNum + "", calculateX(mRadius - DensityUtil.dip2px(mContext, 30), i), calculateY(mRadius - DensityUtil.dip2px(mContext, 30), i));
                mPaint.setStrokeWidth(4);//整点的刻度线加粗
            } else {
                inCircleX = calculateX(mRadius - DensityUtil.dip2px(mContext, 5), i);
                intCircleY = calculateY(mRadius - DensityUtil.dip2px(mContext, 5), i);
                mPaint.setStrokeWidth(2);
            }
            mCanvas.drawLine(outCircleX, outCircleY, inCircleX, intCircleY, mPaint);
        }
        currentPosition = 0;
    }
画数字的方法:
private void drawNum(Canvas canvas, String text, float textX, float textY) {
        mPaint.setTextSize(DensityUtil.dip2px(mContext, 12));
        float textWidth = mPaint.measureText(text);
        canvas.drawText(text, textX - textWidth / 2, textY + DensityUtil.dip2px(mContext, 4), mPaint);//绘制数字并修正数字的位置
    }

而后是画时,分,秒针:
private void drawClockHand() {

        mCanvas.drawLine(mCenterX, mCenterY, calculateX(SecondHandLength, secondRadian + HALF_PI), calculateY(SecondHandLength, secondRadian + HALF_PI), mPaint);
        mPaint.setColor(Color.BLUE);
        mCanvas.drawLine(mCenterX, mCenterY, calculateX(MinuteHandLength, secondRadian / 60 + HALF_PI), calculateY(MinuteHandLength, secondRadian / 60 + HALF_PI), mPaint);

        mPaint.setColor(Color.RED);
        //注意,这里除以的是720不是3600,由于时针走一小格为12分钟,至关于分针走12格,秒针走720格
        mCanvas.drawLine(mCenterX, mCenterY, calculateX(hourHandLength, secondRadian / 720 + HALF_PI), calculateY(hourHandLength, secondRadian / 720 + HALF_PI), mPaint);
    }


好啦,基本的轮廓已经画完:
 
 
咱们让它跑起来吧:
在Run方法里面不断地更新当前时间:
 
 
/**
 * 更新当前时间和刻度值
 */
private void updateCurrentTime() {
    mFormat.format(new Date());
    mHour = mFormat.getCalendar().get(Calendar.HOUR_OF_DAY);
    mMinute = mFormat.getCalendar().get(Calendar.MINUTE);
    mSecond = mFormat.getCalendar().get(Calendar.SECOND);
    int totalS = mHour * 3600 + mMinute * 60 + mSecond;
    secondRadian = -totalS * intervalRadian;
}


更新完以后,再进行绘制。并计算绘制用时,若是不足一秒,经过sleep让其睡眠,确保
咱们每秒刷新一次。
 
 
@Override
public void run() {
    while (mIsDrawing && sizeChanged) {
        try {
            updateCurrentTime();
            long drawBefore = System.currentTimeMillis();
            draw();
            long drawTime = System.currentTimeMillis() - drawBefore;
            if (drawTime < 1000) {
                Thread.sleep(1000 - drawTime);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
附上定义的成员变量:
private SurfaceHolder mHolder;
    private Canvas mCanvas;
    private boolean mIsDrawing;
    private Paint mPaint;
    private int mRadius;
    private int currentPosition = 0;//记录目前所画第几个刻度点,从3点开始逆时针
    private int mCenterX;
    private int mCenterY;
    private int hourHandLength;
    private int MinuteHandLength;
    private int SecondHandLength;
    private double secondRadian;
    private boolean sizeChanged;
    private int mMinute;
    private int mSecond;
    private int mHour;
    private SimpleDateFormat mFormat;
    public static final double intervalRadian = 2 * Math.PI / 60;
    private static final double HALF_PI = Math.PI / 2;
    private Context mContext;

至此,咱们就大功告成啦。跑起来的效果就如开篇所示,有兴趣的朋友不妨试试O(∩_∩)O,第一次写博客,有不足的地方,欢迎指出交流学习 大笑
附上项目源码地址:点击下载源码