Android自定义View实现时钟效果ClockView

来源:互联网 发布:java string 方法 编辑:程序博客网 时间:2024/05/17 01:06

1.废话不多说先上图



录制的可能略卡,三次时间图片是因为我分别点了下面三个按钮的效果,并非bug


2.分析

  • 观看上图咱们需要绘制的有:

    • 时针
    • 分针
    • 秒针
    • 时刻度
    • 秒刻度
    • 仪表盘上的数字
    • 上下午区分标识(AM/PM)
  • View与Surface的取舍:

    • View一般用于绘制静态页面或者界面元素跟随用户的操作(点击、拖拽等)而被动的改变位置、大小等
    • SurfaceView一般用于无需用户操作,界面元素就需要不断的刷新的情况(例如打飞机游戏不断移动的背景)
    • 通过以上两条可以确定SurfaceView正好符合我们的需求

3.SurfaceView注意事项

  • 如何使用SurfaceView

    • 1.继承SurfaceView
    • 2.实现SurfaceHolder.Callback接口
      • surfaceCreated:Surface创建后调用,一般做一些初始化工作
      • surfaceChanged:Surface状态发生变化时调用(例如大小)
      • surfaceDestroyed:Surface销毁时调用,一般在这里结束绘制线程
    • 3.SurfaceHolder:控制Surface的类,得到画布、提交画布、回调等
    • 4.绘制和逻辑
  • SurfaceView的写法

public class MyView extends SurfaceView implements SurfaceHolder.Callback,Runnable {    private SurfaceHolder mHolder;    private Thread mThread;    public MyView(Context context) {        this(context, null);    }    public MyView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        mHolder = getHolder();        mHolder.addCallback(this);        mThread = new Thread(this);    }    @Override    public void surfaceCreated(SurfaceHolder holder) {        mThread.start();    }    @Override    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {    }    @Override    public void surfaceDestroyed(SurfaceHolder holder) {    }    @Override    public void run() {        while (true) {            logic();            draw();        }    }    /**     * 逻辑操作     */    private void logic() {    }    /**     * 绘制操作     */    private void draw() {    }}

这是一般性写法,不过有些人应该已经发现了我们线程里跑的是一个while(true)的无限死循化,因此往往我们会增加一个标识位在surface销毁时置false。修改后部分代码如下:

private boolean flag;@Overridepublic void surfaceCreated(SurfaceHolder holder) {    flag = true;    mThread.start();}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {    flag = false;}@Overridepublic void run() {    while (flag) {        logic();        draw();    }}

当然了这样还是有缺陷的,因为当flag==true时,mThread里面的逻辑操作和绘制操作就在无限运行了。可想而知,如果是那样的话那么我们这里的时钟指针在你眼前飞速的转动,由于gif录制工具不够强大录下的动图根本开不出指针飞速旋转的效果这里就不提供图了,有兴趣的同学可以自己试一下,也可以脑补。。。。。。。。。。。
因此我们就需要对线程加以限制,具体如下:

@Overridepublic void run() {    long start, end;    while (flag) {        start = System.currentTimeMillis();        draw();        logic();        end = System.currentTimeMillis();        try {            if (end - start < 1000) {                Thread.sleep(1000 - (end - start));            }        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

这里的1000指的是每隔1S刷新一次界面,因为我们的始终的最小单位是秒


4.废话啰嗦完了 咱们开始实战

  • step1:通过2的分析,定义需要的属性
// 默认半径private static final int DEFAULT_RADIUS = 200;private SurfaceHolder mHolder;private Canvas mCanvas;private Thread mThread;private boolean flag;// 圆和刻度的画笔private Paint mPaint;// 指针画笔private Paint mPointerPaint;// 画布的宽高private int mCanvasWidth, mCanvasHeight;// 时钟半径private int mRadius = DEFAULT_RADIUS;// 秒针长度private int mSecondPointerLength;// 分针长度private int mMinutePointerLength;// 时针长度private int mHourPointerLength;// 时刻度长度private int mHourDegreeLength;// 秒刻度private int mSecondDegreeLength;// 时钟显示的时、分、秒private int mHour, mMinute, mSecond;
  • step2:初始化操作
public ClockView(Context context, AttributeSet attrs, int defStyleAttr) {    super(context, attrs, defStyleAttr);    mHour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);    mMinute = Calendar.getInstance().get(Calendar.MINUTE);    mSecond = Calendar.getInstance().get(Calendar.SECOND);    mHolder = getHolder();    mHolder.addCallback(this);    mThread = new Thread(this);    mPaint = new Paint();    mPointerPaint = new Paint();    mPaint.setColor(Color.BLACK);    mPaint.setAntiAlias(true);    mPaint.setStyle(Paint.Style.STROKE);    mPointerPaint.setColor(Color.BLACK);    mPointerPaint.setAntiAlias(true);    mPointerPaint.setStyle(Paint.Style.FILL_AND_STROKE);    mPointerPaint.setTextSize(22);    mPointerPaint.setTextAlign(Paint.Align.CENTER);    setFocusable(true);    setFocusableInTouchMode(true);}
  • step3:测量
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    int widthSize = MeasureSpec.getSize(widthMeasureSpec);    int widthMode = MeasureSpec.getMode(widthMeasureSpec);    int heightSize = MeasureSpec.getSize(heightMeasureSpec);    int heightMode = MeasureSpec.getMode(heightMeasureSpec);    int desiredWidth, desiredHeight;    if (widthMode == MeasureSpec.EXACTLY) {        desiredWidth = widthSize;    } else {        desiredWidth = mRadius * 2 + getPaddingLeft() + getPaddingRight();        if (widthMode == MeasureSpec.AT_MOST) {            desiredWidth = Math.min(widthSize, desiredWidth);        }    }    if (heightMode == MeasureSpec.EXACTLY) {        desiredHeight = heightSize;    } else {        desiredHeight = mRadius * 2 + getPaddingTop() + getPaddingBottom();        if (heightMode == MeasureSpec.AT_MOST) {            desiredHeight = Math.min(heightSize, desiredHeight);        }    }    // +4是为了设置默认的2px的内边距,因为绘制时钟的圆的画笔设置的宽度是2px    setMeasuredDimension(mCanvasWidth = desiredWidth + 4, mCanvasHeight = desiredHeight + 4);    mRadius = (int) (Math.min(desiredWidth - getPaddingLeft() - getPaddingRight(),            desiredHeight - getPaddingTop() - getPaddingBottom()) * 1.0f / 2);    calculateLengths();}/** * 计算指针和刻度长度 */private void calculateLengths() {    // 这里我们定义时刻度长度为半径的1/7    mHourDegreeLength = (int) (mRadius * 1.0f / 7);    // 秒刻度长度为时刻度长度的一半    mSecondDegreeLength = (int) (mHourDegreeLength * 1.0f / 2);    // 时针长度为半径一半    // 指针长度比 hour : minute : second = 1 : 1.25 : 1.5    mHourPointerLength = (int) (mRadius * 1.0 / 2);    mMinutePointerLength = (int) (mHourPointerLength * 1.25f);    mSecondPointerLength = (int) (mHourPointerLength * 1.5f);}

测量的前面部分代码基本都是一个模式来写的,需要注意的是当测量模式不是Exectly时的处理,想要了解这一块同学的可以去鸿神的博客学习

  • step4:绘制
/** * 绘制,这部分代码基本固定 */private void draw() {    try {        mCanvas = mHolder.lockCanvas(); // 得到画布        if (mCanvas != null) {            // 在这里绘制内容        }    } catch (Exception e) {        e.printStackTrace();    } finally {        if (mCanvas != null) {            // 提交画布,否则什么都看不见            mHolder.unlockCanvasAndPost(mCanvas);        }    }}现在开始具体的绘制内容(画什么由画布决定,怎么画由画笔决定,这也就是我们上面给画笔设置一系列属性的原因):// 1.将坐标系原点移至去除内边距后的画布中心// 默认在画布左上角,这样做是为了更方便的绘制mCanvas.translate(mCanvasWidth * 1.0f / 2 + getPaddingLeft() - getPaddingRight(),mCanvasHeight * 1.0f / 2 + getPaddingTop() - getPaddingBottom());// 2.绘制圆盘mPaint.setStrokeWidth(2f); // 画笔设置2个像素的宽度mCanvas.drawCircle(0, 0, mRadius, mPaint); // 到这一步就能知道第一步的好处了,否则害的去计算园的中心点坐标// 3.绘制时刻度for (int i = 0; i < 12; i++) {    mCanvas.drawLine(0, mRadius, 0, mRadius - mHourDegreeLength, mPaint);    mCanvas.rotate(30); // 360°平均分成12份,每份30°}// 4.绘制秒刻度mPaint.setStrokeWidth(1.5f);for (int i = 0; i < 60; i++) {    //时刻度绘制过的区域不在绘制    if (i % 5 != 0) {        mCanvas.drawLine(0, mRadius, 0, mRadius - mSecondDegreeLength, mPaint);    }    mCanvas.rotate(6); // 360°平均分成60份,每份6°}// 5.绘制数字mPointerPaint.setColor(Color.BLACK);for (int i = 0; i < 12; i++) {    String number = 6 + i < 12 ? String.valueOf(6 + i) : (6 + i) > 12            ? String.valueOf(i - 6) : "12";    mCanvas.drawText(number, 0, mRadius * 5.5f / 7, mPointerPaint);    mCanvas.rotate(30);}// 6.绘制上下午mCanvas.drawText(mHour < 12 ? "AM" : "PM", 0, mRadius * 1.5f / 4, mPointerPaint);// 7.绘制时针Path path = new Path();path.moveTo(0, 0);int[] hourPointerCoordinates = getPointerCoordinates(mHourPointerLength);path.lineTo(hourPointerCoordinates[0], hourPointerCoordinates[1]);path.lineTo(hourPointerCoordinates[2], hourPointerCoordinates[3]);path.lineTo(hourPointerCoordinates[4], hourPointerCoordinates[5]);path.close();mCanvas.save();mCanvas.rotate(180 + mHour % 12 * 30 + mMinute * 1.0f / 60 * 30);mCanvas.drawPath(path, mPointerPaint);mCanvas.restore();// 8.绘制分针path.reset();path.moveTo(0, 0);int[] minutePointerCoordinates = getPointerCoordinates(mMinutePointerLength);path.lineTo(minutePointerCoordinates[0], minutePointerCoordinates[1]);path.lineTo(minutePointerCoordinates[2], minutePointerCoordinates[3]);path.lineTo(minutePointerCoordinates[4], minutePointerCoordinates[5]);path.close();mCanvas.save();mCanvas.rotate(180 + mMinute * 6);mCanvas.drawPath(path, mPointerPaint);mCanvas.restore();// 9.绘制秒针mPointerPaint.setColor(Color.RED);path.reset();path.moveTo(0, 0);int[] secondPointerCoordinates = getPointerCoordinates(mSecondPointerLength);path.lineTo(secondPointerCoordinates[0], secondPointerCoordinates[1]);path.lineTo(secondPointerCoordinates[2], secondPointerCoordinates[3]);path.lineTo(secondPointerCoordinates[4], secondPointerCoordinates[5]);path.close();mCanvas.save();mCanvas.rotate(180 + mSecond * 6);mCanvas.drawPath(path, mPointerPaint);mCanvas.restore();这里比较难的可能就是指针的绘制,因为我们的指针是个规则形状,其中getPointerCoordinates便是得到这个不规则形状的3个定点坐标,有兴趣的同学可以去研究一下我的逻辑,也可以定义你自己的逻辑。我的逻辑如下(三角函数学的号的同学应该一眼就能看懂):/** * 获取指针坐标 * * @param pointerLength 指针长度 * @return int[]{x1,y1,x2,y2,x3,y3} */private int[] getPointerCoordinates(int pointerLength) {    int y = (int) (pointerLength * 3.0f / 4);    int x = (int) (y * Math.tan(Math.PI / 180 * 5));    return new int[]{-x, y, 0, pointerLength, x, y};}
  • step5:逻辑
    这里逻辑可想而知每个秒,秒数+1到60的时候归0,同时分钟数+1,
    分钟数到60的时候归0,小时数+1,小时数到24的时候归0.
/** * 逻辑 */private void logic() {    mSecond++;    if (mSecond == 60) {        mSecond = 0;        mMinute++;        if (mMinute == 60) {            mMinute = 0;            mHour++;            if (mHour == 24) {                mHour = 0;            }        }    }}
  • step6:装逼的时刻到了
    运行来看一下效果

    我靠什么情况为什么画出来是这么个东西

这里就是要值得大家注意的了,使用surface绘制的时候一定要刷屏,所谓的刷屏在每次绘制前画一个什么都没有图层在上次画出来的东西上面盖住再去画本次实现也很简单只需要在绘制的第一行加上一句就行。
我这里是

//刷屏
mCanvas.drawColor(Color.WHITE);

这里的背景也可以通过自定义属性来自定义的。不知道的同学仍然可以去鸿神的博客学习


5.总结

主要涉及知识点:

  • View和SurfaceView的取舍
  • SurfaceView的使用
  • Canvas使用
  • 坐标系的灵活运用

6.Tips

1.图上的点击按钮改变时间和改变textView上的时候只是定义了一个回调接口,这里不再赘述
2.详情见源码
3.本文同步发布简书

1 0