package magic.ui.widget.throbber; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Transparency; import java.awt.image.BufferedImage; import javax.swing.JComponent; import javax.swing.SwingUtilities; import magic.ui.helpers.ImageHelper; import org.pushingpixels.trident.Timeline; import org.pushingpixels.trident.Timeline.RepeatBehavior; /** * TODO * */ @SuppressWarnings("serial") public abstract class AbstractThrobber extends JComponent { public enum SpinDirection { CLOCKWISE, ANTICLOCKWISE; } private final Timeline timeline = new Timeline(); private BufferedImage currentFrameImage = null; private final boolean isAntiAliasOn; private final SpinDirection spinDirection; private final boolean isDebugMode; // Indicates whether color was set via builder property. protected final boolean isColorSet; // each sub-class uses this to draw a single frame. abstract protected void drawFrame(final Graphics2D g2, final int angle); protected static abstract class Init<T extends Init<T>> { protected abstract T self(); // optional properties with default values. // these are common to all AbstractThrobbers sub-classes. private boolean antiAlias = true; private SpinDirection spinDirection = SpinDirection.CLOCKWISE; private int spinPeriod = 2000; private Color color = null; private boolean isDebugMode = false; /** * Invariably produces a smoother looking image (default is {@code true}). */ public T antiAlias(boolean val) { this.antiAlias = val; return self(); } /** * Default is {@code SpinDirection.CLOCKWISE}. */ public T spinDirection(SpinDirection val) { this.spinDirection = val; return self(); } /** * The time it should take to perform one revolution. Default is 2000 milliseconds. */ public T spinPeriod(int val) { this.spinPeriod = val; return self(); } /** * Defaults to the container foreground color. */ public T color(Color c) { this.color = c; return self(); } /** * Displays bounds and center point. */ public T debugMode(boolean val) { this.isDebugMode = val; return self(); } } public static class Builder extends Init<Builder> { @Override protected Builder self() { return this; } } protected AbstractThrobber(final Init<?> init) { this.isDebugMode = init.isDebugMode; this.spinDirection = init.spinDirection; this.isAntiAliasOn = init.antiAlias; this.setForeground(init.color); isColorSet = (init.color != null); startTimeline(init.spinPeriod); } /** * Must be public to work with Trident time-line. */ public void setAngle(final int angle) { paintNextFrame(angle); } private void paintNextFrame(final int angle) { // draw the next frame to an off-screen image buffer on same // thread as timeline thread so that the impact on the EDT is // kept to a minimum. assert SwingUtilities.isEventDispatchThread() == false; BufferedImage image = getNextFrameImage(angle); if (image != null) { // All the EDT needs to do is draw the image. SwingUtilities.invokeLater(() -> { currentFrameImage = image; repaint(); }); } } private BufferedImage getNextFrameImage(final int angle) { final int W = getWidth(); final int H = getHeight(); if (W <= 0 || H <= 0) { return null; } final BufferedImage image = ImageHelper.getCompatibleBufferedImage(W, H, Transparency.TRANSLUCENT); final Graphics2D g2d = image.createGraphics(); if (isAntiAliasOn) { g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // These make a visible difference producing smoother looking images... g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); } drawFrame(g2d, angle); return image; } protected double getRadians(final double degrees) { return Math.PI * degrees / 180; } private void startTimeline(final int period) { timeline.abort(); timeline.addPropertyToInterpolate( Timeline.property("angle") .on(this) .from(0) .to(356)); timeline.setDuration(period); timeline.playLoop(RepeatBehavior.LOOP); } /** * keep the amount of work this has to do to the absolute minimum for smoother animation. */ @Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (isDebugMode) { drawCrosshair(g); } if (currentFrameImage != null) { g.drawImage(currentFrameImage, 0, 0, null); } } protected SpinDirection getSpinDirection() { return spinDirection; } private void drawCrosshair(final Graphics g) { final int W = getWidth(); final int H = getHeight(); final int cX = W / 2; final int cY = H / 2; g.setColor(Color.red); g.drawLine(0, cY, W, cY); g.drawLine(cX, 0, cX, H); g.drawRect(0, 0, W - 1, H - 1); } @Override public void setVisible(boolean b) { super.setVisible(b); if (timeline != null) { if (b && timeline.getState() == Timeline.TimelineState.SUSPENDED) { timeline.resume(); } else if (!b && timeline.getState() == Timeline.TimelineState.PLAYING_FORWARD) { timeline.suspend(); } } } }