0

How can we fill a drawn circle (or any other shape) partially (e.g. from the bottom to the top) ?

And how to put the percentage's number inside this circle ?

Here's a preview, but for Android environment : Draw a circle partially filled dynamically

Community
  • 1
  • 1
Omar
  • 1,430
  • 1
  • 14
  • 31

1 Answers1

2

Draw the outline, then add clipping and draw the filled version on top of it, then draw the percentage string on top of that.

import java.text.NumberFormat;
import java.util.Objects;

import java.awt.EventQueue;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.FontMetrics;

import java.awt.Dimension;
import java.awt.Shape;
import java.awt.Rectangle;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.font.TextLayout;

import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;

import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class ShapeFiller
extends JPanel {
    private static final long serialVersionUID = 1;

    private final Shape shape;

    private final Timer fillTimer;

    private float percentageFilled;

    private final NumberFormat percentageFormat;

    private final Rectangle textBounds = new Rectangle();
    private final Rectangle iconBounds = new Rectangle();

    public ShapeFiller(Shape shape) {
        this(shape, 250);
    }

    public ShapeFiller(Shape shape,
                       int fillDelayMillis) {
        this.shape = Objects.requireNonNull(shape, "Shape cannot be null");
        this.percentageFormat = NumberFormat.getPercentInstance();
        this.fillTimer = new Timer(Math.max(50, fillDelayMillis),
            new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent event) {
                    incrementPercentage();
                }
            });

        setFont(getFont().deriveFont(getFont().getSize2D() * 4));
    }

    @Override
    public Dimension getPreferredSize() {
        Rectangle bounds = shape.getBounds();
        bounds.add(0, 0);
        return bounds.getSize();
    }

    @Override
    protected void paintComponent(Graphics graphics) {
        super.paintComponent(graphics);

        Graphics2D g = (Graphics2D) graphics;
        g.draw(shape);

        // Fill shape 

        Shape originalClip = g.getClip();

        Rectangle bounds = shape.getBounds();
        int clipHeight = (int) (bounds.height * percentageFilled);
        g.clip(new Rectangle(0, bounds.y + bounds.height - clipHeight,
            getWidth(), clipHeight));
        g.fill(shape);

        g.setClip(originalClip);

        // Draw percentage

        String text = percentageFormat.format(percentageFilled);
        TextLayout layout =
            new TextLayout(text, g.getFont(), g.getFontRenderContext());

        FontMetrics metrics = g.getFontMetrics();
        iconBounds.setBounds(0, 0, 0, 0);
        textBounds.setBounds(0, 0, 0, 0);
        SwingUtilities.layoutCompoundLabel(this,
            metrics, text, null,
            SwingConstants.CENTER, SwingConstants.CENTER,
            SwingConstants.CENTER, SwingConstants.CENTER,
            bounds, iconBounds, textBounds, 0);
        int textX = textBounds.x;
        int textY = textBounds.y + metrics.getAscent();

        Paint originalPaint = g.getPaint();
        g.setPaint(getBackground());
        layout.draw(g, textX, textY);
        g.setPaint(originalPaint);

        // Draw percentage outline

        AffineTransform textPos =
            AffineTransform.getTranslateInstance(textX, textY);
        g.draw(layout.getOutline(textPos));
    }

    public void setPercentageDone(float percentage) {
        if (Float.isNaN(percentage) || Float.isInfinite(percentage)) {
            throw new IllegalArgumentException("Percentage must be finite");
        }
        percentageFilled = Math.max(0, Math.min(1, percentage));
        repaint();
    }

    public void startFilling() {
        percentageFilled = 0;
        fillTimer.restart();
    }

    private void incrementPercentage() {
        setPercentageDone(percentageFilled + 0.01f);
        if (percentageFilled >= 1) {
            fillTimer.stop();
        }
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                Shape shape = new Ellipse2D.Float(50, 50, 200, 200);
                ShapeFiller shapeFiller = new ShapeFiller(shape);
                shapeFiller.startFilling();
                JOptionPane.showMessageDialog(null, shapeFiller);
                System.exit(0);
            }
        });
    }
}
VGR
  • 40,506
  • 4
  • 48
  • 63
  • Interesting, I'm gonna try it. – Omar Jan 19 '16 at 20:27
  • @VGR, the Timer's constructor inside the 2nd class's constructor has problem with the 2nd parameter.. it doesn't fit the signature `Timer(int delay, ActionListener listener)` . – Omar Jan 19 '16 at 21:00
  • @Omar Are you using Java 8? – VGR Jan 19 '16 at 21:06
  • @Omar Replaced lambda with Java 7 code. (You should consider moving to Java 8.) – VGR Jan 19 '16 at 21:28
  • Ok, thank you. (I'm using Netbeans 7.0.1. I think that Java 8 requires 7.4 or 8 for the IDE versions). – Omar Jan 19 '16 at 21:38
  • @VGR, after bit modification (to remove timer), it worked nicely. Thank you very much. – Omar Jan 20 '16 at 21:52