A class that draws a fully animated rotatable wheel with sections containing strings. The wheel once spun, rotates for a certain period and stops at a random point, which gives the selected string. An example of animation in Java AWT
Introduction
The idea behind this project was to create a Java class that designs a simple „wheel-of-fortune“ type of control, which is basically a wheel divided into n sections of the same sizes, and has a tick in the middle of the right side (3 o'clock position) which shows the current selection. The idea was that the wheel could be turned by using the mouse, and also spun with an initial speed and deceleration if the mouse button was released during the movement. Once the wheel would stop spinning, the selected string could be read based on what the tick is pointing at.
The wheel could be used for random selections from limited numbers of strings. The limit is due to the fact that the wheel is divided up to as many sections as the size of the string array, so visually it is only possible to fit in so many.
Since I am quite new to Java programming, this project was in a way intended for me to practice a little bit with Java animation using Java AWT.
The Main Class
The reusable classes needed for the Selection Wheel are: "SelectionWheel.java", "Wheel.java" and "Tick.java". I have written and attached to this project another source file, "MainWheel.java", which is just an example of usage of the SelectionWheel
class.
package SelectionWheel;
import javax.swing.*;
import java.io.File;
import java.io.FilenameFilter;
import java.util.*;
public class MainWheel {
public static void main(String[] args) throws Exception {
int width = 1000, height = 1000;
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ArrayList<String> list = new ArrayList<String>();
list.add("Avatar");
list.add("The Lord of the Rings: The Return of the King");
list.add("Pirates of the Caribbean: Dead Man's Chest");
list.add("The Dark Knight");
list.add("Harry Potter and the Philosopher's Stone");
list.add("Pirates of the Caribbean: At World's End");
list.add("Harry Potter and the Order of the Phoenix");
list.add("Harry Potter and the Half-Blood Prince");
list.add("The Lord of the Rings: The Two Towers");
list.add("Shrek 2");
list.add("Harry Potter and the Goblet of Fire");
list.add("Spider-Man 3");
list.add("Ice Age: Dawn of the Dinosaurs");
list.add("Harry Potter and the Chamber of Secrets");
list.add("The Lord of the Rings: The Fellowship of the Ring");
list.add("Finding Nemo");
list.add("Star Wars: Episode III – Revenge of the Sith");
list.add("Transformers: Revenge of the Fallen");
list.add("Spider-Man");
list.add("Shrek the Third");
SelectionWheel wheel = new SelectionWheel(list);
wheel.hasBorders(true);
wheel.setBounds(10, 10, 700, 700);
JLabel lbl1 = new JLabel("Selection: ");
JLabel lbl2 = new JLabel("Angle: ");
JLabel lbl3 = new JLabel("Speed: ");
JLabel lblsel = new JLabel("(selection)");
JLabel lblang = new JLabel("(angle)");
JLabel lblsp = new JLabel("(speed)");
lbl1.setBounds(720, 10, 100, 20);
lblsel.setBounds(830, 10, 150, 20);
lbl2.setBounds(720, 30, 100, 20);
lblang.setBounds(830, 30, 150, 20);
lbl3.setBounds(720, 50, 100, 20);
lblsp.setBounds(830, 50, 150, 20);
frame.add(wheel);
frame.add(lbl1);
frame.add(lblsel);
frame.add(lbl2);
frame.add(lblang);
frame.add(lbl3);
frame.add(lblsp);
frame.setSize(width, height);
frame.setLayout(null);
frame.setVisible(true);
lblsel.setText(wheel.getSelectedString());
lblang.setText(Double.toString(wheel.getRotationAngle()));
lblsp.setText(Double.toString(wheel.getSpinSpeed()));
while(true) {
while(true)
{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(wheel.isSpinning())
break;
}
while(wheel.isSpinning())
{
lblsel.setText(wheel.getSelectedString());
lblang.setText(Double.toString(wheel.getRotationAngle()));
lblsp.setText(Double.toString(wheel.getSpinSpeed()));
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lblsp.setText(Double.toString(wheel.getSpinSpeed()));
JOptionPane.showMessageDialog(frame, "Selection: " + wheel.getSelectedString());
}
}
}
The SelectionWheel
in the example is initialized with a String ArrayList
containing 20 best grossing movies in 2000s. It is drawn on a JFrame
. There are also several labels – lblsel
shows the current selection, lblang
shows the current rotation angle, and lblsp
shows the current speed of rotation of the wheel.
After the initialization, the code is entering an infinite loop where first a loop is waiting for the wheel to begin spinning, then after the spinning has started, it is refreshing the labels by reading the attributes of the SelectionWheel
, and finally once it has stopped, pops a MessageDialog
showing the finally selected string (the string at which the tick is pointing).
The Classes
There are altogether three classes which are needed for the SelectionWheel
to work.
The first is the SelectionWheel
class, which is basically just a wrapper class which combines the other two classes – Wheel
and Tick
. The Wheel
and Tick
needed to be written separately because the wheel is constantly rotated, while the tick is stationary, so we needed two separate Graphics
objects for drawing them.
All three classes are extensions of JPanel
class.
The Wheel Class
The Wheel
class is where all the animation and calculations happen. My original intention was to just write this class, but, as mentioned previously, the tick needed to be created separately because of the rotation – we need the wheel to rotate, but the tick remains stationary.
The Wheel
class has only one constructor method – it is always initialized with an ArrayList
of String
objects.
public Wheel(ArrayList<String> listOfStrings)
Mouse Listeners
The Wheel
class constructor also creates a MouseListener
– for mousePressed
and mouseReleased
events, and a MouseMotionListener
– for the MouseDragged
event.
The mouse events are tracked so that the animation of the rotation of the wheel can be triggered by using the mouse.
When the mouse is pressed, the rotation stops (if the wheel is moving at the moment). The time and the rotation angle are stored in order to calculate the initial speed and direction of rotation once it is released.
@Override
public void mousePressed(MouseEvent e) {
_mouseDragPosition = new Point2D.Double(e.getX(), e.getY());
double distance = Math.sqrt(Math.pow(_mouseDragPosition.getX() -
_center.getX(),2) + Math.pow(_mouseDragPosition.getY() - _center.getY(),2));
if(distance <= _radius)
{
spinStop();
}
_timeStart = System.currentTimeMillis();
_rotationAngleStart = _rotationAngle;
}
Once the mouse is released, the initial speed is calculated, and the spinning is started by calling the method spinStartAsync
.
@Override
public void mouseReleased(MouseEvent e) {
setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
_timeEnd = System.currentTimeMillis();
_rotationAngleEnd = _rotationAngle;
double initialSpeed = 1000 * (_rotationAngleEnd - _rotationAngleStart) /
(_timeEnd - _timeStart);
initialSpeed = (int)Math.signum(initialSpeed) *
Math.min(Math.abs(initialSpeed), _maxSpinSpeed);
try {
spinStartAsync(Math.abs(initialSpeed),
(int)Math.signum(initialSpeed), _spinDeceleration);
} catch (Exception e1) {
e1.printStackTrace();
}
}
The mouseDragged
event listener is used to rotate the wheel while the mouse is pressed:
addMouseMotionListener(new MouseAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
setCursor(new Cursor(Cursor.HAND_CURSOR));
spinStop();
Point2D mousePos = new Point2D.Double(e.getX(), e.getY());
double k1 = (_mouseDragPosition.getY() - _center.getY()) /
(_mouseDragPosition.getX() - _center.getX());
double k2 = (mousePos.getY() - _center.getY()) / (mousePos.getX() - _center.getX());
double _delta = Math.toDegrees(Math.atan((k2-k1)/(1 + k2 * k1)));
if(!Double.isNaN(_delta))
setRotationAngle(getRotationAngle() + _delta);
_mouseDragPosition = mousePos;
}
});
The Animation
After the mouse button is released, the animation is accomplished by repeatedly repainting the JPanel
– while a separate thread is calculating and updating the rotation angle over time; each time the JPanel
is repainted, the wheel is drawn rotated by a new angle. A deceleration is defined in the calculation of the spinning speed, in order to make sure that the wheel eventually stops spinning.
public void spinStartAsync(double speed, int direction, double deceleration)
(The spinning thread and the calculation process are explained in further text.)
However, the only thing handled so far is the calculation of the angle and calling the repaint method; in order to actually paint the wheel the way we want it, we need to override the paintComponent
method.
@Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
if(_image == null) {
_image = drawImage();
_rotationCenter = new Point2D.Double(
this.getWidth() - 2 * BORDER - 2 * _radius + _center.getX(),
this.getHeight() / 2
);
_imagePosition = new Point2D.Double(
(int)(this.getWidth() - 2 * BORDER - 2 * _radius),
(int)(this.getHeight() / 2 - _center.getY())
);
}
Graphics2D gPanel = (Graphics2D) g;
gPanel.setRenderingHint
(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
gPanel.setRenderingHint
(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
gPanel.rotate(Math.toRadians(_rotationAngle),
_rotationCenter.getX(), _rotationCenter.getY());
gPanel.drawImage(_image, (int)_imagePosition.getX(), (int)_imagePosition.getY(), null);
}
For the sake of performance, and since the wheel – once drawn – will not be changed, we are using a BufferedImage
to store the wheel – so the wheel itself is actually drawn only once. The other approach would be to draw the entire wheel – section by section – each time the JPanel
redraws, but this might cause poor performance, depending on the size of the wheel and the number of the sections.
The BufferedImage
is drawn inside a separate method, which is thoroughly explained in further text.
Get / Set Methods
There are a lot of parameters of the wheel that can be affected / changed during runtime.
public void setShape(Shape shape)
The wheel can have different shapes, and this set method is used to change the shape of the wheel. The currently available shapes are:
public static enum Shape {
CIRCLE,
UMBRELLA
}
This list can easily be extended by adding a method for drawing another shape of a section. The circle shape is drawn as arcs (method fillArc
), and the umbrella shape is drawn as triangles (method fillTriangle
).
public double getRotationAngle()
public void setRotationAngle(double rotationAngle)
The rotation angle can be set directly. The repaint
method is called.
public ArrayList<Color> getColorScheme()
public void setColorScheme(ArrayList<Color> colors)
The Wheel
class contains an ArrayList
of Color
objects that are used to draw (fill) the wheel sections. The colors are used in the order of appearance in the ArrayList
. If the ArrayList
was never set, a default list will be used (method getDefaultColorList
).
A color can also be added to the existing list by using method addColor
.
public int getRadius()
Gets the current radius of the wheel. The radius cannot be changed, it is calculated based on the size of the BufferedImage
.
public ArrayList<String> getListOfStrings()
public void setListOfStrings(ArrayList<String> list)
The list of string
s can be changed during runtime. In that case, the existing BufferedImage
is disposed and a new one is drawn.
The number of String
objects in ArrayList
is limited by a variable private final int LIMIT = 100
. If the size of the ArrayList
is above this limit, an Exception will be thrown.
The reason for limiting the number of string
s is limiting the number of sections – because for a large number of sections, the delta angle becomes very very tiny, which is difficult to draw and may cause distortion and miscalculations.
public Font getFont()
public void setFont(Font font)
Get
s/set
s the font of the displayed string
s.
public double getSpinSpeed()
This is the actual spinning speed of the wheel – it cannot be set, it starts with the initial speed and is decreasing by deceleration until a full stop.
The current speed is calculated in a separate thread, a special TimerTask
object which runs the entire time, and based on the change of the rotation angle over a period of time, it is calculating the current speed of rotation. This class is explained in further text.
public double getMaxSpinSpeed()
public void setMaxSpinSpeed(double speed)
The maximum speed limits the initial speed of the rotation. This parameter was introduced in order to avoid too long spinnings. This can be set differently; by allowing greater maximum speeds, we achieve a larger degree of randomness – however, in that case, it would be advisable to also set the deceleration accordingly.
public double getSpinDeceleration()
public void setSpinDeceleration(double deceleration)
Get
s/set
s the spin deceleration. Note that deceleration must be <= 0
, otherwise the result is an Exception
.
public String getSelectedString() {
return _stringList.get((int)Math.floor(_noElem + (_rotationAngle % 360) / _delta) % _noElem);
}
Get
the currently selected string
(the string
from the section to which the tick is pointing).
The idea is to get the number of deltas (section angle) in the current rotationAngle
. This number is added to the size of the string
arraylist, and then MODed by the size of the string arraylist
, in order to avoid negative indices.
The Tick Class
The Tick
class is very simple and short.
Tick
can have width
and height
– these are the attributes. It can have a random polygon shape, which can be set with the method setPolygon
. If the custom polygon is not set, then the triangle is used, which is calculated in method getTriangle()
.
private Polygon getTriangle() {
Polygon polygon = new Polygon();
polygon.addPoint(0, this.getHeight() / 2);
polygon.addPoint(this.getWidth(),
(int)(this.getHeight() / 2 - this.getWidth() * Math.tan(Math.toRadians(30))));
polygon.addPoint(this.getWidth(),
(int)(this.getHeight() / 2 + this.getWidth() * Math.tan(Math.toRadians(30))));
return polygon;
}
If a custom polygon is used, its size needs to be adjusted to fit the JPanel
, and it needs to be positioned properly, so this method is called:
private void adjustPolygon()
{
int i;
int xmax = Integer.MIN_VALUE, xmin = Integer.MAX_VALUE;
int ymax = xmax, ymin = xmin;
for(i = 0; i < _polygon.xpoints.length; i++)
{
if(_polygon.xpoints[i]>xmax) xmax = _polygon.xpoints[i];
if(_polygon.xpoints[i]<xmin) xmin = _polygon.xpoints[i];
}
for(i = 0; i < _polygon.ypoints.length; i++)
{
if(_polygon.ypoints[i]>ymax) ymax = _polygon.ypoints[i];
if(_polygon.ypoints[i]<ymin) ymin = _polygon.ypoints[i];
}
int width = xmax - xmin;
double factor = (double)this.getWidth() / width;
for(i = 0; i < _polygon.xpoints.length; i++)
{
_polygon.xpoints[i] *= factor;
_polygon.ypoints[i] *= factor;
}
int centerX = 0, centerY = 0;
for(i = 0; i < _polygon.xpoints.length; i++)
{
centerX += _polygon.xpoints[i];
}
centerX /= _polygon.xpoints.length;
for(i = 0; i < _polygon.ypoints.length; i++)
{
centerY += _polygon.ypoints[i];
}
centerY /= _polygon.ypoints.length;
_polygon.translate(this.getWidth() / 2 - centerX, this.getHeight() / 2 - centerY);
}
The overriden paintComponent
method will create the triangle if the custom polygon is not set, and then call fillPolygon
to draw it.
The SelectionWheel Class
The SelectionWheel
class is a wrapper for other two classes – it joins them together and makes sure that the wheel
and tick
are positioned properly in relation to one another. The positions are adjusted in the overriden setBounds
method.
@Override
public void setBounds(int x, int y, int width, int height) {
super.setBounds(x, y, width, height);
_wheel.setBounds(0, 0, width - _tick.getTickWidth(), height);
_tick.setBounds(width - _tick.getTickWidth(), 0, _tick.getTickWidth(), height);
}
The class initializes a Wheel
and a Tick
object, and contains get
and set
methods that are passing attributes to and from these objects.
Points of Interest
Drawing the Wheel
The Wheel
class contains several attributes that are crucial to the process of drawing, the most important being:
Radius
– the radius of the wheel
(circle); it is set in drawImage()
method and will always be as big as the Image
size allows it, minus the specified BORDER
_radius = Math.min(img.getWidth(), img.getHeight()) / 2 - BORDER;
Center
– the center of the wheel
(circle) is always in the middle of the BufferedImage
_center = new Point2D.Double((double)img.getWidth() / 2, (double)img.getHeight() / 2);
stringDistanceFromEdge
– required in positioning the drawn string
– how far from the edge of the wheel
we want it to be; this is hardcoded to 5% of the radius
double stringDistanceFromEdge = 0.05 * _radius;
fontSize
– the optimal size of the font of drawn string
s; this is calculated in a separate method calcFontSize
Calculating Font Size
Calculating the optimal font size was one of the trickiest parts. The idea was to calculate the largest possible font that would still enable all of the string
s to fit into their respective sections, but with alignment to the edge of the wheel, stringDistanceFromEdge
away from it.
The algorithm is as follows:
- First, we find the longest
string
in the ArrayList
. - Then, we set it to maximum allowed font size (
final int MAXFONTSIZE
). - Then we adjust the font size to the maximum possible height of the
String
. For this, we use java.awt.FontMetrics
and a couple of basic trigonometric rules, such as Pythagoras theorem and triangle similarity. - Then, the
width
is adjusted by trial-and-error; if the string
is wider than the section, the font size is reduced, and if the string
is narrower than the section, the font size is increased – point by point – and then width is measured using FontMetrics
in each iteration – until the string
fits the section.
I tried to solve the font size problem in many different ways (i.e., linear proportionality of font size and string
height, but unfortunately, this rule did not provide the best results). In the end, I used this convenient solution.
private int calcFontSize(Graphics g, double stringDistanceFromEdge, int maxStringWidth) {
String tmpString = "";
for(int i = _noElem - 1; i >= 0; i--) {
if(_stringList.get(i).length() > tmpString.length())
tmpString = _stringList.get(i);
}
int fontSize = MAXFONTSIZE;
g.setFont(new Font(_font.getFamily(), _font.getStyle(), fontSize));
FontMetrics fontMetrics = g.getFontMetrics();
Rectangle2D stringBounds = fontMetrics.getStringBounds(tmpString, g);
int maxHeight = (int)Math.floor
(2 * stringDistanceFromEdge * Math.sin(Math.toRadians(_delta / 2)));
if(stringBounds.getHeight() > maxHeight) {
fontSize = (int)Math.floor(fontSize * maxHeight / stringBounds.getHeight());
g.setFont(new Font(_font.getFamily(), _font.getStyle(), fontSize));
fontMetrics = g.getFontMetrics();
stringBounds = fontMetrics.getStringBounds(tmpString, g);
}
double K = stringBounds.getWidth() / stringBounds.getHeight();
maxHeight = (int)Math.floor(2 * (_radius - stringDistanceFromEdge) *
Math.tan(Math.toRadians(_delta / 2)) / (1 + 2 * K * Math.tan(Math.toRadians(_delta / 2))));
while(stringBounds.getWidth() < maxStringWidth) {
g.setFont(new Font(_font.getFamily(), _font.getStyle(), ++fontSize));
fontMetrics = g.getFontMetrics();
stringBounds = fontMetrics.getStringBounds(tmpString, g);
}
while(stringBounds.getWidth() > maxStringWidth) {
g.setFont(new Font(_font.getFamily(), _font.getStyle(), --fontSize));
fontMetrics = g.getFontMetrics();
stringBounds = fontMetrics.getStringBounds(tmpString, g);
}
return Math.min(fontSize, MAXFONTSIZE);
}
"Zooming"
There is, of course, the eventuality that a string
is so long that the resulting font size would be unreadable. So I introduced a variable MINFONTSIZE
which is limiting the font size to a minimum value. The first idea was to just not draw the string
s if the font size is too small, but then I thought of a better solution – to "zoom" in the wheel
, so that the font can be larger. So, if the font size is too small, the wheel
is proportionally enlarged in order that the minimum font size is satisfied. This is happening inside the drawImage
method:
if(fontSize < MINFONTSIZE) {
_zoomFactor = (double)MINFONTSIZE / fontSize;
width += (int) 2 * ((_zoomFactor * _radius) - _radius);
height += (int) 2 * ((_zoomFactor * _radius) - _radius);
_radius = (int)(_zoomFactor * _radius);
img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
g2d = (Graphics2D) img.getGraphics();
maxStringWidth = (int)(_radius - 2 * stringDistanceFromEdge);
fontSize = calcFontSize(g2d, stringDistanceFromEdge, maxStringWidth);
}
Now, the wheel
image is larger than the surrounding container (JPanel
), so it needs to be properly placed. Also, a new rotation center needs to be calculated. This is a part of the paintComponent
overriden method, and is only calculated once after the wheel
image is drawn:
if(_image == null) {
_image = drawImage();
_rotationCenter = new Point2D.Double(
this.getWidth() - _image.getWidth(null) + _center.getX(),
this.getHeight() / 2
);
_imagePosition = new Point2D.Double(
(int)(this.getWidth() - _image.getWidth(null)),
(int)(this.getHeight() / 2 - _center.getY())
);
}
This is how it looks like:
Drawing in a Loop
Each section of the wheel is drawn in a separate iteration of the loop. The simplest way of drawing the wheel was to just turn it around bit by bit and draw the necessary elements.
The loop is going through as many iterations as there are string
s in the ArrayList
(which is equal to the number of sections of the wheel
).
First, the line (section border) from the centre of the wheel
to the 3 o'clock mark is drawn.
if(hasBorders) {
g2d.setColor(Color.BLACK);
g2d.drawLine((int)_center.getX(), (int)_center.getY(),
(int)_center.getX() + _radius, (int)_center.getY());
}
(The border can be switched on and of with hasBorders
variable.)
Then, the section above it is filled with color. The angle of the section is 360 / (number of sections). The color is chosen from the _colors ArrayList
variable.
g2d.setColor(_colors.get(_colorCounter++ % _colors.size()));
if(_shape == Shape.UMBRELLA)
fillTriangle(g2d);
else
fillArc(g2d);
Depending on the chosen Shape
of the wheel, the section is either drawn as an arc or as a triangle.
private void fillArc(Graphics g2d) {
g2d.fillArc((int)_center.getX() - _radius, (int)_center.getY() -
_radius, 2 * _radius, 2 * _radius, 0, (int)- Math.ceil(_delta));
if(hasBorders) {
g2d.setColor(Color.black);
g2d.drawArc((int)_center.getX() - _radius, (int)_center.getY() -
_radius, 2 * _radius, 2 * _radius, 0, (int)- Math.ceil(_delta));
}
}
private void fillTriangle(Graphics2D g2d) {
int[] xpoints = new int[3];
xpoints[0] = (int)_center.getX();
xpoints[1] = (int)_center.getX() + _radius;
int dx = (int) (2 * _radius * Math.pow(Math.sin(Math.toRadians(_delta / 2)), 2));
xpoints[2] = xpoints[1] - dx;
int[] ypoints = new int[3];
ypoints[0] = (int)_center.getY();
ypoints[1] = (int)_center.getY();
int dy = (int) (2 * _radius * Math.sin(Math.toRadians
(_delta / 2)) * Math.cos(Math.toRadians(_delta / 2)));
ypoints[2] = ypoints[1] + dy;
g2d.fillPolygon(xpoints, ypoints, 3);
if(hasBorders) {
g2d.setColor(Color.black);
g2d.drawLine(xpoints[1], ypoints[1], xpoints[2], ypoints[2]);
}
}
Next, the string
is drawn. The wheel
is rotated first for half a section angle so that the string
would be drawn across the middle, and after drawing the string
, it is rotated for another half of the section angle.
g2d.rotate(Math.toRadians(_delta / 2), _center.getX(), _center.getY());
g2d.setColor(Color.BLACK);
fontMetrics = g2d.getFontMetrics();
stringWidth = fontMetrics.stringWidth(_stringList.get(i));
g2d.drawString(_stringList.get(i), (int)(_center.getX() +
maxStringWidth - stringWidth + stringDistanceFromEdge),
(int)(_center.getY() + (double)fontMetrics.getHeight() / 2 -
fontMetrics.getMaxDescent()));
g2d.rotate(Math.toRadians(_delta / 2), _center.getX(), _center.getY());
Rendering Hints
Always be sure to use rendering hints when drawing in Java, otherwise the image can become very grainy. These are the hints I used:
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
Wheel Rotation
The rotation itself is happening in a special class SpinRunnable
which implements the Runnable
interface:
private class SpinRunnable implements Runnable {
private double spinSpeed;
private int spinDirection;
private double spinDeceleration;
public SpinRunnable(double speed, int direction, double deceleration) {
this.spinSpeed = speed;
this.spinDirection = direction;
this.spinDeceleration = deceleration;
}
public void run()
{
_spinOnOff = true;
int sleepTime = 1000 / _refreshRate;
double delta;
while(_spinOnOff && spinSpeed > 0)
{
delta = spinDirection * (spinSpeed / _refreshRate);
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
setRotationAngle(getRotationAngle() + delta);
spinSpeed += spinDeceleration / _refreshRate;
}
_spinOnOff = false;
}
}
The spinning is accomplished quite simply – by decreasing the speed by the deceleration factor over time, and calculating and setting rotation angle until the speed is 0 or until the spinning is turned off (_spinOnOff == false
).
An instance of this class is initiated and its run method started by spinStartAsync
method. The method is starting a new Thread
, so that the spinning wouldn't block the rest of the code.
public void spinStartAsync(double speed, int direction, double deceleration) throws Exception
{
if(deceleration > 0)
throw new Exception("Illegal parameter value: acceleration must be < 0");
SpinRunnable spinRunnable = new SpinRunnable(speed, direction, deceleration);
Thread t = new Thread(spinRunnable);
t.start();
}
Tracking the Speed of Rotation
To track the speed of rotation, there is also a separate class which extends the TimerTask
class.
private class speedTimerTask extends TimerTask {
@Override
public void run() {
double prevAngle, nowAngle;
long sleepTime = 100;
while(true) {
prevAngle = getRotationAngle();
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
nowAngle = getRotationAngle();
nowAngle = (nowAngle + Math.signum(nowAngle) * 360) % 360;
_spinSpeed = Math.abs(nowAngle - prevAngle) * (1000 / sleepTime);
}
}
}
This class contains the run
method which is also run in a separate thread – it runs in an infinite loop where, in each iteration, it calculates the angle change over time, and based on that, sets the _spinSpeed
variable.
History
- 4th July, 2020: Initial version