My CSDN: ListerCi My simple book: The Dawn of the East

On Monday morning, Lao Wang was carrying the bun in his mouth when he entered the company. Lao Wang secretly said, “No, I want to escape, but the product has been cut off:” Our progress bar needs to be revised, I found you a style, you do it.” Lao Wang thought of a progress bar, what can there be. Reaching out and taking over the product, the progress bar looks like this:

Lao Wang frowned, realizing that it wasn’t easy, but for a cool-headed old developer, he never said “should” or “might” that hurt his image. He smiled at the product and said, “I’ll have it by the end of the day.”

.

1. Background and filling of progress bar

Seeing such a requirement, Wang’s first reaction was to implement the external background and fill color first. First draw the overall background of the exterior, then draw the fill of the interior according to the progress. So first we need to implement a custom View like this:

public class ProgressTestView extends View {
    private Context mContext;
    private int mWidth;
    private int mHeight;
    private float mRadius;

    private Paint mBackgroundPaint;
    private RectF mBackgroundRectF;

    public ProgressTestView(Context context) {
        this(context, null);
    }

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

    public ProgressTestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        init();
    }

    private void init(a) {
        setLayerType(LAYER_TYPE_SOFTWARE, null); // Disable hardware acceleration
        mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBackgroundPaint.setColor(Color.GRAY);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
        mRadius = mHeight / 2.0 f;
        mBackgroundRectF = new RectF(0.0, mWidth, mHeight);
    }

    private void drawBackground(Canvas canvas) {
        canvas.drawRoundRect(mBackgroundRectF, mRadius, mRadius, mBackgroundPaint);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas); drawBackground(canvas); }}Copy the code

This custom View is very simple. In onSizeChanged(), get the height and width set for the View. Since you want to draw a rounded rectangle, set half the height to the radius at both ends of the rounded rectangle. The effect is as follows:

Once you have the background, you can start drawing the content. The background is also filled with a rounded rectangle. The rounded rectangle has the same height and radian as the background rectangle, and the width is the product of the current progress (∈[0, 1]) and the background width. According to this idea, you need to define a value that represents the current progress and provides the outside world with a way to modify that progress. Redraw the current View every time the outside world changes its progress. The code is as follows:

public class ProgressTestView extends View {
    / /...
    private float mCurProgress = 0;
    private Paint mContentPaint;
    / /...

    public void setCurProgress(float progress) {
        if (progress > 1) mCurProgress = 1;
        else if (progress < 0) mCurProgress = 0;
        else mCurProgress = progress;
        invalidate();
    }

    private void init(a) {
        / /...
        mContentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mContentPaint.setColor(mContext.getResources().getColor(R.color.color_progress_content));
    }

    / /...

    private void drawContent(Canvas canvas) {
        canvas.drawRoundRect(new RectF(0.0, mCurProgress * mWidth, mHeight),
                mRadius, mRadius, mContentPaint);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas); drawBackground(canvas); drawContent(canvas); }}Copy the code

Then define an animation where the View is used to see the effect:

ValueAnimator animator = ValueAnimator.ofFloat(0.1);
animator.setDuration(5000);
animator.addUpdateListener(animation -> {
    float percentage = (float) animation.getAnimatedValue();
    mProgressView.setCurProgress(percentage);
});
animator.start();
Copy the code

The final result looks like this:

Is there something wrong with this effect? Lao Wang also fell into a meditation, as a compulsive disorder, simply can not accept this ugly animation.

A closer look reveals that the filled content only displays correctly if width >= height. So can we only show the part where the background intersects the content? Of course you can! This involves image composition – PorterDuffXfermode.

Two, the use of PorterDuffXfermode and pothole avoidance guide

PorterDuffXfermode is used for the synthesis of two images. There are 16 synthesis methods defined in the API. In the figure, the Src image in blue and the Dst image in yellow can be obtained by different combination methods. In order to achieve the intersection mentioned above, the contents of the progress bar as Dst and background as Src can be synthesized by DstIn.

With this in mind, you can modify the original code by looking at the PorterDuffXfermode usage documentation. Note that DST is drawn before SRC.

public class ProgressTestView extends View {
    
    private PorterDuffXfermode mPorterDuffXfermode;

    / /...

    private void init(a) {
        setLayerType(LAYER_TYPE_SOFTWARE, null);

        mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);

        mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBackgroundPaint.setColor(Color.GRAY);

        mContentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mContentPaint.setColor(mContext.getResources().getColor(R.color.color_progress_content));
    }

    / /...

    private void drawContent(Canvas canvas) {
        canvas.drawRoundRect(new RectF(0.0, mCurProgress * mWidth, mHeight),
                mRadius, mRadius, mContentPaint);
        mContentPaint.setXfermode(mPorterDuffXfermode);
        canvas.drawRoundRect(mBackgroundRectF, mRadius, mRadius, mContentPaint);
        mContentPaint.setXfermode(null); }}Copy the code

When I run it, I find that the effect does not change, because Layer is not taken into account.

Layer can be understood as a Layer of Canvas. By default, Canvas has only one Layer and all the drawings are on the same Layer. When you need to draw multilayer images, you can use Canvas.savelayer (…) Generate a new Layer, the content drawn on the new Layer is independent of the content of other layers, the call to Canvas. RestoreToCount (int SC) will overwrite this Layer on the existing canvas image. Canvas manages Layer in the form of stack, as shown in the diagram below.

In image blending, DST is drawn first and SRC is drawn later. If you do not create a new Layer, everything on the Canvas will be treated as DST when drawing SRC, so the background and other content will also be involved in the image blending, which is easy to get the wrong effect.

public class ProgressTestView extends View {

    / /...

    private void drawContent(Canvas canvas) {
        int sc = canvas.saveLayer(0.0, mWidth, mHeight, null);

        canvas.drawRoundRect(mRectFBackground, mRadius, mRadius, mBgPaint);
        mContentPaint.setXfermode(mContentMode);
        canvas.drawRoundRect(new RectF(0.0, mWidth * mCurPercentage, mHeight),
                mRadius, mRadius, mContentPaint);
        mContentPaint.setXfermode(null);

        canvas.restoreToCount(sc);
    }

    / /...
}
Copy the code

And let’s see what happens. Three words: comfortable ~

Third, text drawing

With the successful experience above, the idea of drawing text is very clear. The text is DST, the content of the progress bar is SRC, and the text itself is white. When the two intersect, the text in the intersection part will draw the color of the content of the progress bar. Looking at the renderings, it is clear that the synthesis method is SRC_ATOP. The text drawing and baseline calculation methods are not introduced separately here. The entire code of the progress bar is shown below.

public class SaleProgressView extends View {
    private int mWidth;
    private int mHeight;
    private float mRadius;
    private RectF mRectFBackground;

    private Paint mBgPaint;
    private Paint mContentPaint;
    private Paint mTextPaint;

    private float mBaseLineY;

    private PorterDuffXfermode mContentMode;
    private PorterDuffXfermode mTextMode;

    private Bitmap mTextBitmap;
    private Canvas mTextCanvas;

    private float mCurPercentage;

    public SaleProgressView(Context context) {
        this(context, null);
    }

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

    public SaleProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(a) {
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);

        mContentMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
        mTextMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP);

        mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBgPaint.setColor(Color.GRAY);
        mContentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mContentPaint.setColor(Color.GREEN);
        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setTextSize(36);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
    }

    public void setCurPercentage(float curPercentage) {
        mCurPercentage = curPercentage;
        invalidate();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
        // Radius of the circle
        mRadius = mHeight / 2.0 f;
        if (mRectFBackground == null) {
            mRectFBackground = new RectF(0.0,
                    mWidth, mHeight);
        }
        if (mBaseLineY == 0.0 f) {
            Paint.FontMetricsInt fm = mTextPaint.getFontMetricsInt();
            mBaseLineY = mHeight / 2.0 f - (fm.descent / 2.0 f + fm.ascent / 2.0 f);
        }

        mTextBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
        mTextCanvas = new Canvas(mTextBitmap);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        drawContent(canvas);
        drawText(canvas);
    }

    private void drawContent(Canvas canvas) {
        int sc = canvas.saveLayer(0.0, mWidth, mHeight, null);

        canvas.drawRoundRect(mRectFBackground, mRadius, mRadius, mBgPaint);
        mContentPaint.setXfermode(mContentMode);
        canvas.drawRoundRect(new RectF(0.0, mWidth * mCurPercentage, mHeight),
                mRadius, mRadius, mContentPaint);
        mContentPaint.setXfermode(null);

        canvas.restoreToCount(sc);
    }

    private void drawText(Canvas canvas) {
        String text = "Downloading";
        mTextPaint.setColor(Color.GREEN);
        mTextCanvas.drawText(text, mWidth / 2.0 f, mBaseLineY, mTextPaint);

        mTextPaint.setXfermode(mTextMode);

        mTextPaint.setColor(Color.WHITE);
        mTextCanvas.drawRoundRect(new RectF(0.0, mWidth * mCurPercentage, mHeight),
                mRadius, mRadius, mTextPaint);

        canvas.drawBitmap(mTextBitmap, 0.0.null);

        mTextPaint.setXfermode(null); }}Copy the code

.

When Lao Wang completed this requirement, it was already dawn and Lao Wang smartly showed the effect to the yawned product who had just come to work. After seeing the product, he asked, “Didn’t you say you would give it to me by the end of work yesterday?” Lao Wang smiled and said, “I haven’t come off work yet.”