Java 版幸运转盘






4.83/5 (5投票s)
用 Java AWT 编写的动画随机字符串选择轮类
引言
此项目的构思是创建一个 Java 类,该类设计一个简单的“轮盘”类型的控件,它基本上是一个被分成 n 个大小相等的扇区的轮子,并在右侧的中间(3 点钟位置)有一个刻度,显示当前选择。构思是可以通过鼠标转动轮子,如果鼠标按钮在移动过程中释放,还可以以初始速度和减速度旋转。一旦轮子停止旋转,就可以根据刻度指向的位置读取选定的字符串。
该轮盘可用于从有限数量的字符串中进行随机选择。限制是由于轮子被分割成与字符串数组大小一样多的扇区,因此在视觉上只能容纳这么多。
由于我刚开始学习 Java 编程,因此此项目在某种程度上是为了让我练习一点 Java AWT 动画。
主类
选择轮盘所需的其他类是:“SelectionWheel.java”、“Wheel.java”和“Tick.java”。我已编写并添加到此项目中的另一个源文件是“MainWheel.java”,它只是 SelectionWheel
类使用的一个示例。
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) {
// wait for action
while(true)
{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(wheel.isSpinning())
break;
}
// while spinning
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()));
// show selection
JOptionPane.showMessageDialog(frame, "Selection: " + wheel.getSelectedString());
}
}
}
示例中的 SelectionWheel
使用一个包含 2000 年代 20 部票房最高的电影的 String ArrayList
进行初始化。它绘制在一个 JFrame
上。还有几个标签——lblsel
显示当前选择,lblang
显示当前旋转角度,lblsp
显示轮子的当前旋转速度。
初始化后,代码进入一个无限循环,首先是一个循环等待轮子开始旋转,然后一旦开始旋转,它会通过读取 SelectionWheel
的属性来刷新标签,最后一旦停止,会弹出一个 MessageDialog
显示最终选定的字符串(刻度指向的字符串)。
类
总共有三个类是 SelectionWheel
工作所必需的。
第一个是 SelectionWheel
类,它基本上只是一个包装类,它结合了另外两个类——Wheel
和 Tick
。Wheel
和 Tick
需要单独编写,因为轮子在不断旋转,而刻度是固定的,所以我们需要两个单独的 Graphics
对象来绘制它们。
所有三个类都是 JPanel
类的扩展。
轮子类
Wheel
类是所有动画和计算发生的地方。我最初的意图只是编写这个类,但如前所述,刻度需要单独创建,因为要旋转——我们需要轮子旋转,但刻度保持固定。
Wheel
类只有一个构造方法——它总是用 String
对象的 ArrayList
进行初始化。
public Wheel(ArrayList<String> listOfStrings)
鼠标监听器
Wheel
类构造方法还创建了一个 MouseListener
——用于 mousePressed
和 mouseReleased
事件,以及一个 MouseMotionListener
——用于 MouseDragged
事件。
跟踪鼠标事件是为了通过鼠标触发轮子旋转的动画。
当鼠标按下时,旋转会停止(如果轮子此时正在移动)。时间与旋转角度被存储起来,以便在释放鼠标时计算初始速度和旋转方向。
@Override
public void mousePressed(MouseEvent e) {
_mouseDragPosition = new Point2D.Double(e.getX(), e.getY());
// to stop the spinning if the circle is clicked on
double distance = Math.sqrt(Math.pow(_mouseDragPosition.getX() -
_center.getX(),2) + Math.pow(_mouseDragPosition.getY() - _center.getY(),2));
if(distance <= _radius)
{
spinStop();
}
// to measure initial speed
_timeStart = System.currentTimeMillis();
_rotationAngleStart = _rotationAngle;
}
一旦鼠标释放,就会计算初始速度,并通过调用 spinStartAsync
方法启动旋转。
@Override
public void mouseReleased(MouseEvent e) {
setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
// to measure initial speed
_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();
}
}
mouseDragged
事件监听器用于在按下鼠标时旋转轮子
addMouseMotionListener(new MouseAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
setCursor(new Cursor(Cursor.HAND_CURSOR));
spinStop();
/*
* Use the equation for angle between two vectors:
* vector 1 between last position of mouse and center of circle
* vector 2 between current position of mouse and center of circle
* ("k" is direction coefficient)
*/
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;
}
});
动画
释放鼠标按钮后,通过反复重绘 JPanel
来完成动画——同时一个单独的线程正在计算并随着时间更新旋转角度;每次重绘 JPanel
时,都会绘制旋转了新角度的轮子。在旋转速度的计算中定义了减速,以确保轮子最终停止旋转。
public void spinStartAsync(double speed, int direction, double deceleration)
(旋转线程和计算过程将在后续文本中进一步说明。)
但是,到目前为止只处理了角度的计算和调用 repaint 方法;为了能够按我们想要的方式实际绘制轮子,我们需要重写 paintComponent
方法。
@Override
public void paintComponent(Graphics g)
{
/*
* Paintcomponent - if the image is null, create it and then draw it
* whilst keeping the current rotation.
* The image can be larger than the displaying area,
* so after it is drawn it needs to be placed properly.
*/
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);
}
为了性能考虑,并且由于轮子——一旦绘制——就不会被改变,我们使用 BufferedImage
来存储轮子——所以轮子本身实际上只绘制一次。另一种方法是每次 JPanel
重绘时——逐个扇区——绘制整个轮子,但这可能会导致性能不佳,具体取决于轮子的大小和扇区数量。
BufferedImage
在单独的方法中绘制,该方法将在后续文本中进行详细说明。
Get / Set 方法
在运行时可以影响/更改轮子的参数有很多。
public void setShape(Shape shape)
轮子可以有不同的形状,此 set 方法用于更改轮子的形状。当前可用的形状是
public static enum Shape {
CIRCLE,
UMBRELLA
}
通过添加绘制另一种形状扇区的方法,可以轻松扩展此列表。圆形是通过弧线(fillArc
方法)绘制的,而雨伞形状是通过三角形(fillTriangle
方法)绘制的。
public double getRotationAngle()
public void setRotationAngle(double rotationAngle)
旋转角度可以直接设置。调用 repaint
方法。
public ArrayList<Color> getColorScheme()
public void setColorScheme(ArrayList<Color> colors)
Wheel
类包含一个 Color
对象的 ArrayList
,用于绘制(填充)轮子扇区。颜色按照 ArrayList
中出现的顺序使用。如果 ArrayList
从未设置,则会使用默认列表(getDefaultColorList
方法)。
还可以通过使用 addColor
方法将颜色添加到现有列表中。
public int getRadius()
获取轮子的当前半径。半径不可更改,它根据 BufferedImage
的大小计算得出。
public ArrayList<String> getListOfStrings()
public void setListOfStrings(ArrayList<String> list)
string
列表可以在运行时更改。在这种情况下,现有的 BufferedImage
将被丢弃并绘制新的。
ArrayList
中 String
对象的数量受到变量 private final int LIMIT = 100
的限制。如果 ArrayList
的大小超过此限制,则会抛出异常。
限制 string
数量的原因是限制扇区数量——因为对于大量扇区,增量角度会非常非常小,难以绘制并可能导致失真和计算错误。
public Font getFont()
public void setFont(Font font)
获取
/设置
显示的 string
的字体。
public double getSpinSpeed()
这是轮子的实际旋转速度——无法设置,它以初始速度开始,并随着减速度逐渐减小,直到完全停止。
当前速度是在一个单独的线程中计算的,一个特殊的 TimerTask
对象,它一直运行,并且基于一段时间内的旋转角度变化,它正在计算当前的旋转速度。此类将在后续文本中进行说明。
public double getMaxSpinSpeed()
public void setMaxSpinSpeed(double speed)
最大速度限制了旋转的初始速度。引入此参数是为了避免过长时间的旋转。这可以不同地设置;通过允许更大的最大速度,我们实现了更大的随机性——但是,在这种情况下,建议也相应地设置减速。
public double getSpinDeceleration()
public void setSpinDeceleration(double deceleration)
获取
/设置
旋转减速。请注意,减速必须 <= 0
,否则结果将是 Exception
。
public String getSelectedString() {
return _stringList.get((int)Math.floor(_noElem + (_rotationAngle % 360) / _delta) % _noElem);
}
获取
当前选定的 string
(刻度指向的扇区的 string
)。
思路是获取当前 rotationAngle
中增量(扇区角度)的数量。此数字会加到 string
数组列表的大小上,然后通过 string
数组列表的大小进行 MOD 运算,以避免负索引。
刻度类
Tick
类非常简单且简短。
Tick
可以有 width
和 height
——这些是属性。它可以有一个随机的多边形形状,可以通过 setPolygon
方法进行设置。如果未设置自定义多边形,则使用三角形,该三角形在 getTriangle()
方法中计算。
private Polygon getTriangle() {
/*
* Get triangle polygon - default shape of the tick.
*/
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;
}
如果使用自定义多边形,则需要调整其大小以适应 JPanel
,并且需要正确放置它,因此调用此方法
private void adjustPolygon()
{
/*
* Adjust the size and position of the custom polygon shape of the tick.
*/
int i;
// calculate width/height of the polygon
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;
// scale polygon
double factor = (double)this.getWidth() / width;
for(i = 0; i < _polygon.xpoints.length; i++)
{
_polygon.xpoints[i] *= factor;
_polygon.ypoints[i] *= factor;
}
// calculate center of polygon
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;
// translate polygon to center of the panel
_polygon.translate(this.getWidth() / 2 - centerX, this.getHeight() / 2 - centerY);
}
重写的 paintComponent
方法将在未设置自定义多边形时创建三角形,然后调用 fillPolygon
进行绘制。
SelectionWheel 类
SelectionWheel
类是其他两个类的包装器——它将它们组合在一起,并确保 wheel
和 tick
相对于彼此正确放置。位置在重写的 setBounds
方法中调整。
@Override
public void setBounds(int x, int y, int width, int height) {
/*
* Adjust the bounds of the wheel and tick based on tick width.
*/
super.setBounds(x, y, width, height);
_wheel.setBounds(0, 0, width - _tick.getTickWidth(), height);
_tick.setBounds(width - _tick.getTickWidth(), 0, _tick.getTickWidth(), height);
}
该类初始化一个 Wheel
和一个 Tick
对象,并包含 get
和 set
方法,这些方法将属性传递给这些对象或从这些对象接收。
关注点
绘制轮子
Wheel
类包含几个对绘制过程至关重要的属性,其中最重要的是
Radius
——wheel
(圆)的半径;它在drawImage()
方法中设置,并且始终与Image
大小允许的一样大,减去指定的BORDER
_radius = Math.min(img.getWidth(), img.getHeight()) / 2 - BORDER;
Center
——wheel
(圆)的中心始终位于BufferedImage
的中间_center = new Point2D.Double((double)img.getWidth() / 2, (double)img.getHeight() / 2);
stringDistanceFromEdge
——在定位绘制的string
时需要——我们希望它距离轮子边缘有多远;这被硬编码为半径的 5%。double stringDistanceFromEdge = 0.05 * _radius;
fontSize
——绘制的string
的最佳字体大小;这是在一个单独的方法calcFontSize
中计算的
计算字体大小
计算最佳字体大小是棘手的部分之一。思路是计算最大的可能字体,该字体仍然能让所有 string
都适合各自的扇区,但与轮子边缘对齐,距离其 stringDistanceFromEdge
。
算法如下
- 首先,我们找到
ArrayList
中最长的string
。 - 然后,我们将其设置为最大允许字体大小(
final int MAXFONTSIZE
)。 - 然后,我们根据
String
的最大可能高度调整字体大小。为此,我们使用java.awt.FontMetrics
以及一些基本的三角学规则,例如毕达哥拉斯定理和三角形相似。 - 然后,通过反复试验调整
width
;如果string
比扇区宽,则减小字体大小,如果string
比扇区窄,则增加字体大小——逐点——然后在每次迭代中使用FontMetrics
测量width
——直到string
适合扇区。
我尝试了多种方法来解决字体大小问题(例如,字体大小和 string
高度的线性比例,但遗憾的是,此规则并未提供最佳结果)。最终,我使用了这个方便的解决方案。
private int calcFontSize(Graphics g, double stringDistanceFromEdge, int maxStringWidth) {
/*
* Calculates the optimal font size for the strings inside the sections.
* The strings need to be positioned next to the broader end of the section.
* The optimal size will depend on the longest string length
* and maximum height of the section
* in the left border of the rectangle surrounding the string.
*/
// Find the longest string
String tmpString = "";
for(int i = _noElem - 1; i >= 0; i--) {
if(_stringList.get(i).length() > tmpString.length())
tmpString = _stringList.get(i);
}
// Set it to max font size and calculate rectangle
int fontSize = MAXFONTSIZE;
g.setFont(new Font(_font.getFamily(), _font.getStyle(), fontSize));
FontMetrics fontMetrics = g.getFontMetrics();
Rectangle2D stringBounds = fontMetrics.getStringBounds(tmpString, g);
// Adjust string height / font size
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);
}
// Adjust string width
// If the string is too narrow, increase font until it fits
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);
}
// If the string is too wide, decrease font until it fits
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);
}
“缩放”
当然,存在 string
过长导致字体大小不可读的可能性。因此,我引入了一个变量 MINFONTSIZE
,它将字体大小限制为最小值。最初的想法是如果字体大小太小就不绘制 string
,但后来我想到一个更好的解决方案——“缩放” wheel
,这样字体就可以更大。因此,如果字体大小太小,wheel
会按比例放大,以满足最小字体大小的要求。这发生在 drawImage
方法中。
// Adjust the parameters (for "zoom in") - if the font size is too small
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);
}
现在,wheel
图像比周围的容器(JPanel
)大,因此需要正确放置。此外,还需要计算新的旋转中心。这是 paintComponent
重写方法的一部分,并且仅在 wheel
图像绘制后计算一次。
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())
);
}
我们将设计一个简单的布局,并针对所有可能的分辨率进行验证。
循环绘制
轮子的每个扇区都在循环的单独迭代中绘制。绘制轮子的最简单方法是逐点旋转并绘制必要的元素。
循环的迭代次数等于 ArrayList
中的 string
数量(这等于 wheel
的扇区数量)。
首先,从 wheel
中心到 3 点钟标记的线(扇区边框)被绘制。
// Draw section border
if(hasBorders) {
g2d.setColor(Color.BLACK);
g2d.drawLine((int)_center.getX(), (int)_center.getY(),
(int)_center.getX() + _radius, (int)_center.getY());
}
(边框可以通过 hasBorders
变量打开或关闭。)
然后,上面的扇区用颜色填充。扇区的角度是 360 / (扇区数量)。颜色从 _colors ArrayList
变量中选择。
// Fill section depending on the chosen shape
g2d.setColor(_colors.get(_colorCounter++ % _colors.size()));
if(_shape == Shape.UMBRELLA)
fillTriangle(g2d);
else //if(_shape == Shape.CIRCLE)
fillArc(g2d);
根据选择的轮子 Shape
,扇区将绘制为圆弧或三角形。
private void fillArc(Graphics g2d) {
g2d.fillArc((int)_center.getX() - _radius, (int)_center.getY() -
_radius, 2 * _radius, 2 * _radius, 0, (int)- Math.ceil(_delta)); // use ceil
// because of decimal part (would be left empty)
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) {
/*
* Method that draws section as a triangle (in case Shape=UMBRELLA was chosen)
*/
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]);
}
}
接下来,绘制 string
。首先将 wheel
旋转半个扇区角度,以便 string
绘制在中间,然后在绘制 string
之后,再旋转半个扇区角度。
// Draw string - rotate half delta, then draw then rotate the other half
// (to have the string in the middle)
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());
渲染提示
在 Java 中绘图时,一定要使用渲染提示,否则图像会变得非常粗糙。这些是我使用的提示。
// Set rendering hints
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
轮子旋转
旋转本身发生在名为 SpinRunnable
的特殊类中,该类实现了 Runnable
接口。
private class SpinRunnable implements Runnable {
/*
* Runnable class that handles the spinning of the wheel.
* It sets the rotation angle by calculating the speed through time based on deceleration.
* Each setRotationAngle call will cause the wheel to be redrawn.
*/
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;
}
}
旋转的实现非常简单——通过随着时间的推移按减速度因子减小速度,并计算和设置旋转角度,直到速度为 0 或直到旋转被关闭(_spinOnOff == false
)。
此类的实例由 spinStartAsync
方法初始化并启动其 run 方法。该方法启动一个新的 Thread
,这样旋转就不会阻塞其余的代码。
public void spinStartAsync(double speed, int direction, double deceleration) throws Exception
{
/*
* Method that starts the spinning thread.
* Parameters:
* speed => degrees per second
* direction => "< 0" = clockwise , "> 0" = counter-clockwise, "=0" = stand still
* deceleration => "< 0" = degrees per second per second reducing speed,
* "= 0" = perpetual spin, "> 0" = throw 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();
}
跟踪旋转速度
为了跟踪旋转速度,还有一个单独的类继承了 TimerTask
类。
private class speedTimerTask extends TimerTask {
/*
* TimerTask class that monitors and refreshes the _spinSpeed
* The speed is calculated as a difference of two rotation angles over a period of time.
* We add the 360 to the "now" angle and then MOD it by 360
* to avoid miscalculation when passing the full circle.
*/
@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);
}
}
}
此类包含 run
方法,该方法也在一个单独的线程中运行——它在一个无限循环中运行,在每次迭代中,它计算一段时间内的角度变化,并基于此设置 _spinSpeed
变量。
历史
- 2020 年 7 月 4 日:初始版本