Effect display:
Source of inspiration:
Let’s get straight to the point:
1. Step 1: Create a customView
inheritanceView
, implement the constructor as follows
public WatchBoard(Context context) {
this(context, null);
}
public WatchBoard(Context context, AttributeSet attrs) {
super(context, attrs);
}Copy the code
2. Add some necessary attributes, and customize the resource file, write code to obtain the attributes
Some required attributes, we can see from the example diagram that we need to customize and necessary attributes are mainly the following
private float mRadius;
private float mPadding;
private float mTextSize;
private float mHourPointWidth;
private float mMinutePointWidth;
private float mSecondPointWidth;
private int mPointRadius;
private float mPointEndLength;
private int mColorLong;
private int mColorShort;
private int mHourPointColor;
private int mMinutePointColor;
private int mSecondPointColor;
private Paint mPaint; Copy the code
About the role of each attribute also write, before looking at other people’s custom View some attributes do not know what to do, difficult to understand:
Define resource file: invalue
New Under Filewatch_board_attr.xml
File, as follows
Copy the code
Constructor gets properties and sets default values, adds exception handling (if an exception occurs, use all defaults)
public WatchBoard(Context context, AttributeSet attrs) { super(context, attrs) obtainStyledAttrs(attrs) } private void obtainStyledAttrs(AttributeSet attrs) { TypedArray array = null try { array = getContext().obtainStyledAttributes(attrs, R.styleable.WatchBoard) mPadding = array.getDimension(R.styleable.WatchBoard_wb_padding, DptoPx(10)) mTextSize = array.getDimension(R.styleable.WatchBoard_wb_text_size, SptoPx(16)) mHourPointWidth = array.getDimension(R.styleable.WatchBoard_wb_hour_pointer_width, DptoPx(5)) mMinutePointWidth = array.getDimension(R.styleable.WatchBoard_wb_minute_pointer_width, DptoPx(3)) mSecondPointWidth = array.getDimension(R.styleable.WatchBoard_wb_second_pointer_width, DptoPx(2)) mPointRadius = (int) array.getDimension(R.styleable.WatchBoard_wb_pointer_corner_radius, DptoPx(10)) mPointEndLength = array.getDimension(R.styleable.WatchBoard_wb_pointer_end_length, DptoPx(10)) mColorLong = array.getColor(R.styleable.WatchBoard_wb_scale_long_color, Color.argb(225, 0, 0, 0)) mColorShort = array.getColor(R.styleable.WatchBoard_wb_scale_short_color, Color.argb(125, 0, 0, 0)) mMinutePointColor = array.getColor(R.styleable.WatchBoard_wb_minute_pointer_color, Color.BLACK) mSecondPointColor = array.getColor(R.styleable.WatchBoard_wb_second_pointer_color, Color.red)} catch (Exception e) {// Use the default values mPadding = DptoPx(10) mTextSize = SptoPx(16) mHourPointWidth = DptoPx(5) mMinutePointWidth = DptoPx(3) mSecondPointWidth = DptoPx(2) mPointRadius = (int) DptoPx(10) mPointEndLength = DptoPx(10) mColorLong = Color.argb(225, 0, 0, 0) mColorShort = Color.argb(125, 0, 0, 0) mMinutePointColor = Color.BLACK mSecondPointColor = Color.RED } finally { if (array ! = null) { array.recycle() } } }Copy the code
The dimension conversion method used
private float DptoPx(int value) {
return SizeUtil.Dp2Px(getContext(), value);
}
private float SptoPx(int value) {
return SizeUtil.Sp2Px(getContext(), value);
}Copy the code
SizeUtil
See the blog for tools:Custom View size conversion
3. Initialize the brush
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
}Copy the code
The current constructor looks like this
public WatchBoard(Context context, AttributeSet attrs) {
super(context, attrs);
obtainStyledAttrs(attrs);
init();
}Copy the code
4. Because the dial is always displayed as a circle, to make the graph is always inview
It’s easy in the middle, but that would waste a lot of space, so we should rewrite itonMeasure
Method, so that the dial will always take up only one square of space, but the premise is that the user must give a certain value, whether it is width or height or both.
Treatment idea:
1. When the width and height arewrap_content
Throw an exception because such an operation is not appropriate for the component
2. Set the initial width to a large value, and take the minimum value when the width or height is specified. Since one of the values must be specified, the minimum width and height will be obtained later
The code is as follows:
Custom exception
class NoDetermineSizeException extends Exception { public NoDetermineSizeException(String message) { super(message); }}Copy the code
onMeasure
Methods:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = 1000; int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED || heightMeasureSpec == MeasureSpec.AT_MOST || heightMeasureSpec == MeasureSpec.UNSPECIFIED) { try { throw new NoDetermineSizeException(" Width height has at least one deterministic value, not both wrAP_content "); } catch (NoDetermineSizeException e) { e.printStackTrace(); } } else { if (widthMode == MeasureSpec.EXACTLY) { width = Math.min(widthSize, width); } if (heightMode == MeasureSpec.EXACTLY) { width = Math.min(heightSize, width); } } setMeasuredDimension(width, width); }Copy the code
It now looks like this :(width and height arematch_parent
Still takes up only one square.)
The reason for this is to reduce the waste of space, mainly to avoid the following situation (set width or height tomatch_parent
The display of other components is affected.
5. Obtain the radius and tail length of the dial circle
The acquired value should be after the measurement is completed, so inonSizeChange
To get inside
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRadius = (Math.min(w, h) - mPadding) / 2;
mPointEndLength = mRadius / 6;
}Copy the code
6. Now comes the most important drawing phase
There are several stages
1>. Draw the outer dial
2>. Draw the scale and time mark
3. Draw a pointer
First draw the dial
In order to reduce the computation, first of allcanvas
The origin of the coordinates is moved to the center
The operation of this step is shown in the figure below:
@Override protected void onDraw(Canvas canvas) { canvas.save(); canvas.translate(getWidth() / 2, getHeight() / 2); . canvas.restore(); }Copy the code
We’ve got the radius, so draw the circle
public void paintCircle(Canvas canvas) {
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(0, 0, mRadius, mPaint);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.translate(getWidth() / 2, getHeight() / 2);
paintCircle(canvas);
canvas.restore();
}Copy the code
This step has the following effects:
The next step is to draw the scale and text
As you can see from the example diagram, there are60
The Angle between the two scales is theta6 °
, which contains12
One hour scale.
What we want to do is, of course, draw a line directly on the X or Y axis, but the current X and Y are both horizontal and vertical with respect to the origin, so we can think of drawing the coordinate system by rotating it 6 degrees at a time, total rotation60
Times, and each time inx
Axis ory
Draw on the axis.
Set scale lengthmLineWidth
, the selectedY
Axes draw lines as follows:
right60
To judge the scale, the hourly and non-hourly scales are set to different lengths, colors, and widths after drawing a canvas to rotate6 °
, can complete the drawing of all scales, the code is as follows:
Private void paintScale(Canvas Canvas) {mPaint. SetStrokeWidth (sizeutil.dp2px (getContext()), 1)) int lineWidth = 0 for (int I = 0 if (I % 5 == 0) { SetColor (mColorLong) lineWidth = 40} else {// lineWidth = 30 mPaint. SetColor (mColorShort) mPaint.setStrokeWidth(SizeUtil.Dp2Px(getContext(), 1)) } canvas.drawLine(0, -mRadius + SizeUtil.Dp2Px(getContext(), 10), 0, -mRadius + SizeUtil.Dp2Px(getContext(), 10) + lineWidth, mPaint) canvas.rotate(6) } canvas.restore()Copy the code
It now looks like this:
The next step is to draw the text. Since only the text is available on the hour, the text will be drawn inside the if drawn on the hour.
First get the text content to display
String text = ((i / 5) == 0 ? 12 : (i / 5)) + "";
It’s easy to understand,i
for0 ~ 60
And the first oneY
I’m going to draw it on my axis12
Dot, everything else is right5
You can get that by taking the remainder.
As you can see from the example, the text is vertical, so when drawing the text, you should move the center of the canvas behind the scale and rotate it at the opposite Angle of the scalecanvas.save()
andcanvas.restore()
This may be a little hard to understand, but here’s the picture:
There are a few numbers to work with
1. The rotated coordinate center needs to move behind the scale, so what is the Y coordinate after it moves?
2. To draw the text in a vertical direction, what is the Angle of counterclockwise deflection?
3. How to calculate the height of the text?
Let’s deal with these numbers:
1>. The figure above is clearly marked, the origin of the coordinates is moved behind the scale, and the value of the Y-axis after the move is as follows:
Y coordinate = -mradius + mLineWidth + text height + offset between text and scale
We designed the offset by ourselves and set it as 5dp for the time being. As the scale length has been determined, only the height of the text is left, which can be obtained by the following method
mPaint.setTextSize(mTextSize) String text = ((i / 5) == 0 ? 12: (i / 5)) + "" Rect textBound = new Rect() mPaint.getTextBounds(text, 0, text.length(), textBound) int textHeight = textBound.bottom - textBound.topCopy the code
The final y-coordinate value after the move is:
-mRadius + DptoPx(5) + lineWidth + (textBound.bottom - textBound.top))
The Angle of rotation to draw text is-6 * i
(Negative of the current rotation Angle)
Now that you have the data, it’s time to draw the text. Drawing text now also requires drawing the beginning of the textX,Y
Coordinates, notice what’s in thereY
Coordinates The coordinates of the baseline. For those who don’t understand, please look at the blog. There are details about drawing text at the end:Android copy JINGdong home page round broadcast text (also known as vertical running lantern)
The text is drawn as follows:
This is pretty clear, so start drawing the text
canvas.save()
canvas.translate(0, -mRadius + DptoPx(5) + lineWidth + (textBound.bottom - textBound.top))
canvas.rotate(-6 * i)
mPaint.setStyle(Paint.Style.FILL)
canvas.drawText(text, -(textBound.right - textBound.left) / 2,textBound.bottom, mPaint)
canvas.restore()Copy the code
Complete drawing scale method
Private void paintScale(Canvas Canvas) {mPaint. SetStrokeWidth (sizeutil.dp2px (getContext()), 1)) int lineWidth = 0 for (int I = 0 if (I % 5 == 0) { SetColor (mColorLong) lineWidth = 40 mPaint. SetTextSize (mTextSize) String text = ((I / 5) == 0? 12: (i / 5)) + "" Rect textBound = new Rect() mPaint.getTextBounds(text, 0, text.length(), textBound) mPaint.setColor(Color.BLACK) canvas.save() canvas.translate(0, -mRadius + DptoPx(5) + lineWidth + (textBound.bottom - textBound.top)) canvas.rotate(-6 * i) mPaint.setStyle(Paint.Style.FILL) canvas.drawText(text, -(textBound.right - textBound.left) / 2,textBound.bottom, MPaint) canvas.restore()} else {// lineWidth = 30 mPaint. SetColor (mColorShort) mPaint.setStrokeWidth(SizeUtil.Dp2Px(getContext(), 1)) } canvas.drawLine(0, -mRadius + SizeUtil.Dp2Px(getContext(), 10), 0, -mRadius + SizeUtil.Dp2Px(getContext(), 10) + lineWidth, mPaint) canvas.rotate(6) } canvas.restore() }Copy the code
Effect:
We can see from the picture that the text and the scale are drawn where we want them to be
Next draw the pointer:
The pointer is drawn withcanvas.drawRoundRect
Method that needs to specify a pointerRectF
Property, to simplify the calculation, we still use the method of drawing on the Y-axis and then rotating the specified Angle.
First, get the current practice and calculate the Angle value to be rotated in each minute and second
Calendar calendar = Calendar.getInstance();
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
int angleHour = (hour % 12) * 360 / 12;
int angleMinute = minute * 360 / 60;
int angleSecond = second * 360 / 60; Copy the code
To obtain a pointerRectF
Schematic diagram of:
It’s a lot easier when you understand itY
Shaft drawingRoundRect
, and then rotate the corresponding Angle, time and second hand rotation Angle is different, so they need to usecanvas.save()
andcanvas.restore()
Methods include. Directly on all Pointers to the code:
private void paintPointer(Canvas canvas) {
Calendar calendar = Calendar.getInstance();
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
int angleHour = (hour % 12) * 360 / 12;
int angleMinute = minute * 360 / 60;
int angleSecond = second * 360 / 60;
canvas.save();
canvas.rotate(angleHour);
RectF rectFHour = new RectF(-mHourPointWidth / 2, -mRadius * 3 / 5, mHourPointWidth / 2, mPointEndLength);
mPaint.setColor(mHourPointColor);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mHourPointWidth);
canvas.drawRoundRect(rectFHour, mPointRadius, mPointRadius, mPaint);
canvas.restore();
canvas.save();
canvas.rotate(angleMinute);
RectF rectFMinute = new RectF(-mMinutePointWidth / 2, -mRadius * 3.5f / 5, mMinutePointWidth / 2, mPointEndLength);
mPaint.setColor(mMinutePointColor);
mPaint.setStrokeWidth(mMinutePointWidth);
canvas.drawRoundRect(rectFMinute, mPointRadius, mPointRadius, mPaint);
canvas.restore();
canvas.save();
canvas.rotate(angleSecond);
RectF rectFSecond = new RectF(-mSecondPointWidth / 2, -mRadius + 15, mSecondPointWidth / 2, mPointEndLength);
mPaint.setColor(mSecondPointColor);
mPaint.setStrokeWidth(mSecondPointWidth);
canvas.drawRoundRect(rectFSecond, mPointRadius, mPointRadius, mPaint);
canvas.restore();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mSecondPointColor);
canvas.drawCircle(0, 0, mSecondPointWidth * 4, mPaint);
}Copy the code
Finally, inonDraw()
Inside call each drawing method can. Then refresh every second. The finalonDraw
As follows:
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.translate(getWidth() / 2, getHeight() / 2);
paintCircle(canvas);
paintScale(canvas);
paintPointer(canvas);
canvas.restore();
postInvalidateDelayed(1000);
}Copy the code