Effect display:

Source of inspiration:

Let’s get straight to the point:

1. Step 1: Create a customViewinheritanceView, 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: invalueNew Under Filewatch_board_attr.xmlFile, 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

SizeUtilSee 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 inviewIt’s easy in the middle, but that would waste a lot of space, so we should rewrite itonMeasureMethod, 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_contentThrow 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

onMeasureMethods:

@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_parentStill 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_parentThe 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 inonSizeChangeTo 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 allcanvasThe 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 are60The Angle between the two scales is theta6 °, which contains12One 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 rotation60Times, and each time inxAxis oryDraw on the axis.

Set scale lengthmLineWidth, the selectedYAxes draw lines as follows:

right60To 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,ifor0 ~ 60And the first oneYI’m going to draw it on my axis12Dot, everything else is right5You 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,YCoordinates, notice what’s in thereYCoordinates 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.drawRoundRectMethod that needs to specify a pointerRectFProperty, 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 pointerRectFSchematic diagram of:

It’s a lot easier when you understand itYShaft 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 finalonDrawAs 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

End result:

This customizationViewIt’s written on my ownDemoIn the collection, I don’t want to separate them out and waste time. Give me theDemoCollection Address:LibManager

javaThe file is inUILib moduleUnder thewatchboard packageNext. I’ll write about it laterDemoSet introduction. First put a dynamic diagram (demo part only).

So far this customizationViewIt’s done. Welcome to point out any inadequacies, and build a new exchangeAndroidThe development ofQQGroup, welcome aboard.

Group no. :375276053