androidcanvasviewporter-duff

Clear a zone outside a specific circle with canvas


I have a custom view which show a progress by filling a circle but now I'm looking for a way to erase the zone of my view outside this white circle :

enter image description here

Here, my code :

public class CircleGauge extends View {
    private int value = 75;
    private Paint backgroundPaint, gaugePaint, textPaint, circlePaint;

    ... constructors 

    private void init() {
        DisplayMetrics metrics = getResources().getDisplayMetrics();

        backgroundPaint = new Paint();
        backgroundPaint.setColor(ResourcesCompat.getColor(getResources(), R.color.favorite_position_gauge_background, null));
        backgroundPaint.setStyle(Paint.Style.FILL);
        backgroundPaint.setAntiAlias(true);

        gaugePaint = new Paint();
        gaugePaint.setColor(ResourcesCompat.getColor(getResources(), R.color.favorite_position_gauge, null));
        gaugePaint.setAntiAlias(true);

        circlePaint = new Paint();
        circlePaint.setColor(Color.WHITE);
        circlePaint.setStyle(Paint.Style.STROKE);
        circlePaint.setStrokeWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, metrics));
        circlePaint.setAntiAlias(true);

        textPaint = new Paint();
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setColor(Color.WHITE);
        textPaint.setFakeBoldText(true);
        textPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 23, metrics));
        textPaint.setAntiAlias(true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), backgroundPaint);
        canvas.drawRect(0, ((float) (100 - value) / 100F) * canvas.getHeight(), canvas.getWidth(), canvas.getHeight(), gaugePaint);
        canvas.drawText(getContext().getString(R.string.percent_value, value), getWidth() / 2, getHeight() * .6F, textPaint);
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, (getHeight() / 2) - circlePaint.getStrokeWidth() / 2, circlePaint);
    }

    public void setValue(int value) {
        this.value = value;
        invalidate();
    }
}

I pretty sure I can do this with PorterDuff but I don't know how.


Solution

  • Android drawable resources already give you everything you need to make this without resorting to subclassing View and overriding onDraw.

    First let's start with the drawable resource. We want to have a circle with one color covering a circle of another color. For this we use a LayerDrawable which is specified in XML with <layer-list>.

    Each Drawable has a level value. You can adjust the level (0-10000) by calling setLevel on the Drawable. We want to use the level to control the appearance of the lighter circle. For that we will use a ClipDrawable which is defined in XML with <clip>.

    For the circles themselves, we can use ShapeDrawables (<shape>). Here's what it looks like when we put it all together:

    <?xml version="1.0" encoding="utf-8"?>
    <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    
        <item android:id="@+id/field" android:gravity="fill">
            <shape android:shape="oval">
                <stroke android:width="2dp" android:color="@android:color/white"/>
                <solid android:color="#FF78606D"/>
            </shape>
        </item>
    
        <item android:id="@+id/progress" android:gravity="fill">
            <clip android:gravity="bottom" android:clipOrientation="vertical">
                <shape android:shape="oval" >
                    <stroke android:width="2dp" android:color="@android:color/white"/>
                    <solid android:color="#FFAB9BA6"/>
                </shape>
            </clip>
        </item>
    
    </layer-list>
    

    Now we can just use a `TextView and put this drawable as the background.

        <TextView
            android:id="@+id/text"
            android:layout_height="64dp"
            android:layout_width="64dp"
            android:layout_gravity="center"
            android:background="@drawable/circle_progress"
            android:gravity="center"
            android:textAppearance="?android:textAppearanceLarge"
            android:text="0%"/>
    

    Based on your colors I used Theme.AppCompat which is a dark theme.

    Here's a quick and dirty demo that shows how it all works together:

    public class MainActivity extends AppCompatActivity {
    
        private TextView mTextView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            mTextView = (TextView) findViewById(R.id.text);
            setLevel(0);
            new AsyncTask<Void, Integer, Void>() {
                @Override
                protected Void doInBackground(Void... params) {
    
                    try {
                        for (int i = 0; i <= 100; i++) {
                            publishProgress(i);
                            Thread.sleep(100);
                        }
                    } catch (InterruptedException e) {
                        // just leave
                    }
    
                    return null;
                };
    
                @Override
                protected void onProgressUpdate(Integer... values) {
                    setLevel(values[0]);
                }
            }.execute();
    
        }
    
        private void setLevel(int level) {
    
            mTextView.setText(Integer.toString(level) + "%");
            LayerDrawable layerDrawable = (LayerDrawable) mTextView.getBackground();
            Drawable progress = layerDrawable.findDrawableByLayerId(R.id.progress);
            progress.setLevel(level * 100); // drawable levels go 0-10000
        }
    
    }