Android скругляет края на кольцеобразной полосе прогресса

Я пытаюсь сделать круговой индикатор выполнения на Android, и это кажется довольно простой задачей, но я борюсь с округлением краев прогресса и вторичного прогресса.

Есть ли способ сделать это без создания пользовательского представления? Использование радиуса углов? или девять патчей?

Для этого представления (см. вложение) я использую простой XML-файл.

<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>

введите здесь описание изображения


person moujib    schedule 04.07.2015    source источник
comment
Я думаю, вам следует использовать настраиваемый вид... потому что я тоже сталкиваюсь с той же проблемой... и я сделал это с пользовательским представлением   -  person Moinkhan    schedule 10.07.2015
comment
@Moinkhan, не могли бы вы сделать свой код доступным на Github? Заранее спасибо.   -  person moujib    schedule 10.07.2015


Ответы (5)


Просто создайте класс с именем MyProgress в своем пакете... и вставьте следующий код..

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;
    }
}

и добавьте следующую строку в res->values->attr.xml под тегом и создайте ее

<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>

вот именно....и использовать в своем макете..

<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" />

Вы также можете изменить все свойства программно, используя setMethods()... введите здесь описание изображения

не стесняйтесь спрашивать что-нибудь .. удачи

[Обновление от 23 января 2016 г.]

наконец, я загрузил код на github. Вы можете указать ссылку здесь https://github.com/msquare097/MProgressBar.

Теперь вы можете использовать этот ProgressBar, просто написав следующую строку в файле build.gradle вашего приложения. Вам не нужно копировать приведенный выше код.

compile 'com.msquare.widget.mprogressbar:mprogressbar:1.0.0'
person Moinkhan    schedule 11.07.2015
comment
Спасибо, это то, что мне нужно. Принял ваш ответ. Было бы здорово сделать его доступным на github. - person moujib; 11.07.2015
comment
@Moinkhan, как я могу установить Cap не только в конце прогресса, но и в начале??? TnX - person hamid_c; 16.01.2016
comment
хорошо ... для стартовой шапки я этого не делал. но вы можете добиться этого, используя вторичный прогресс, установленный на 0, и применить колпачок, как хотите ... надеюсь, вы понимаете ... - person Moinkhan; 16.01.2016
comment
@Moinkhan Tnx за ваш ответ, я сделал это по-другому, я добавил mPrimaryPaint.setStrokeCap(Paint.Cap.ROUND); до конца, где вы создали mPrimaryPaint, и это решило мою проблему - person hamid_c; 16.01.2016

Я смог добиться этого, используя список слоев и добавив точку с каждой стороны линии. Первый будет застрять наверху, а второй будет следовать за прогрессом, поскольку внутри элемента вращения добавляется вставка. Однако вам придется настроить его в соответствии с размером вашего макета индикатора выполнения. У меня 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>

У меня здесь нет вторичного процесса, но вы также сможете настроить его для этого.

введите описание изображения здесьвведите здесь описание изображения

person Jon    schedule 05.07.2019
comment
Как выглядит ваш элемент ProgressBar в макете? Я пытался установить этот drawable как атрибут android:progressDrawable безрезультатно. - person Ian Butler; 18.09.2019
comment
работает, отлично. Единственным недостатком является то, что вам нужно изменить значения, если размер отличается от 250dp. - person Redar; 12.04.2020
comment
Люблю это! очень хорошо сделано. Однако мне нужно, чтобы начало прогресса было квадратным. Кроме того, если progress=0, точка все еще отображается. Как не иметь точки, когда прогресс = 0? - person EPAgg; 05.01.2021
comment
Меня устраивает. Это то, что я ищу. Просто нужно немного подкорректировать 'bottom' и 'insetBottom'. Я вообще не понимаю, почему и как это работает. Размер моего индикатора выполнения составляет 92 dp, толщина - 6 dp, не знаю, почему мне нужно установить bottom, а insetBottom - 67,5. Объясните пожалуйста, если знаете ответ. - person Nguyen; 24.02.2021

Простой и эффективный класс, расширяющий View для рисования кругового прогресса с закругленными углами в качестве опции. Цвет прогресса, цвет фона, ширина штриха также настраиваются. Как видно из другого моего ответа.

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()
  }
}
person EyesClear    schedule 25.01.2019

вы можете добавить круглую/овальную форму на конце и в начале кольца, как показано ниже.

список слоев, который можно рисовать, содержит две формы круга / овала, которые можно рисовать, и форму кольца, которую можно рисовать

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>

демонстрация

person Yessy    schedule 06.06.2018

Я попытался решить эту проблему для индикаторов выполнения произвольного размера, и если вы можете убедиться, что индикатор выполнения является квадратным, и вы не используете поля, это должно работать, но это не так. Android 10 иногда отображает это правильно, иногда нет. Особенно значения прогресса> 50% имеют тенденцию к поломке. Это не рабочее решение, но может дать кому-то идею. Есть еще проблемы с градиентной заливкой. Конечный цвет ему не соответствует. Но если вы используете сплошные цвета, все должно работать нормально. Кроме того, закругленный колпачок приводит к тому, что полоса показывает больший диапазон значений. Моя идея состояла в том, чтобы заменить кольцо формой, которая представляет собой прямоугольник размером 5dp x 2,5dp в цвете фона с обрезанной половиной круга. Вам нужно будет использовать разные вращения для начала и конца:

<?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>
person hohteri    schedule 13.05.2020