Musings about Java and Swing

Sliding Clock Using Java Swing

I enjoy creating clocks using Java Swing. I’ve already written an article about a clock, Qlocktwo with Java Swing.

Introduction

Back when digital clocks were first conceived, someone created a mechanical digital clock. The numbers were printed on plastic wheels, which rotated from one digit to the next. In honor of those mechanical digital clocks, I created the sliding clock. Here’s the Swing GUI I created.

Sliding Clock Swing GUI

The font is Dialog Bold, 48 point. The font is large because I have poor vision. You can’t tell from the picture, but the digits slide upward to reveal the next digit. The main purpose of this article is to show you how I achieved the sliding effect.

Overview

I wrote 7 classes. One class is an internal Runnable class. The rest of the classes create the GUI model and the GUI view. I use the model / view / controller pattern (MVC).

When I say I use the model / view / controller pattern with Java Swing, I mean:

  1. The view may read values from the model.
  2. The view may not update the model.
  3. The controller will update the model.
  4. The controller will repaint / revalidate the view.

Basically, the model is ignorant of the view and controller. This allows you to change the view and controller from Swing to a web site, or an Android app.

The model / view / controller pattern allows you to focus on one part of the Swing GUI at a time. In general, you’ll create the model first, then the view, and finally the controllers. You will have to go back and add fields to the model. I guarantee that you’ll come up with something you didn’t think of when you created the first cut of the model classes.

Because this is a clock, there are no controls on the GUI and no controller classes. A Runnable, running in a separate thread, updates the GUI model and view. The Runnable acts as the controller for this Java Swing application.

I used Windows 10 and Java 8 to create the Swing GUI.

Now for the code. The first class is the main SlidingClock class.

package com.ggl.sliding.clock;

import javax.swing.SwingUtilities;

import com.ggl.sliding.clock.model.SlidingClockModel;
import com.ggl.sliding.clock.view.SlidingClockFrame;

public class SlidingClock implements Runnable {

	public static void main(String[] args) {
		SwingUtilities.invokeLater(new SlidingClock());
	}

	@Override
	public void run() {
		new SlidingClockFrame(new SlidingClockModel());
	}

}

This short class does 3 things.

  1. Puts the Swing components on the Event Dispatch thread (EDT) by executing the invokeLater method of SwingUtilities.
  2. Instantiates the SlidingClockModel class.
  3. Instantiates the SlidingClockFrame class.

A version of this class is how I start all of my Swing applications.

Model

Let’s look at the model class, SlidingClockModel.

package com.ggl.sliding.clock.model;

import java.text.SimpleDateFormat;
import java.util.Calendar;

public class SlidingClockModel {

	private static final SimpleDateFormat OUTPUT_TIME = new SimpleDateFormat("hhmmssa");

	private String hourString;
	private String tenMinuteString;
	private String minuteString;
	private String tenSecondString;
	private String secondString;
	private String meridianString;

	public SlidingClockModel() {
		setCurrentTime();
	}

	public void setCurrentTime() {
		Calendar currentTime = Calendar.getInstance();
		String timeString = OUTPUT_TIME.format(currentTime.getTime());
		this.hourString = convertHourString(timeString);
		this.tenMinuteString = timeString.substring(2, 3);
		this.minuteString = timeString.substring(3, 4);
		this.tenSecondString = timeString.substring(4, 5);
		this.secondString = timeString.substring(5, 6);
		this.meridianString = timeString.substring(6, 8);
	}

	private String convertHourString(String timeString) {
		String hourString = timeString.substring(0, 2);
		int hour = Integer.valueOf(hourString);
		return String.format("%2d", hour);
	}

	public String getHourString() {
		return hourString;
	}

	public String getTenMinuteString() {
		return tenMinuteString;
	}

	public String getMinuteString() {
		return minuteString;
	}

	public String getTenSecondString() {
		return tenSecondString;
	}

	public String getSecondString() {
		return secondString;
	}

	public String getMeridianString() {
		return meridianString;
	}

}

The setCurrentTime method gets the current date and time, converts the time to a time string, and breaks the time string into pieces for the clock display. I use a SimpleDateFormat to convert the time to a time string.

The default time zone is the time zone where the Java Swing application is run. If you want to use a different time zone, you would set the SimpleDateFormat time zone before doing the time conversion.

The remainder of the SlidingClockModel class consists of getters to get the time string components.

View

Now that we’ve defined the model class, let’s look at the view classes. The first view class is the SlidingClockFrame class.

package com.ggl.sliding.clock.view;

import javax.swing.JFrame;

import com.ggl.sliding.clock.model.SlidingClockModel;
import com.ggl.sliding.clock.runnable.ClockRunnable;

public class SlidingClockFrame {

	private ClockPanel clockPanel;

	private JFrame frame;

	private SlidingClockModel model;

	public SlidingClockFrame(SlidingClockModel model) {
		this.model = model;
		createPartControl();
	}

	private void createPartControl() {
		frame = new JFrame("Clock");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		clockPanel = new ClockPanel(model);
		frame.add(clockPanel.getPanel());

		frame.pack();
		frame.setVisible(true);

		ClockRunnable clockRunnable = new ClockRunnable(this, model);
		new Thread(clockRunnable).start();
	}

	public void updatePartControl() {
		clockPanel.updatePartControl();
	}
}

The createPartControl method is pretty much the same for all of my Java Swing projects. For this project, we create a JFrame, and add the ClockPanel (JPanel). For other projects, we’ll add one or more JPanels.

Notice that we don’t extend JFrame. We use a JFrame. The only reason you should extend a Swing component, or any Java class, is when you want to override one or more of the methods.

For most Swing applications, you want to create the Swing components first, then update them as needed. In the createPartControl method, we have the following hierarchy of Swing components.

JFrame -> ClockPanel (JPanel) -> SlidingPanel (JPanel)

The clock panel is sufficiently complicated that I created it in its own class. I created the sliding panel in its own class because we need more than one of them. The SlidingPanel class is a Swing component I created that can be used in other slider projects.

The updatePartControl method updates the Swing components created in the createPartControl method. Generally, when you create a Swing GUI, you create the Swing components and later update the Swing components. Beginner coders tend to get confused and try and delete and recreate Swing components. In the vast majority of GUI’s, this is not only unnecessary, but creates a bad user experience (UX).

Next, we’ll take a look at the ClockPanel class.

package com.ggl.sliding.clock.view;

import java.awt.Color;
import java.awt.Component;
import java.awt.Font;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JPanel;

import com.ggl.sliding.clock.model.SlidingClockModel;

public class ClockPanel {

	private JPanel panel;

	private SlidingClockModel model;

	private SlidingPanel hourPanel;
	private SlidingPanel tenMinutePanel;
	private SlidingPanel minutePanel;
	private SlidingPanel tenSecondPanel;
	private SlidingPanel secondPanel;
	private SlidingPanel meridianPanel;

	public ClockPanel(SlidingClockModel model) {
		this.model = model;
		createPartControl();
	}

	private void createPartControl() {
		panel = new JPanel();
		panel.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY, 4, false));
		panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS));

		Font font = panel.getFont();
		Font derivedFont = font.deriveFont(Font.BOLD, 48F);

		String[] hourValues = { " 1", " 2", " 3", " 4", " 5", " 6", " 7", " 8", " 9", "10", "11", "12" };
		String[] tenValues = { "0", "1", "2", "3", "4", "5" };
		String[] digitValues = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
		String[] meridianValues = { "AM", "PM" };

		hourPanel = new SlidingPanel(hourValues, derivedFont);
		hourPanel.setPanelValue(model.getHourString());
		panel.add(hourPanel);

		panel.add(createSeparatorPanel());

		tenMinutePanel = new SlidingPanel(tenValues, derivedFont);
		tenMinutePanel.setPanelValue(model.getTenMinuteString());
		panel.add(tenMinutePanel);

		minutePanel = new SlidingPanel(digitValues, derivedFont);
		minutePanel.setPanelValue(model.getMinuteString());
		panel.add(minutePanel);

		panel.add(createSeparatorPanel());

		tenSecondPanel = new SlidingPanel(tenValues, derivedFont);
		tenSecondPanel.setPanelValue(model.getTenSecondString());
		panel.add(tenSecondPanel);

		secondPanel = new SlidingPanel(digitValues, derivedFont);
		secondPanel.setPanelValue(model.getSecondString());
		panel.add(secondPanel);

		panel.add(createSeparatorPanel());

		meridianPanel = new SlidingPanel(meridianValues, derivedFont);
		meridianPanel.setPanelValue(model.getMeridianString());
		panel.add(meridianPanel);
	}

	public void updatePartControl() {
		hourPanel.updatePanelValue(model.getHourString());
		tenMinutePanel.updatePanelValue(model.getTenMinuteString());
		minutePanel.updatePanelValue(model.getMinuteString());
		tenSecondPanel.updatePanelValue(model.getTenSecondString());
		secondPanel.updatePanelValue(model.getSecondString());
		meridianPanel.updatePanelValue(model.getMeridianString());
	}

	public JPanel getPanel() {
		return panel;
	}

	private JPanel createSeparatorPanel() {
		JPanel panel = new JPanel();
		panel.setBackground(Color.LIGHT_GRAY);

		Component component = Box.createHorizontalStrut(0);
		panel.add(component);

		return panel;
	}

}

Notice that we don’t extend JPanel. We use a JPanel. The only reason you should extend a Swing component, or any Java class, is when you want to override one or more of the methods.

In the createPartControl method, we create six SliderPanel instances and three divider JPanels. We create a SliderPanel for the hours, tens of minutes, minutes, tens of seconds, seconds, and the meridian (AM or PM). We create the divider panels in the createSeparatorPanel method. The divider panels are simple enough that we create them in a method.

The updatePartControl method updates the Swing components created in the createPartControl method. Generally, when you create a Swing GUI, you create the Swing components and later update the Swing components. Beginner coders tend to get confused and try and delete and recreate Swing components. In the vast majority of GUI’s, this is not only unnecessary, but creates a bad user experience (UX).

Finally, we’ll take a look at the SlidingPanel class.

package com.ggl.sliding.clock.view;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;

import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class SlidingPanel extends JPanel {

	private static final long serialVersionUID = 661553022861652947L;

	private static final int MARGIN = 4;

	private int imageY;

	private BufferedImage slidingImage;

	private Dimension characterDimension;

	private final Font font;

	private String currentValue;

	private final String[] panelValues;

	public SlidingPanel(String[] panelValues, Font font) {
		this.panelValues = panelValues;
		this.font = font;
		this.characterDimension = calculateFontSize();
		this.slidingImage = generateSlidingImage();
		this.setPreferredSize(characterDimension);
	}

	private Dimension calculateFontSize() {
		int maxWidth = 0;
		int maxHeight = 0;
		FontRenderContext frc = new FontRenderContext(null, true, true);
		for (String s : panelValues) {
			Rectangle2D r2D = font.getStringBounds(s, frc);
			int rWidth = (int) Math.round(r2D.getWidth());
			int rHeight = (int) Math.round(r2D.getHeight());
			maxWidth = Math.max(maxWidth, rWidth);
			maxHeight = Math.max(maxHeight, rHeight);
		}

		return new Dimension(maxWidth, maxHeight);
	}

	private BufferedImage generateSlidingImage() {
		int height = calculateStringHeight() * (panelValues.length + 1);
		BufferedImage slidingImage = new BufferedImage(characterDimension.width, height, BufferedImage.TYPE_INT_RGB);
		Graphics g = slidingImage.getGraphics();
		g.setColor(Color.WHITE);
		g.fillRect(0, 0, characterDimension.width, height);
		g.setColor(Color.BLACK);
		g.setFont(font);

		int y = characterDimension.height - MARGIN;

		for (String s : panelValues) {
			g.drawString(s, 0, y);
			y += calculateStringHeight();
		}

		g.drawString(panelValues[0], 0, y);
		g.dispose();
		return slidingImage;
	}

	public void setPanelValue(String value) {
		int index = getValueIndex(value);
		this.currentValue = value;
		this.imageY = calculateStringHeight() * index;
		repaint();
	}

	public void updatePanelValue(String value) {
		if (!currentValue.equals(value)) {
			int index = getValueIndex(value);
			int finalY = calculateStringHeight() * index;
			SliderAnimation sliderAnimation = new SliderAnimation(imageY, finalY);
			new Thread(sliderAnimation).start();
			this.currentValue = value;
		}
	}

	private int getValueIndex(String value) {
		for (int index = 0; index < panelValues.length; index++) {
			if (value.equals(panelValues[index])) {
				return index;
			}
		}

		return -1;
	}

	private int calculateStringHeight() {
		return characterDimension.height + MARGIN;
	}

	@Override
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);
		BufferedImage subImage = slidingImage.getSubimage(0, imageY, characterDimension.width,
				characterDimension.height);
		g.drawImage(subImage, 0, 0, this);
	}

	public class SliderAnimation implements Runnable {
		private int originalY;
		private int finalY;
		private int index;

		public SliderAnimation(int originalY, int finalY) {
			this.originalY = originalY;
			this.finalY = finalY;
		}

		@Override
		public void run() {
			int differenceY = finalY - originalY;
			if (finalY == 0) {
				differenceY = characterDimension.height + MARGIN;
			}

			int steps = 10;
			double difference = (double) differenceY / steps;
			for (int index = 1; index <= steps; index++) {
				imageY = (int) Math.round(difference * index + originalY);
				update();
				sleep(30L);
			}

			if (finalY == 0) {
				imageY = 0;
				update();
			} else {
				imageY = finalY;
			}
		}

		private void update() {
			SwingUtilities.invokeLater(new Runnable() {
				@Override
				public void run() {
					SlidingPanel.this.repaint();
				}
			});
		}

		private void sleep(long duration) {
			try {
				Thread.sleep(duration);
			} catch (InterruptedException e) {

			}
		}

	}
}

We extend a JPanel in the SlidingPanel class because we override the paintComponent method. The paintComponent method draws a String on the JPanel.

The constructor of the SlidingPanel class takes a String array and the font to be used when drawing the String. We create a BufferedImage of the String array in the generateSlidingImage method. Here’s what the BufferedImage looks like, from a debugging test. The font is a smaller font size.

BufferedImage of the String array

Notice that the first String is created at the top and at the bottom of the BufferedImage. This allows us to smoothly animate the changing of the digit in the internal Runnable class.

I created a separate setPanelValue method and a updatePanelValue method. This allows us to display the time when we create the GUI view, and run the sliding animation when the panel value is changed. During the change from regular time to daylight saving time, the hour will jump by two digits. I’m not going to be awake to see that jump.

The paintComponent method gets a sub-image of the BufferedImage, and draws the sub-image on the JPanel.

I created the SliderAnimation class as an internal class so I wouldn’t have a huge list of parameters to pass. This Runnable is created and executed every time the panel value changes. I guessed 300 milliseconds for the animation, and it turned out to be a nice looking animation.

In the run method of the slider animation, I divided the movement into 10 steps. Again, this was a guess that turned out to be a nice looking animation.

Each slider panel gets its own animation thread. That means that up to 7 threads will be running at the same time; the event dispatch thread for the GUI, the clock thread which we haven’t discussed yet, and up to 5 slider animation threads. I’ve seen 4 slider animation threads at the same time, and it works smoothly on my Windows 10 laptop.

At the end of the run method of the slider animation, we check to see if we’re displaying the first String, and if we are, we reset the Y coordinate from the end of the BufferedImage to the beginning. That’s how we turn a BufferedImage into a “wheel”.

The update method of the slider animation class puts the update of the slider panel in the Event Dispatch thread using the SwingUtilities invokeLater method. We do this because the slider animation is running in a different thread. Swing components must be created and updated on the Event Dispatch thread.

Controller

Because this is a clock, there are no controls on the Swing GUI, and no controller code. However, there is a ClockRunnable class that updates the time in the GUI model and updates the GUI view.

Let’s take a look at the ClockRunnable class.

package com.ggl.sliding.clock.runnable;

import javax.swing.SwingUtilities;

import com.ggl.sliding.clock.model.SlidingClockModel;
import com.ggl.sliding.clock.view.SlidingClockFrame;

public class ClockRunnable implements Runnable {

	private SlidingClockFrame frame;

	private SlidingClockModel model;

	public ClockRunnable(SlidingClockFrame frame, SlidingClockModel model) {
		this.frame = frame;
		this.model = model;
	}

	@Override
	public void run() {
		while (true) {
			model.setCurrentTime();
			update();
			sleep(200L);
		}
	}

	private void update() {
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				frame.updatePartControl();
			}
		});
	}

	private void sleep(long duration) {
		try {
			Thread.sleep(duration);
		} catch (InterruptedException e) {

		}
	}

}

The update method of the clock runnable class puts the update of the slider panel in the Event Dispatch thread using the SwingUtilities invokeLater method. We do this because the clock runnable is running in a different thread. Swing components must be created and updated on the Event Dispatch thread.

This concludes my article, I hope I showed you an interesting use for a BufferedImage.

Post a Comment

Your email is kept private. Required fields are marked *