android custom view draw a clock

In this article, we will customize a View to implement a clock. Let's take a look at the rendering first.
Here is just a static picture. In fact, the second hand is moving. As for other effects, you can add them on your own.

1. Attribute setting

Create a new file attrs. In the res/values directory XML, in which we will define the attributes required by the clock. Here, I only defined four attributes, namely hour, minute, second and background color.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="WatchView">
        <attr name="hour" format="integer"/>
        <attr name="min" format="integer"/>
        <attr name="sec" format="integer"/>
        <attr name="background_color" format="color"/>
    </declare-styleable>
</resources>

2. Customize WatchView

Create a new class WatchView, which inherits from View, and then add members such as brush, pointer number, length, etc. In the constructor, we get the attribute value defined in the layout file.

public class WatchView extends View {
    private static final String TAG = "WatchView";

    private Paint mPaint;

    private int mCircleWidth = 20;
    private int radius = 500;
    // These will be recalculated below
    private int lenHour = 200;
    private int lenMin = 300;
    private int lenSec = 400;

    private int mProgressSecond = 0;
    private int mProgressMin = 30;
    private int mProgressHour = 11;

    // background
    private int mBackgroundColor;
    public WatchView(Context context) {
        this(context,null);
    }

    public WatchView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);

    }

    public WatchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        // Get the attribute value defined in the layout file and convert it
        TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.WatchView);
        mProgressHour = array.getInteger(R.styleable.WatchView_hour,0);
        mProgressHour %= 12;
        mProgressMin = array.getInteger(R.styleable.WatchView_min,0);
        mProgressMin %= 60;
        mProgressSecond = array.getInteger(R.styleable.WatchView_sec,0);
        mProgressSecond %= 60;
        mBackgroundColor = array.getColor(R.styleable.WatchView_background_color,getResources().getColor(R.color.white));
        // Don't forget to recycle
        array.recycle();

    }
    
}

Next, initialize the attributes of the brush.

    private void Init(){
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setColor(getResources().getColor(R.color.black));
        mPaint.setStrokeWidth(10); 
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(80);
        mPaint.setTextAlign(Paint.Align.CENTER);

    }

The meaning of the above attributes is relatively simple. If you don't understand it, you can see it from the source code.

The custom View also needs to handle wrap_content and match_ In the source code of View, the two attributes of parent are treated the same, so you will see that the View defined by yourself is set to wrap_content and match_ The size of the parent is the same, so we need to wrap it_ Set a value for content, and you can determine it yourself.

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSepcSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        // It inherits from the custom View and needs to be wrapped_ Content this mode specifies a specific value, otherwise its size and match_ There is no difference between parents
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(500,500);
        }else if (widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(500,heightSpecSize);
        }else if (heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSepcSize,500);
        }

    }

Next, we need to initialize the attributes defined at the beginning, such as the radius of the disk and the length of the pointer. Here, the radius is determined according to the length and width of the whole view. Where do we need to determine these values? If we set it in the initialization function or onMeasure function, we will find that the getWidth size is 0, because the size of the view has not been determined at this time. If we put all these calculations in onDraw, the rendering efficiency of the view will become very low, because the onDraw function will be called every refresh, Therefore, too much work should not be done in it, so these work can be done in the onLayout function.

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        // Initialization radius, pointer length, etc. these calculations cannot be completed in the initialization function and onMeasure, because the width has not been determined at that time, getWidth = 0
        // Don't put it in onDraw, because onDraw calls are frequent. It's best not to put too many operations in it
        int w = getWidth() - getPaddingLeft() - getPaddingRight();
        int h = getHeight() - getPaddingTop() - getPaddingBottom();
        if (w > h){
            radius = h / 2;
        }else {
            radius = w / 2;
        }
        // Determine pointer length
        lenSec = (int) ((float)(radius) * 3 / 4);
        lenMin = (int) ((float)(radius) / 2);
        lenHour = (int) ((float)(radius) / 4);
    }

3. Start drawing

Start our drawing work in the onDraw function. First, draw the dial and background color

        int center;
        if (getWidth() > getHeight()){
            center = getHeight() / 2;
        }else {
            center = getWidth() / 2;
        }
        // Center coordinates
        float xCenter = center;
        float yCenter = center;
        // Table frame
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(mCircleWidth);
        mPaint.setColor(getResources().getColor(R.color.black));
        canvas.drawCircle(center,center,radius,mPaint);
        // Draw a small circle in the center of rotation of the pointer
        canvas.drawCircle(xCenter,yCenter,10,mPaint);
        // Draw background
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mBackgroundColor);
        canvas.drawCircle(center,center,radius - mCircleWidth / 2,mPaint);
        mPaint.setStrokeWidth(mCircleWidth);
        mPaint.setColor(getResources().getColor(R.color.black));

Next, draw the scale on the dial. The large scale and small scale should be processed separately. A total of 60 scales are drawn. When calculating, I calculate clockwise from 0 o'clock according to the number of the clock, and the interval between each scale is 6 degrees.

        // Draw 12 large and other small scales
        for (int i = 0; i < 60; i++) {
            // Large scale
            int len = 0;
            if (i%5 == 0){
                len = 30;
                mPaint.setStrokeWidth(20);
            }else {
                len = 20;
                mPaint.setStrokeWidth(10);
            }
            double hudu = i * 6 * Math.PI / 180;
            double sin1 = Math.sin(hudu);
            double cos1 = Math.cos(hudu);
            float xEnd = (float) (radius * sin1)+xCenter;
            float yEnd = -(float) (radius * cos1)+yCenter;

            float xStart = (float) ((radius-len) * sin1)+xCenter;
            float yStart = -(float) ((radius-len) * cos1)+yCenter;

            canvas.drawLine(xStart,yStart,xEnd,yEnd,mPaint);
        }

Next, draw the hour hand, minute hand, second, etc. the coordinate calculation formulas are similar, but it seems difficult to understand. For example, when calculating the hour hand angle, if it is now 8 o'clock and starts with 12 o'clock as 0 degrees, its angle should be 8 / 12 * 360, but it is hour. We use mProgressHour. It is an integer and 8 / 12 is 0, Unless we first convert it to float type, so I adjusted the calculation order during calculation, and I need to figure it out for myself.

        /**
         * Next, when I calculate radians, the formula looks chaotic, because the hours, minutes and seconds are of type int,
         * For example, when calculating mProgressMin/60, we expect the result to be 0.5, but it is actually 0, so I adjusted the calculation order,
         * You can also convert the type before calculating
         */
        // Draw second hand
        mPaint.setStrokeWidth(10);
        double sec = mProgressSecond * 360 * Math.PI / 60 / 180;
        float sStart = (float) (lenSec * Math.sin(sec)) + xCenter;
        float sEnd = - (float) (lenSec * Math.cos(sec)) + yCenter;
        canvas.drawLine(xCenter, yCenter, sStart, sEnd, mPaint);

        // Draw minute hand
        mPaint.setStrokeWidth(20);
        double min = mProgressMin * 360 * Math.PI / 60 / 180;
        float mStart = (float) (lenMin * Math.sin(min)) + xCenter;
        float mEnd = - (float) (lenMin * Math.cos(min)) + yCenter;
        canvas.drawLine(xCenter, yCenter, mStart, mEnd, mPaint);

        // Draw the hour hand, which should be offset by a certain angle according to the minute hand
        mPaint.setStrokeWidth(30);
        // First calculate the occupied angle mProgressHour / 12 * 360, and then calculate radian / 180 * math PI
        double hour = mProgressHour * 360 * Math.PI / 12 / 180;
        // This causes the clockwise to shift by a certain angle
        hour += mProgressMin * 30 * Math.PI / 180 / 60;
        float hStart = (float) (lenHour * Math.sin(hour)) + xCenter;
        float hEnd = - (float) (lenHour * Math.cos(hour)) + yCenter;
        canvas.drawLine(xCenter, yCenter, hStart, hEnd, mPaint);

The last is to draw the numbers on the dial. Here, the numbers need to be displayed in the middle. Refer to the specific drawing principle and display mode

https://www.jianshu.com/p/8b97627b21c4

        // Draw numbers
        mPaint.setStrokeWidth(radius/60);
        mPaint.setTextSize((float) (radius / 3.5));
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextAlign(Paint.Align.CENTER);
        // When you draw a number, you should center it by a certain amount
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float distance = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;
        for (int i = 0; i < 12; i++) {
            double d = (i+1) * 30 * Math.PI / 180;
            float x = (float) ((radius - 100) * Math.sin(d) + xCenter);
            float y = (float) (-(radius - 100) * Math.cos(d) + yCenter);
            float baseline = y + distance;

            canvas.drawText(String.valueOf(i+1),x,baseline,mPaint);
        }

The drawing work is finished here. Next, start a thread and refresh the data every second.

        // Drawing thread
        new Thread(){
            @Override
            public void run() {
                while (true){
                    mProgressSecond +=1;
                    if (mProgressSecond == 60){
                        mProgressSecond = 0;
                        mProgressMin += 1;
                        // Processing hour
                        if (mProgressMin == 60){
                            mProgressMin = 0;
                            mProgressHour += 1;
                        }
                    }
                    // Redraw
                    postInvalidate();
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

Finally, attach all the codes.

public class WatchView extends View {
    private static final String TAG = "WatchView";

    private Paint mPaint;

    private int mCircleWidth = 20;
    private int radius = 500;
    // These will be recalculated below
    private int lenHour = 200;
    private int lenMin = 300;
    private int lenSec = 400;

    private int mProgressSecond = 0;
    private int mProgressMin = 30;
    private int mProgressHour = 11;

    // background
    private int mBackgroundColor;
    public WatchView(Context context) {
        this(context,null);
    }

    public WatchView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);

    }

    public WatchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        Init();
        TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.WatchView);
        mProgressHour = array.getInteger(R.styleable.WatchView_hour,0);
        mProgressHour %= 12;
        mProgressMin = array.getInteger(R.styleable.WatchView_min,0);
        mProgressMin %= 60;
        mProgressSecond = array.getInteger(R.styleable.WatchView_sec,0);
        mProgressSecond %= 60;
        mBackgroundColor = array.getColor(R.styleable.WatchView_background_color,getResources().getColor(R.color.white));
        array.recycle();

    }

    public int getmProgressSecond() {
        return mProgressSecond;
    }

    public void setmProgressSecond(int mProgressSecond) {
        this.mProgressSecond = mProgressSecond % 60;
    }

    public int getmProgressMin() {
        return mProgressMin;
    }

    public void setmProgressMin(int mProgressMin) {
        this.mProgressMin = mProgressMin % 60;
    }

    public int getmProgressHour() {
        return mProgressHour;
    }

    public void setmProgressHour(int mProgressHour) {
        this.mProgressHour = mProgressHour % 12;
    }

    private void Init(){
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setColor(getResources().getColor(R.color.black));
        mPaint.setStrokeWidth(10);  // Ring width 10
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(100);
        mPaint.setTextAlign(Paint.Align.CENTER);

        // Drawing thread
        new Thread(){
            @Override
            public void run() {
                while (true){
                    mProgressSecond +=1;
                    if (mProgressSecond == 60){
                        mProgressSecond = 0;
                        mProgressMin += 1;
                        // Processing hour
                        if (mProgressMin == 60){
                            mProgressMin = 0;
                            mProgressHour += 1;
                        }
                    }

                    // Redraw
                    postInvalidate();
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSepcSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        // It inherits from the custom View and needs to be wrapped_ Content this mode specifies a specific value, otherwise its size and match_ There is no difference between parents
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(500,500);
        }else if (widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(500,heightSpecSize);
        }else if (heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSepcSize,500);
        }

    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        // Initialization radius, pointer length, etc. these calculations cannot be completed in the initialization function and onMeasure, because the width has not been determined at that time, getWidth = 0
        // Don't put it in onDraw, because onDraw calls are frequent. It's best not to put too many operations in it
        int w = getWidth() - getPaddingLeft() - getPaddingRight();
        int h = getHeight() - getPaddingTop() - getPaddingBottom();
        if (w > h){
            radius = h / 2;
        }else {
            radius = w / 2;
        }
        // Determine pointer length
        lenSec = (int) ((float)(radius) * 3 / 4);
        lenMin = (int) ((float)(radius) / 2);
        lenHour = (int) ((float)(radius) / 4);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int center;
        if (getWidth() > getHeight()){
            center = getHeight() / 2;
        }else {
            center = getWidth() / 2;
        }
        // Center coordinates
        float xCenter = center;
        float yCenter = center;
        // Table frame
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(mCircleWidth);
        mPaint.setColor(getResources().getColor(R.color.black));
        canvas.drawCircle(center,center,radius,mPaint);
        // Draw a small circle in the center of rotation of the pointer
        canvas.drawCircle(xCenter,yCenter,10,mPaint);
        // Draw background
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mBackgroundColor);
        canvas.drawCircle(center,center,radius - mCircleWidth / 2,mPaint);
        mPaint.setStrokeWidth(mCircleWidth);
        mPaint.setColor(getResources().getColor(R.color.black));


        // Draw 12 large and other small scales
        for (int i = 0; i < 60; i++) {
            // Large scale
            int len = 0;
            if (i%5 == 0){
                len = 30;
                mPaint.setStrokeWidth(20);
            }else {
                len = 20;
                mPaint.setStrokeWidth(10);
            }
            double hudu = i * 6 * Math.PI / 180;
            double sin1 = Math.sin(hudu);
            double cos1 = Math.cos(hudu);
            float xEnd = (float) (radius * sin1)+xCenter;
            float yEnd = -(float) (radius * cos1)+yCenter;

            float xStart = (float) ((radius-len) * sin1)+xCenter;
            float yStart = -(float) ((radius-len) * cos1)+yCenter;

            canvas.drawLine(xStart,yStart,xEnd,yEnd,mPaint);
        }
        /**
         * Next, when I calculate radians, the formula looks chaotic, because the hours, minutes and seconds are of type int,
         * For example, when calculating mProgressMin/60, we expect the result to be 0.5, but it is actually 0, so I adjusted the calculation order,
         * You can also convert the type before calculating
         */
        // Draw second hand
        mPaint.setStrokeWidth(10);
        double sec = mProgressSecond * 360 * Math.PI / 60 / 180;
        float sStart = (float) (lenSec * Math.sin(sec)) + xCenter;
        float sEnd = - (float) (lenSec * Math.cos(sec)) + yCenter;
        canvas.drawLine(xCenter, yCenter, sStart, sEnd, mPaint);

        // Draw minute hand
        mPaint.setStrokeWidth(20);
        double min = mProgressMin * 360 * Math.PI / 60 / 180;
        float mStart = (float) (lenMin * Math.sin(min)) + xCenter;
        float mEnd = - (float) (lenMin * Math.cos(min)) + yCenter;
        canvas.drawLine(xCenter, yCenter, mStart, mEnd, mPaint);

        // Draw the hour hand, which should be offset by a certain angle according to the minute hand
        mPaint.setStrokeWidth(30);
        // First calculate the occupied angle mProgressHour / 12 * 360, and then calculate radian / 180 * math PI
        double hour = mProgressHour * 360 * Math.PI / 12 / 180;
        // This causes the clockwise to shift by a certain angle
        hour += mProgressMin * 30 * Math.PI / 180 / 60;
        float hStart = (float) (lenHour * Math.sin(hour)) + xCenter;
        float hEnd = - (float) (lenHour * Math.cos(hour)) + yCenter;
        canvas.drawLine(xCenter, yCenter, hStart, hEnd, mPaint);

        // Draw numbers
        mPaint.setStrokeWidth(radius/60);
        mPaint.setTextSize((float) (radius / 3.5));
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextAlign(Paint.Align.CENTER);
        // When drawing a number, in order to center the number, a certain offset should be given to the y coordinate
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float distance = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;
        for (int i = 0; i < 12; i++) {
            double d = (i+1) * 30 * Math.PI / 180;
            float x = (float) ((radius - 100) * Math.sin(d) + xCenter);
            float y = (float) (-(radius - 100) * Math.cos(d) + yCenter);
            float baseline = y + distance;

            canvas.drawText(String.valueOf(i+1),x,baseline,mPaint);
        }

    }

}

Keywords: Java Android

Added by Jtech on Sat, 12 Feb 2022 04:15:02 +0200