Analogue Clock

Your browser is completely ignoring the tag!
Your browser is completely ignoring the tag!
Your browser is completely ignoring the tag!

London

Paris

New York

If the applets are not running you may need to add an exception to the Java Control Panel.

The following code paints an analogue clock face onto a graphics context. It's not a complete and final solution, there are few minor issues with the painting of the clock numbers at certain scales etc, however it does work for most component sizes and is reasonably efficient.

License

This software is released under the GNU GPLv3 license


Clock Face Painter

This class paints a clock face onto the graphic context provided by the component that displays the clock. To use this class see the Usage section.

/**
 * Copyright (c) 2006, 2010 Keang Ltd. All Rights Reserved.
 *
 * This class is based on the Clock2.java program from Sun Microsystems Inc
 * @(#)Clock2.java	1.8 97/01/24
 * Copyright (c) 1994-1996 Sun Microsystems, Inc. All Rights Reserved.
 *
 */

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

/**
 * Paints an analogue clock face and hands using the graphics object provided
 * by another component.
 *
 *@author Keang Ltd
 *@version 1.0, 12/09/2006
 */
class AnalogueClockPainter
{
private static final int HOURS = 12;
private static final int MINUTES = 60;
private static final int SECONDS = 60;
private static final int HOUR_HAND_INC = 4;
private static final double HOUR_HAND_TH = 0.25;
private static final double MIN_HAND_TH = 0.1;
private static final double HUB_TH = 0.025;

private static final int EXT_TICK_TIME_SLOT_MINUTES = 15;
private static final int N_EXT_TICKS = 12 * 60 / EXT_TICK_TIME_SLOT_MINUTES;


private static final int X = 0;
private static final int Y = 1;

private GregorianCalendar cal;
private Font font = new Font("SansSerif", Font.PLAIN, 12);
private Color handCol = Color.blue;
private Color secHandCol = Color.blue;
private Color textCol = Color.darkGray;
private Color faceCol = Color.white;
private Color rimCol = Color.black;
private Color[] extTickCol = new Color[] {Color.green, Color.yellow, Color.red.brighter()};
private boolean showSeconds;
private boolean showNumbers = true;
private int digitwd;
private int ascent;
private int clockRadius;
private int hub;
private Date time;

private ArrayList<ExtTick> extTicks = new ArrayList<ExtTick>();
private Object extTicksLock = new Object();
private int[][][] extTickPos = new int[N_EXT_TICKS][2][4];
private int[][][] tickPos = new int[HOURS][2][2];
private int[][] numberPos = new int[HOURS][2];
private int[][] secHandPos = new int[SECONDS][2];
private int[][][] minHandPos = new int[MINUTES][2][4];
private int[][][][] hourHandPos = new int[HOURS][MINUTES/HOUR_HAND_INC][2][4];

private long[] extTickTimes;
private boolean showExternalTicks;

/**
 * Constructs a default clock painter for this time zone
 *
 */
public AnalogueClockPainter()
    {
    this(TimeZone.getDefault());
    }

/**
 * Constructs a clock painter for the given time zone
 *
 *@param timezone - the timezone to use
 */
public AnalogueClockPainter(TimeZone timezone)
    {
    cal = new GregorianCalendar(timezone);
    setSecondsEnabled(true);

    time = new Date();
    }

/**
 * Set the radius of this analogue clock face
 *
 *@param rad - the radius in pixels
 */
public void setRadius(int rad)
    {
    if ( rad <= 0 )
        throw new IllegalArgumentException("Radius ("+rad+") must be greater than 0 ");

    clockRadius = rad;

    preCalcHands();
    preCalcTicks();
    preCalcExtTicks();
    preCalcFaceNumbers();
    }

/**
 * Set the times of the ticks to show on the exterior of the rim.
 *
 *@param times - an array of times in seconds denoting the position of a tick
 */
public void setExternalTicks(long[] times)
    {
    extTickTimes = Arrays.copyOf(times, times.length);
    preCalcExtTicks(extTickTimes);
    }

/**
 * Turn display of seconds and seconds hand on or off.
 *
 *@param state - true to show seconds hand
 **/
public void setSecondsEnabled(boolean state)
    {
    showSeconds = state;
    }

/**
 * Turn display of clock face numbers on or off.
 *
 *@param state - true to show clock face numbers 1 to 12
 **/
public void setNumbersEnabled(boolean state)
    {
    showNumbers = state;
    }

/**
 * Set the clock to a certain time zone. Example call:
 * 
 * clock.setTimeZone(TimeZone.getTimeZone(tz));
 * 
* @param tz the time zone **/ public void setTimeZone(TimeZone tz) { cal = new GregorianCalendar(tz); } /** * Set the clock to a certain time. * * @param d the time to display **/ public void setTime(Date d) { if ( d == null ) throw new NullPointerException("Date cannot be null"); time = d; } /** * Set the font for the numbers * * @param f the font to use **/ public void setFontMetrics(FontMetrics fm) { font = fm.getFont(); digitwd = fm.charWidth('8'); ascent = fm.getAscent(); preCalcFaceNumbers(); } /** * Set the color of the minute and hour hand * * @param c - the color for the clock hands **/ public void setHandsColor(Color c) { handCol = c; } /** * Set the color for the text and the seconds hand. * * @param c - the color for the text and the seconds hand **/ public void setTextColor(Color c) { textCol = c; secHandCol = c; } /** * Set the color for the dial's face. * * @param c - the color of the dial or null for a transparent face **/ public void setFaceColor(Color c) { faceCol = c; } /** * Set the color for the dial's rim. * * @param c - the color of the rim or null for no rim **/ public void setRimColor(Color c) { rimCol = c; } /** * Set the colors for the external ticks. The first element of the array is used * first and if lines are drawn over the same line subsequent colours are used until * the last colour is reached. The array can be any length * * @param c - the colors of the external ticks **/ public void setExternalTickColor(Color[] c) { extTickCol = c; } /** * Paint the clock, repainting everything, not just what changed * since the last call to paint. **/ public void paint(Graphics g) { int s, m, h; cal.setTime(time); s = cal.get(Calendar.SECOND); m = cal.get(Calendar.MINUTE); h = cal.get(Calendar.HOUR_OF_DAY); int dia = clockRadius * 2; // draw the clock face if ( faceCol != null ) { g.setColor(faceCol); g.fillOval(0, 0, dia, dia); } // draw the clock face rim if ( rimCol != null ) { g.setColor(rimCol); g.drawOval(0,0, dia, dia); } // draw the ticks or the numbers g.setColor(textCol); if ( showNumbers ) { // draw the digits g.setFont(font); for ( int i = 0; i < numberPos.length; ++i ) g.drawString(String.valueOf(i+1), numberPos[i][X], numberPos[i][Y]); } else { // draw the ticks for ( int i = 0; i < tickPos.length; ++i ) g.drawLine(tickPos[i][0][X], tickPos[i][0][Y], tickPos[i][1][X], tickPos[i][1][Y]); } // change the hour hand from 24 hour to 12 hour format if ( h >= HOURS ) h -= HOURS; int hm = m/HOUR_HAND_INC; // get the position of the hour hand int[] xh = hourHandPos[h][hm][X]; int[] yh = hourHandPos[h][hm][Y]; // draw the hour hand g.setColor(handCol); g.fillPolygon(xh, yh, xh.length); // draw the hands hub int hubD = hub<1; g.fillOval(clockRadius-hub, clockRadius-hub, hubD, hubD); // get the position of the minute hand int[] xm = minHandPos[m][X]; int[] ym = minHandPos[m][Y]; // draw the minute hand with a darker outline so that if the minute and // hour hand overlap the minute hand appears to be on top and doesn't merge // with the hour hand. g.setColor(handCol.darker()); g.drawPolygon(xm, ym, xm.length); g.setColor(handCol); g.fillPolygon(xm, ym, xm.length); if (showSeconds) { // draw the seconds hand g.setColor(secHandCol); g.drawLine(clockRadius, clockRadius, secHandPos[s][X], secHandPos[s][Y]); } // draw the external ticks synchronized ( extTicksLock ) { if ( showExternalTicks && extTickCol != null && extTicks.size() > 0 ) { for ( int i = 0, size = extTicks.size(); i < size; i++ ) { ExtTick tmp = extTicks.get(i); g.setColor(tmp.col); // get the position of the external tick int[] xt = extTickPos[tmp.pos][X]; int[] yt = extTickPos[tmp.pos][Y]; // g.drawPolygon(xt, yt, xt.length); g.fillPolygon(xt, yt, xt.length); } } } } /** * Precalculate the hand positions * */ private void preCalcHands() { // get the relative lengths of each hand int rs = 8 * clockRadius/9; int rm = rs; int rh = 6 * rm/9; float aFactor = (float)(Math.PI/30); float halfPI = (float)(Math.PI/2); // seconds hand for ( int i = 0; i < SECONDS; ++i ) { float ang = i * aFactor - halfPI; secHandPos[i][X] = (int)(Math.cos(ang) * rs + clockRadius); secHandPos[i][Y] = (int)(Math.sin(ang) * rs + clockRadius); } // minute hand for ( int i = 0; i < MINUTES; ++i ) { float ang = i * aFactor - halfPI; minHandPos[i][X][0] = (int)(Math.cos(ang) * rm + clockRadius + 0.5); minHandPos[i][Y][0] = (int)(Math.sin(ang) * rm + clockRadius + 0.5); minHandPos[i][X][1] = (int)(Math.cos(ang + MIN_HAND_TH) * rm/3 + clockRadius + 0.5); minHandPos[i][Y][1] = (int)(Math.sin(ang + MIN_HAND_TH) * rm/3 + clockRadius + 0.5); minHandPos[i][X][2] = clockRadius; minHandPos[i][Y][2] = clockRadius; minHandPos[i][X][3] = (int)(Math.cos(ang - MIN_HAND_TH) * rm/3 + clockRadius + 0.5); minHandPos[i][Y][3] = (int)(Math.sin(ang - MIN_HAND_TH) * rm/3 + clockRadius + 0.5); } // hour hand aFactor = (float)(Math.PI/180); for ( int i = 0; i < HOURS; ++i ) { for ( int j = 0; j < MINUTES/HOUR_HAND_INC; ++j ) { int m = i*30 + j*HOUR_HAND_INC/2; float ang = m * aFactor - halfPI; hourHandPos[i][j][X][0] = (int)(Math.cos(ang) * rh + clockRadius + 0.5); hourHandPos[i][j][Y][0] = (int)(Math.sin(ang) * rh + clockRadius + 0.5); hourHandPos[i][j][X][1] = (int)(Math.cos(ang + HOUR_HAND_TH) * rh/3 + clockRadius + 0.5); hourHandPos[i][j][Y][1] = (int)(Math.sin(ang + HOUR_HAND_TH) * rh/3 + clockRadius + 0.5); hourHandPos[i][j][X][2] = clockRadius; hourHandPos[i][j][Y][2] = clockRadius; hourHandPos[i][j][X][3] = (int)(Math.cos(ang - HOUR_HAND_TH) * rh/3 + clockRadius + 0.5); hourHandPos[i][j][Y][3] = (int)(Math.sin(ang - HOUR_HAND_TH) * rh/3 + clockRadius + 0.5); } } // hands centre circle hub = (int)(clockRadius*HUB_TH); if ( hub < 3 ) hub = 3; } /** * Precalculate the face tick positions. Ticks are lines at the positions * that numbers would normally be displayed. * */ private void preCalcTicks() { double inc = 2 * Math.PI / HOURS; double ang = 0 - (Math.PI/2); int r2 = clockRadius * 9 / 10; int r3 = clockRadius * 19 / 20; for ( int i = 0; i < HOURS; ++i ) { ang += inc; double cosA = Math.cos(ang); double sinA = Math.sin(ang); // create bigger ticks for the 3, 6, 9, 12 positions if ( i % 3 == 2 ) { tickPos[i][0][X] = (int)(cosA*r2)+clockRadius; tickPos[i][0][Y] = (int)(sinA*r2)+clockRadius; } else { tickPos[i][0][X] = (int)(cosA*r3)+clockRadius; tickPos[i][0][Y] = (int)(sinA*r3)+clockRadius; } tickPos[i][1][X] = (int)(cosA*clockRadius)+clockRadius; tickPos[i][1][Y] = (int)(sinA*clockRadius)+clockRadius; } } /** * Precalculate the face number positions * */ private void preCalcFaceNumbers() { int r2 = clockRadius - (ascent/2 > digitwd ? ascent/2 : digitwd) - 2; numberPos[0][X] = clockRadius + r2/2 - digitwd/2; numberPos[0][Y] = clockRadius - 87 * r2/100 + ascent/2; numberPos[1][X] = clockRadius + 87*r2/100 - digitwd/2; numberPos[1][Y] = clockRadius - r2/2 + ascent/2; numberPos[2][X] = clockRadius + r2 - digitwd/3; numberPos[2][Y] = clockRadius + (2*ascent/5); numberPos[3][X] = clockRadius + 87*r2/100 - digitwd/2; numberPos[3][Y] = clockRadius + r2/2 + ascent/2; numberPos[4][X] = clockRadius + r2/2 - digitwd/2; numberPos[4][Y] = clockRadius + 87 * r2/100 + (2*ascent/5); numberPos[5][X] = clockRadius - digitwd/2; numberPos[5][Y] = clockRadius + r2 + (2*ascent/5); numberPos[6][X] = clockRadius - r2/2 - digitwd/2; numberPos[6][Y] = clockRadius + 87 * r2/100 + (2*ascent/5); numberPos[7][X] = clockRadius - 87 * r2/100 - digitwd/2; numberPos[7][Y] = clockRadius + r2/2 + ascent/2; numberPos[8][X] = clockRadius - r2 - (2*digitwd/3); numberPos[8][Y] = clockRadius + (2*ascent/5); numberPos[9][X] = clockRadius - 87 * r2/100 - digitwd; numberPos[9][Y] = clockRadius - r2/2 + ascent/2; numberPos[10][X] = clockRadius - r2/2 - digitwd; numberPos[10][Y] = clockRadius - 87 * r2/100 + ascent/2; numberPos[11][X] = clockRadius - digitwd; numberPos[11][Y] = clockRadius - r2 + (2*ascent/5); } /** * Precalculate the external rim tick positions. * * This assumes the times will be in numeric order */ private void preCalcExtTicks() { double inc = 2 * Math.PI / N_EXT_TICKS; // calc the length of the tick int r2 = clockRadius * 23 / 20; double[] ang = new double[2]; ang[1] -= (Math.PI / 2) + (inc / 2); for ( int i = 0; i < N_EXT_TICKS; i++ ) { // calc the angle of the tick ang[0] = ang[1]; ang[1] += inc; double cosA = Math.cos(ang[0]); double sinA = Math.sin(ang[0]); extTickPos[i][X][0] = (int)(cosA*r2)+clockRadius; extTickPos[i][Y][0] = (int)(sinA*r2)+clockRadius; extTickPos[i][X][1] = (int)(cosA*clockRadius)+clockRadius; extTickPos[i][Y][1] = (int)(sinA*clockRadius)+clockRadius; cosA = Math.cos(ang[1]); sinA = Math.sin(ang[1]); extTickPos[i][X][2] = (int)(cosA*clockRadius)+clockRadius; extTickPos[i][Y][2] = (int)(sinA*clockRadius)+clockRadius; extTickPos[i][X][3] = (int)(cosA*r2)+clockRadius; extTickPos[i][Y][3] = (int)(sinA*r2)+clockRadius; } } /** * Precalculate the external rim tick positions. * * This assumes the times will be in numeric order */ private void preCalcExtTicks(long[] times) { if ( times == null || times.length == 0 ) { synchronized ( extTicksLock ) { extTicks.clear(); } return; } ArrayList<ExtTick> ticks = new ArrayList<ExtTick>(); // turn times into 12 hour clock long midday = 12 * 60 * 60 * 1000; for ( int i = 0; i < times.length; i++ ) { // if time is 12 - 24 hours then change it to 0 - 12 hours if ( times[i] > midday ) times[i] -= midday; } // sort the times into order Arrays.sort(times); long prevT = -1; ExtTick prevTick = null; for ( int i = 0; i < times.length; ++i ) { // round the time to the nearest time slot long t = times[i]; // round to the nearest n minutes t = ((t + (EXT_TICK_TIME_SLOT_MINUTES * 60 * 1000/2))/ (EXT_TICK_TIME_SLOT_MINUTES * 60 * 1000)); if ( t == prevT && prevTick != null ) { prevTick.brighter(); continue; } prevT = t; if ( t >= N_EXT_TICKS ) { t = N_EXT_TICKS-1; } prevTick = new ExtTick(t); ticks.add(prevTick); } synchronized ( extTicksLock ) { extTicks = ticks; } } public void setExternalTicksEnabled(boolean enable) { showExternalTicks = enable; } private class ExtTick { int pos; Color col; int colNum = 0; ExtTick(long index) { pos = (int)index; col = extTickCol[colNum]; } void brighter() { colNum++; if ( colNum >= extTickCol.length ) colNum = extTickCol.length - 1; col = extTickCol[colNum]; } } }

Usage

To use the class you need to include the following code in your component. If you are using one of the standard components such as JButton create your own class that extends the component and add the following code.

This example uses a JButton but this technique will work with any object that extends JComponent and could easily be made to work with AWT components.

    public class ClockButton extends JButton
    {
    private final static int BORDER_SIZE = 12;
    private AnalogueClockPainter clock = new AnalogueClockPainter();
    private int originX, originY;

    ...
    add the Constructors here. The constructors must call initClock()
    ...


    /**
     * initialise the clock
     *
     */
    private void initClock()
        {

        // this method should be called
        clock.setFontMetrics(getFontMetrics(getFont()));

        // these methods are optional and just configure the clock display
        clock.setExternalTicksEnabled(true);

        clock.setTextColor(Color.BLACK);
        clock.setFaceColor(Color.YELLOW);
        clock.setHandsColor(Color.RED);

        clock.setSecondsEnabled(true);


        // add a size change listener to set the clock size if the button size changes
        addComponentListener(new ComponentAdapter()
            {
            public void componentResized(ComponentEvent e)
                {
                Dimension s = getSize();

                int min = Math.min(s.width, s.height);

                if ( min > BORDER_SIZE )
                    min -= BORDER_SIZE;

                // get the drawing origin to position the clock in the centre of the component
                originX = (s.width-min+1)/2;
                originY = (s.height-min+1)/2;

                clock.setRadius(min/2);

                repaint();
                }
            } );

        // create a swing timer to fire every second and update the clock display
        // if you are not displaying the second hand then set this time to a greater
        // value ie 15 seconds or 1 minute
        Timer t = new Timer(1000, new ActionListener()
            {
            Date date = new Date();

            @Override
            public void actionPerformed(ActionEvent e)
                {
                date.setTime(System.currentTimeMillis());
                clock.setTime(date);
                repaint();
                }
            });

        t.setRepeats(true);
        t.start();
        }

    /**
     * Overrides paint to paint the label onto the face of the button. Uses
     * super.paintComponent() method to draw the button.
     *
     *@param g - the graphics object used to draw this button
     */
    public void paintComponent(Graphics g)
        {
        super.paintComponent(g);

        Graphics cg = g.create();

        cg.translate(originX, originY);
        clock.paint(cg);

        cg.dispose();
        }
    }
    

The initClock() method should be called from the Constructor to initialise the AnalogueClockPainter object. The size change listener is necessary if the component's size may change once it is displayed ie through the user changing the window size. If the components size is guaranteed to be fixed or you don't want the clock size to change then just call the setRadius() method once the clock size is known.

The timer is only required if the clock is displaying real time ie you want the hands to move in real time. If the clock is displaying a fixed time, just call the setDate() method instead.

To improve efficiency add a ComponentListener to stop/start the timer as the component is hidden/shown.

 
Back to the Top