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); } } }