简体   繁体   中英

Android round edges on ring shaped progressbar

I'm trying to make a circular progress bar on android and it seems pretty straightforward task , but I'm struggling with rounding the edges of the progress and secondary progress.

Is there a way to do that without making a custom view ? Using a corners radius ? or nine patch drawable ?

For this view (see attachement) I'm using a simple xml file

<item android:id="@android:id/progress">


    <shape
        android:useLevel="true"
        android:innerRadius="@dimen/sixty_dp"
        android:shape="ring"
        android:thickness="@dimen/seven_dp">

        <solid android:color="#477C5B"/>

        <stroke android:width="1dip"
            android:color="#FFFF"/>
    </shape>






</item>

在此处输入图片说明

Just create class called MyProgress in your package .. and paste the following code..

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.View;

public class MyProgress extends View {

    private Paint mPrimaryPaint;
    private Paint mSecondaryPaint;
    private RectF mRectF;
    private TextPaint mTextPaint;
    private Paint mBackgroundPaint;

    private boolean mDrawText = false;

    private int mSecondaryProgressColor;
    private int mPrimaryProgressColor;
    private int mBackgroundColor;

    private int mStrokeWidth;

    private int mProgress;
    private int mSecodaryProgress;

    private int mTextColor;

    private int mPrimaryCapSize;
    private int mSecondaryCapSize;
    private boolean mIsPrimaryCapVisible;
    private boolean mIsSecondaryCapVisible;

    private int x;
    private int y;
    private int mWidth = 0, mHeight = 0;


    public MyProgress(Context context) {
        super(context);
        init(context, null);
    }

    public MyProgress(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public MyProgress(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    void init(Context context, AttributeSet attrs) {
        TypedArray a;
        if (attrs != null) {
            a = context.getTheme().obtainStyledAttributes(
                    attrs,
                    R.styleable.MyProgress,
                    0, 0);
        } else {
            throw new IllegalArgumentException("Must have to pass the attributes");
        }

        try {
            mDrawText = a.getBoolean(R.styleable.MyProgress_showProgressText, false);

            mBackgroundColor = a.getColor(R.styleable.MyProgress_backgroundColor, android.R.color.darker_gray);
            mPrimaryProgressColor = a.getColor(R.styleable.MyProgress_progressColor, android.R.color.darker_gray);
            mSecondaryProgressColor = a.getColor(R.styleable.MyProgress_secondaryProgressColor, android.R.color.black);

            mProgress = a.getInt(R.styleable.MyProgress_progress, 0);
            mSecodaryProgress = a.getInt(R.styleable.MyProgress_secondaryProgress, 0);

            mStrokeWidth = a.getDimensionPixelSize(R.styleable.MyProgress_strokeWidth, 20);
            mTextColor = a.getColor(R.styleable.MyProgress_textColor, android.R.color.black);

            mPrimaryCapSize = a.getInt(R.styleable.MyProgress_primaryCapSize, 20);
            mSecondaryCapSize = a.getInt(R.styleable.MyProgress_secodaryCapSize, 20);

            mIsPrimaryCapVisible = a.getBoolean(R.styleable.MyProgress_primaryCapVisibility, true);
            mIsSecondaryCapVisible = a.getBoolean(R.styleable.MyProgress_secodaryCapVisibility, true);
        } finally {
            a.recycle();
        }

        mBackgroundPaint = new Paint();
        mBackgroundPaint.setAntiAlias(true);
        mBackgroundPaint.setStyle(Paint.Style.STROKE);
        mBackgroundPaint.setStrokeWidth(mStrokeWidth);
        mBackgroundPaint.setColor(mBackgroundColor);

        mPrimaryPaint = new Paint();
        mPrimaryPaint.setAntiAlias(true);
        mPrimaryPaint.setStyle(Paint.Style.STROKE);
        mPrimaryPaint.setStrokeWidth(mStrokeWidth);
        mPrimaryPaint.setColor(mPrimaryProgressColor);

        mSecondaryPaint = new Paint();
        mSecondaryPaint.setAntiAlias(true);
        mSecondaryPaint.setStyle(Paint.Style.STROKE);
        mSecondaryPaint.setStrokeWidth(mStrokeWidth - 2);
        mSecondaryPaint.setColor(mSecondaryProgressColor);

        mTextPaint = new TextPaint();
        mTextPaint.setColor(mTextColor);

        mRectF = new RectF();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mRectF.set(getPaddingLeft(), getPaddingTop(), w - getPaddingRight(), h - getPaddingBottom());
        mTextPaint.setTextSize(w / 5);
        x = (w / 2) - ((int) (mTextPaint.measureText(mProgress + "%") / 2));
        y = (int) ((h / 2) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2));
        mWidth = w;
        mHeight = h;
        invalidate();
    }

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

        mPrimaryPaint.setStyle(Paint.Style.STROKE);
        mSecondaryPaint.setStyle(Paint.Style.STROKE);

        // for drawing a full progress .. The background circle
        canvas.drawArc(mRectF, 0, 360, false, mBackgroundPaint);

        // for drawing a secondary progress circle
        int secondarySwipeangle = (mSecodaryProgress * 360) / 100;
        canvas.drawArc(mRectF, 270, secondarySwipeangle, false, mSecondaryPaint);

        // for drawing a main progress circle
        int primarySwipeangle = (mProgress * 360) / 100;
        canvas.drawArc(mRectF, 270, primarySwipeangle, false, mPrimaryPaint);

        // for cap of secondary progress
        int r = (getHeight() - getPaddingLeft() * 2) / 2;      // Calculated from canvas width
        double trad = (secondarySwipeangle - 90) * (Math.PI / 180d); // = 5.1051
        int x = (int) (r * Math.cos(trad));
        int y = (int) (r * Math.sin(trad));
        mSecondaryPaint.setStyle(Paint.Style.FILL);
        if (mIsSecondaryCapVisible)
            canvas.drawCircle(x + (mWidth / 2), y + (mHeight / 2), mSecondaryCapSize, mSecondaryPaint);

        // for cap of primary progress
        trad = (primarySwipeangle - 90) * (Math.PI / 180d); // = 5.1051
        x = (int) (r * Math.cos(trad));
        y = (int) (r * Math.sin(trad));
        mPrimaryPaint.setStyle(Paint.Style.FILL);
        if (mIsPrimaryCapVisible)
            canvas.drawCircle(x + (mWidth / 2), y + (mHeight / 2), mPrimaryCapSize, mPrimaryPaint);


        if (mDrawText)
            canvas.drawText(mProgress + "%", x, y, mTextPaint);
    }

    public void setDrawText(boolean mDrawText) {
        this.mDrawText = mDrawText;
        invalidate();
    }

    public void setBackgroundColor(int mBackgroundColor) {
        this.mBackgroundColor = mBackgroundColor;
        invalidate();
    }

    public void setSecondaryProgressColor(int mSecondaryProgressColor) {
        this.mSecondaryProgressColor = mSecondaryProgressColor;
        invalidate();
    }

    public void setPrimaryProgressColor(int mPrimaryProgressColor) {
        this.mPrimaryProgressColor = mPrimaryProgressColor;
        invalidate();
    }

    public void setStrokeWidth(int mStrokeWidth) {
        this.mStrokeWidth = mStrokeWidth;
        invalidate();
    }

    public void setProgress(int mProgress) {
        this.mProgress = mProgress;
        invalidate();
    }

    public void setSecondaryProgress(int mSecondaryProgress) {
        this.mSecodaryProgress = mSecondaryProgress;
        invalidate();
    }

    public void setTextColor(int mTextColor) {
        this.mTextColor = mTextColor;
        invalidate();
    }

    public void setPrimaryCapSize(int mPrimaryCapSize) {
        this.mPrimaryCapSize = mPrimaryCapSize;
        invalidate();
    }

    public void setSecondaryCapSize(int mSecondaryCapSize) {
        this.mSecondaryCapSize = mSecondaryCapSize;
        invalidate();
    }

    public boolean isPrimaryCapVisible() {
        return mIsPrimaryCapVisible;
    }

    public void setIsPrimaryCapVisible(boolean mIsPrimaryCapVisible) {
        this.mIsPrimaryCapVisible = mIsPrimaryCapVisible;
    }

    public boolean isSecondaryCapVisible() {
        return mIsSecondaryCapVisible;
    }

    public void setIsSecondaryCapVisible(boolean mIsSecondaryCapVisible) {
        this.mIsSecondaryCapVisible = mIsSecondaryCapVisible;
    }


    public int getSecondaryProgressColor() {
        return mSecondaryProgressColor;
    }

    public int getPrimaryProgressColor() {
        return mPrimaryProgressColor;
    }

    public int getProgress() {
        return mProgress;
    }

    public int getBackgroundColor() {
        return mBackgroundColor;
    }

    public int getSecodaryProgress() {
        return mSecodaryProgress;
    }

    public int getPrimaryCapSize() {
        return mPrimaryCapSize;
    }

    public int getSecondaryCapSize() {
        return mSecondaryCapSize;
    }
}

and add the following line in res->values->attr.xml under a tag and build it

<declare-styleable name="MyProgress">
    <attr name="showProgressText" format="boolean" />
    <attr name="progress" format="integer" />
    <attr name="secondaryProgress" format="integer" />
    <attr name="progressColor" format="color" />
    <attr name="secondaryProgressColor" format="color" />
    <attr name="backgroundColor" format="color" />
    <attr name="primaryCapSize" format="integer" />
    <attr name="secodaryCapSize" format="integer" />
    <attr name="primaryCapVisibility" format="boolean" />
    <attr name="secodaryCapVisibility" format="boolean" />
    <attr name="strokeWidth" format="dimension" />
    <attr name="textColor" format="color" />
</declare-styleable>

that's it .... and to use in your layout ..

<Your_Package_Name.MyProgress
    android:padding="20dp"
    android:id="@+id/timer1"
    app:strokeWidth="10dp"
    app:progress="30"
    app:secondaryProgress="50"
    app:backgroundColor="@android:color/black"
    app:progressColor="@android:color/holo_blue_bright"
    app:secondaryProgressColor="@android:color/holo_blue_dark"
    app:primaryCapSize="30"
    app:secodaryCapSize="40"
    app:primaryCapVisibility="true"
    app:secodaryCapVisibility="true"
    android:layout_width="200dp"
    android:layout_height="200dp" />

You can also change all property progrmatically using setMethods()...在此处输入图片说明

feel free to ask anything .. best of luck

[Update 23-01-2016]

finally I uploaded code on github. You can refer it from here https://github.com/msquare097/MProgressBar

Now You can use this ProgressBar by simply writing following line in your app build.gradle file. You don't have to copy above code.

compile 'com.msquare.widget.mprogressbar:mprogressbar:1.0.0'

I was able to achieve this using a layer list, and adding a dot to each side of the line. The first one will be stuck at the top, while the second one will follow the progress as an inset is added inside a rotate element. You will have to adjust it according to the size of your progressbar layout though. Mine is 250dp x 250dp.

progress_drawable.xml

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <rotate android:fromDegrees="270" android:toDegrees="270">
        <shape
            android:innerRadiusRatio="2.55"
            android:shape="ring"
            android:thickness="15dp"
            android:useLevel="true">
            <solid android:color="@color/main_color" />
        </shape>
        </rotate>
    </item>
    <item android:bottom="211dp">
        <shape
            android:innerRadiusRatio="1000"
            android:shape="ring"
            android:thickness="7dp"
            android:useLevel="false">
            <solid android:color="@color/main_color" />
        </shape>
    </item>
    <item>
        <rotate>
            <inset android:insetBottom="211dp">
                <shape
                    android:innerRadiusRatio="1000"
                    android:shape="ring"
                    android:thickness="7dp"
                    android:useLevel="false">
                    <solid android:color="@color/main_color" />
                </shape>
            </inset>
        </rotate>
    </item>
</layer-list>

I don't have a secondary process here, but you should be able to adjust it for that as well.

在此处输入图片说明 在此处输入图片说明

A simple and efficient class extending View to draw circular progress, with rounded corners as an option. Progress color, background color, stroke width are also customizable. As seen in my other answer .

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import androidx.annotation.FloatRange

class CircularProgressView : View {
  constructor(context: Context) : super(context)
  constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
  constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

  private val progressPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.STROKE
  }
  private val backgroundPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.STROKE
  }

  private val rect = RectF()
  private val startAngle = -90f
  private val maxAngle = 360f
  private val maxProgress = 100

  private var diameter = 0f
  private var angle = 0f

  override fun onDraw(canvas: Canvas) {
    drawCircle(maxAngle, canvas, backgroundPaint)
    drawCircle(angle, canvas, progressPaint)
  }

  override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
    diameter = Math.min(width, height).toFloat()
    updateRect()
  }

  private fun updateRect() {
    val strokeWidth = backgroundPaint.strokeWidth
    rect.set(strokeWidth, strokeWidth, diameter - strokeWidth, diameter - strokeWidth)
  }

  private fun drawCircle(angle: Float, canvas: Canvas, paint: Paint) {
    canvas.drawArc(rect, startAngle, angle, false, paint)
  }

  private fun calculateAngle(progress: Float) = maxAngle / maxProgress * progress

  fun setProgress(@FloatRange(from = 0.0, to = 100.0) progress: Float) {
    angle = calculateAngle(progress)
    invalidate()
  }

  fun setProgressColor(color: Int) {
    progressPaint.color = color
    invalidate()
  }

  fun setProgressBackgroundColor(color: Int) {
    backgroundPaint.color = color
    invalidate()
  }

  fun setProgressWidth(width: Float) {
    progressPaint.strokeWidth = width
    backgroundPaint.strokeWidth = width
    updateRect()
    invalidate()
  }

  fun setRounded(rounded: Boolean) {
    progressPaint.strokeCap = if (rounded) Paint.Cap.ROUND else Paint.Cap.BUTT
    invalidate()
  }
}

you can add a circle/oval shape on the end & begin side of the ring, just like the below code

a layer-list drawable contain two circle/oval shape drawable and a ring shape drawable

round_progress_drawable.xml

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:bottom="294px"
        android:left="540px"
        android:right="48px"
        android:top="294px">
        <shape
            android:innerRadius="6px"
            android:shape="oval">
            <solid android:color="#FFFFFF"/>
        </shape>
    </item>
    <item>
        <shape
            android:innerRadius="240px"
            android:shape="ring"
            android:thickness="12px">
            <size
                android:width="600px"
                android:height="600px"/>
            <solid android:color="#FFFFFF"/>
        </shape>
    </item>
    <item>
        <rotate>
            <layer-list>
                <item
                    android:bottom="294px"
                    android:left="540px"
                    android:right="48px"
                    android:top="294px">
                    <shape
                        android:innerRadius="6px"
                        android:shape="oval">
                        <solid android:color="#FFFFFF"/>
                    </shape>
                </item>
            </layer-list>
        </rotate>
    </item>
</layer-list>

round_progress_animation_selector.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_window_focused="true">
        <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
            android:duration="5000"
            android:propertyName="ImageLevel"
            android:repeatCount="infinite"
            android:valueFrom="0"
            android:valueTo="10000"
            android:valueType="intType">

        </objectAnimator>
    </item>
    <item>
        <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
            android:duration="0"
            android:propertyName="ImageLevel"
            android:valueFrom="0"
            android:valueTo="0"
            android:valueType="intType">

        </objectAnimator>
    </item>
</selector>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimaryDark"
    tools:context=".MainActivity">


    <ImageView
        android:stateListAnimator="@animator/round_progress_animation_selector"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/round_progress_drawable" />

</FrameLayout>

演示

I tried to tackle this for arbitrary size progress bars and if you can ensure the progress bar is a square and you don't use margins, this should work but it doesn't. Android 10 renders it sometimes correctly sometime it doesn't. Especially progress values > 50% tend to break badly. It is not a working solution but might give someone an idea. There are still issues with the gradient fill. The end color doesn't match it. But if you use solid colors, it should work fine. Also the rounded cap causes the bar to show larger value range. My idea was to replace the ring with a shape that is 5dp x 2.5dp rectangle in background color with clipped half a circle. You would need to use different rotations for the start and end:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
<rotate
    android:fromDegrees="270"
    android:toDegrees="270">
    <shape
        android:innerRadiusRatio="2.4"
        android:shape="ring"
        android:thickness="5dp"
        android:useLevel="true"><!-- this line fixes the issue for lollipop api 21 -->

        <gradient
            android:angle="0"
            android:endColor="@color/yellow"
            android:startColor="@color/orange"
            android:type="sweep"
            android:useLevel="false" />

    </shape>
</rotate>
    </item>
    <item
        android:gravity="top">
        <shape
            android:innerRadiusRatio="1000"
            android:shape="ring"
            android:thickness="2.5dp"
            android:useLevel="false">
            <solid android:color="@color/orange" />
            <size android:height="5dp"/>
        </shape>
    </item>
    <item>
        <rotate>
            <scale android:scaleGravity="top|center_horizontal"
                android:scaleHeight="150%"
                android:scaleWidth="150%">
                <shape
                    android:innerRadiusRatio="1000"
                    android:shape="ring"
                    android:thickness="2.5dp"
                    android:useLevel="false">
                    <solid android:color="@color/yellow" />
                </shape>
            </scale>
        </rotate>
    </item>
</layer-list>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM