preface

A recent project encountered a requirement that required a circular progress bar to indicate the loading of a download request. At the same time, different ICONS should be used to show the various states in the downloading process: waiting, downloading, pause, error, done.

See the following figure for specific ICONS:

The above icon is from www.iconfont.cn/.

Consider that there are as many as five states. Use the existing control combination display, and then judge the state to control the display of each icon is not appropriate. Take this opportunity to brush up on custom controls by simply stroking one of these custom controls: the CircleProgressBar.

To directly copy CircleProgressBar, use circleProgressbar.java

Custom control

First of all, you need to understand the basic principles of android custom control, control drawing process. It is recommended to check out the official documentation Custom View Components. Note: The document is in English and has walls.

A brief summary is shown in the following table:

Once you understand the basics, start customizing controls. If you haven’t read the above document, please write down the steps below.

Create a View

Generally custom views are inherited from Android.view.view. But since we are a custom ProgressBar, it is not necessary to start over, directly inherited from android. The widget. The ProgressBar. Such setProgress (int progress); These basic methods don’t need to be defined anymore. So, I call my control CircleProgressBar extends ProgressBar.

Observe the above ICONS, except that the status is loaded in progress and its shape is changed during downloading, the other states are all a static picture. Now all you have to do is circle progress and draw the two vertical lines in the middle.

Define custom properties

We can create a LinearLayout directly from the.xml file when using the controls provided by the Android SDK.

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal" />
Copy the code

You can also configure various attributes directly in the.xml file, such as Android :orientation=”horizontal” in the code above. Public CircleProgressBar(Context Context, AttributeSet attrs) {} Public CircleProgressBar(Context Context, AttributeSet attrs) {} This constructor allows us to create and edit instances of our custom controls in an.xml file:

public CircleProgressBar(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}
Copy the code

Also, to define our custom attributes (eg: color, size, etc.) in the.xml file, we need to add the following constructor:

public CircleProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}
Copy the code

DefStyleAttr The integer variable is a declare-styleable value defined in the res/values/attrs. XML file. Based on this, we need to create a new res/values/attrs.xml file and define some custom attributes that we need to use.

Look at the outer progress bar you want to implement. There are two progress bars: one for the default circle and one for the color of the progress. Therefore, it involves the definition of the color width and height of the two progress bars. You definitely need a radius to draw a circle. So all defined attributes are as follows:

<?xml version="1.0" encoding="utf-8"? >
<resources>
    <declare-styleable name="CircleProgressBar">
        <! -- Default circle color -->
        <attr name="defaultColor" format="color" />
        <! -- Progress bar color -->
        <attr name="reachedColor" format="color" />
        <! -- Default circle height -->
        <attr name="defaultHeight" format="dimension" />
        <! -- Progress bar height -->
        <attr name="reachedHeight" format="dimension" />
        <! -- Radius of the circle -->
        <attr name="radius" format="dimension" />
    </declare-styleable>
</resources>
Copy the code

This code declares five custom properties that belong to styleable: CircleProgressBar. For convenience, the name of a styleable is the same as the class name of our custom control. Custom controls can be used directly after they are defined. See comments in the XML for the specific meanings of custom attribute values.

These custom properties can be set directly in use:

<com.chengww.circleprogressdemo.CircleProgressBar
    android:layout_width="46dp"
    android:layout_height="46dp"
    android:padding="6dp"
    android:id="@+id/cp_progress"
    app:defaultColor="#D8D8D8"
    app:reachedColor="#1296DB"
    app:defaultHeight="2.5 dp"
    app:reachedHeight="2.5 dp" />
Copy the code

Gets custom attributes

Now that you have defined the custom properties, you need to get the custom properties that are set in the actual use. Otherwise there is no point in defining custom attributes. First define the member variables:

private int mDefaultColor;
private int mReachedColor;
private int mDefaultHeight;
private int mReachedHeight;
private int mRadius;
private Paint mPaint;
private Status mStatus = Status.Waiting;
Copy the code

Then it’s time to get the member variables. Public CircleProgressBar(Context Context, AttributeSet attrs, int defStyleAttr) {} This is the method that gets the value of a custom property set by the user:

    public CircleProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar);
        // The color of the default circle
        mDefaultColor = typedArray.getColor(R.styleable.CircleProgressBar_defaultColor, Color.parseColor("#D8D8D8"));
        // Color of the progress bar
        mReachedColor = typedArray.getColor(R.styleable.CircleProgressBar_reachedColor, Color.parseColor("#1296DB"));
        // The height of the default circle
        mDefaultHeight = typedArray.getDimension(R.styleable.CircleProgressBar_defaultHeight, dp2px(context, 2.5 f));
        // Height of the progress bar
        mReachedHeight = typedArray.getDimension(R.styleable.CircleProgressBar_reachedHeight, dp2px(context, 2.5 f));
        // Radius of the circle
        mRadius = typedArray.getDimension(R.styleable.CircleProgressBar_radius, dp2px(context, 17));
        typedArray.recycle();

        setPaint();
    }
Copy the code

When we create a View in an XML file, all the properties declared in the XML file are passed into the View constructor above. Return a TypedArray object by calling the obtainStyledAttributes() method of the Context. Then use the TypedArray object directly to get the value of the custom property. The second argument is to get the default value if not obtained. Because a TypedArray object is a shared resource, you must call the Recycle () method after the value is fetched to recycle it.

Use Java methods to set custom properties

The above method can only set custom attributes through an XML file, which can only be obtained when the View is initialized. To modify a property value at run time using Java methods, add Getter and Setter methods to a property value (a member variable).

private Status mStatus = Status.Waiting;

public Status getStatus(a) {
    return mStatus;
}

public void setStatus(Status status) {
    if (mStatus == status) return;
    mStatus = status;
    invalidate();
}
Copy the code

Note the setStatus method, which calls the invalidate() method after assigning the value to mStatus. If the properties of our custom control change, the appearance of the control may also change. In this case, the invalidate() method is called and the system calls the View’s onDraw() to redraw. Similarly, changes in control properties can cause the size and shape of the control to change, so requestLayout() can be called to request measurements for a new layout location. Note: You do not need to call requestLayout() if you are sure that the control will not change size or position after changing a property. Similarly, if the control does not need to be redrawn, you do not need to call the invalidate() method.

Get some basic properties, where mStatus is used to represent the current View state corresponding to various download states. We use these states to determine how to draw the appropriate effects. Each state is represented by an internal enumeration.

public enum Status {
    Waiting,
    Pause,
    Loading,
    Error,
    Finish
}
Copy the code

SetPaint () above initializes the paint method. To draw the progress ring and each static Drawable. Attach the setPaint() method code:

private void setPaint(a) {
    mPaint = new Paint();
    // Here are some properties to set the brush
    mPaint.setAntiAlias(true);/ / anti-aliasing
    mPaint.setDither(true);// Anti jitter, draw out the graph should be more soft and clear
    mPaint.setStyle(Paint.Style.STROKE);// Set the fill style
    Style.FILL_AND_STROKE: FILL the interior and STROKE */
    mPaint.setStrokeCap(Paint.Cap.ROUND);// Set the brush type
}
Copy the code

Handle the layout of the View

The View of measurement

A View is always displayed in its width and height, measurement of the View is to enable the custom control can be displayed according to a variety of different situations with the appropriate width and height. The specific method used is onMeasure() method. This method is overridden from the system method and takes two arguments: int widthMeasureSpec, int heightMeasureSpec. These two parameters contain two important pieces of information: Mode and Size. Get Mode and Size:

int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
Copy the code

The above code can obtain widthMode, heightMode, widthSize, heightSize.

Mode represents the parent of the current control and tells us how the control should be laid out. The Mode has three optional values: EXACTLY, AT_MOST, and UNSPECIFIED. What they mean is:

  • EXACTLY: The parent control tells us that the child control has a certain size, and you layout according to that size. For example, we specify a certain dp value and match_parent.
  • AT_MOST: The current control cannot exceed a fixed maximum value, usually wrAP_content.
  • UNSPECIFIED: The current control has no limitations and is UNSPECIFIED, which is rare.

Size is actually a Size passed in by the parent layout that the parent wants the current layout to be.

Here’s how the onMeasure() method is written in our code:

@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    int paintHeight = Math.max(mReachedHeight, mDefaultHeight);

    if(heightMode ! = MeasureSpec.EXACTLY) {int exceptHeight = getPaddingTop() + getPaddingBottom() + mRadius * 2 + paintHeight;
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(exceptHeight, MeasureSpec.EXACTLY);
    }
    if(widthMode ! = MeasureSpec.EXACTLY) {int exceptWidth = getPaddingLeft() + getPaddingRight() + mRadius * 2 + paintHeight;
        widthMeasureSpec = MeasureSpec.makeMeasureSpec(exceptWidth, MeasureSpec.EXACTLY);
    }

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
Copy the code

We only need to deal with cases where the width and height are not specified exactly, and use the padding plus the entire circle and Paint width to calculate the exact value.

Next is the rendering effect.

Draw the View

As stated at the beginning: observing the above ICONS, except that the status is loaded in progress during downloading and its shape is changed, the other states are all a static picture. Draw other static images using drawable.draw(canvas); Methods. Now let’s talk about how to draw this state in the download.

Rewrite the onDraw() method and we’ll start drawing circles:

canvas.translate(getPaddingStart(), getPaddingTop());
mPaint.setStyle(Paint.Style.STROKE);
Draw some Settings for the default circle (border)
mPaint.setColor(mDefaultColor);
mPaint.setStrokeWidth(mDefaultHeight);
canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
Copy the code

DrawCircle (mRadius, mRadius, mRadius, mPaint); Draws the circle in its default state. Then change the color of the brush and draw the arc according to the progress.

// Draw some Settings for the progress bar
mPaint.setColor(mReachedColor);
mPaint.setStrokeWidth(mReachedHeight);
// Draw the arc according to the progress
float sweepAngle = getProgress() * 1.0 f / getMax() * 360;
canvas.drawArc(new RectF(0.0, mRadius * 2, mRadius * 2), -90, sweepAngle, false, mPaint);
Copy the code

Finally draw the two vertical lines in the middle of the circle and the download state is complete. Here is an example drawing a vertical line with a width of 2/5 radius (1/5 + 1/5) and a height of 1/2 radius (1/2 + 1/2) :

mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(dp2px(getContext(), 2));
mPaint.setColor(Color.parseColor("# 667380"));
canvas.drawLine(mRadius * 4 / 5, mRadius * 3 / 4, mRadius * 4 / 5.2 * mRadius - (mRadius * 3 / 4), mPaint);
canvas.drawLine(2 * mRadius - (mRadius * 4 / 5), mRadius * 3 / 4.2 * mRadius - (mRadius * 4 / 5), 2 * mRadius - (mRadius * 3 / 4), mPaint);
Copy the code

The onDraw() method is then completed by drawing different states by judging mStatus. Complete onDraw() code and associated dp2px method:

@Override
protected synchronized void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    /** * here canvas.save(); And canvas. Restore (); Are the two match each other, function is used to save the canvas state and remove the saved state * when we rotate the canvas, scaling, translation operations such as when we are really want to specific elements, but when you use the method of canvas for these operations, actually is the operation of the entire canvas, * All subsequent elements on the canvas will be affected, so we call canvas.save() before the operation to save the current state of the canvas, and after the operation to retrieve the previously saved state, * (for example: After the previous element sets the pan or rotate operation, the next element executes canvas.save() before drawing; And canvas.restore()) so that subsequent elements are not affected (by panning or rotation) */
    canvas.save();
    // To ensure that the outermost arc is fully displayed, we usually set the padding property of our custom view so that we have the inner margin, so the brush should be shifted to the inner margin so that the brush is right on the outermost arc
    // Pan the pen to the specified paddingLeft, getPaddingTop() position
    canvas.translate(getPaddingStart(), getPaddingTop());

    int mDiameter = (int) (mRadius * 2);
    if (mStatus == Status.Loading) {
        mPaint.setStyle(Paint.Style.STROKE);
        Draw some Settings for the default circle (border)
        mPaint.setColor(mDefaultColor);
        mPaint.setStrokeWidth(mDefaultHeight);
        canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);

        // Draw some Settings for the progress bar
        mPaint.setColor(mReachedColor);
        mPaint.setStrokeWidth(mReachedHeight);
        // Draw the arc according to the progress
        float sweepAngle = getProgress() * 1.0 f / getMax() * 360;
        canvas.drawArc(new RectF(0.0, mRadius * 2, mRadius * 2), -90, sweepAngle, false, mPaint);

        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(dp2px(getContext(), 2));
        mPaint.setColor(Color.parseColor("# 667380"));
        canvas.drawLine(mRadius * 4 / 5, mRadius * 3 / 4, mRadius * 4 / 5.2 * mRadius - (mRadius * 3 / 4), mPaint);
        canvas.drawLine(2 * mRadius - (mRadius * 4 / 5), mRadius * 3 / 4.2 * mRadius - (mRadius * 4 / 5), 2 * mRadius - (mRadius * 3 / 4), mPaint);
    } else {
        int drawableInt;
        switch (mStatus) {
            case Waiting:
            default:
                drawableInt = R.mipmap.ic_waiting;
                break;
            case Pause:
                drawableInt = R.mipmap.ic_pause;
                break;
            case Finish:
                drawableInt = R.mipmap.ic_finish;
                break;
            case Error:
                drawableInt = R.mipmap.ic_error;
                break;
        }
        Drawable drawable = getContext().getResources().getDrawable(drawableInt);
        drawable.setBounds(0.0, mDiameter, mDiameter);
        drawable.draw(canvas);
    }
    canvas.restore();
}

float dp2px(Context context, float dp) {
    final float scale = context.getResources().getDisplayMetrics().density;
    return dp * scale + 0.5 f;
}
Copy the code

Handling user interaction

This step is not necessary because the control only displays the status for downloading updates. To use it, you can set the click event yourself.

Finished product effect GIF:

Demo apk download: blog.chengww.com/files/Circl…

Download the source code: github.com/chengww5217…