65.9K
CodeProject 正在变化。 阅读更多。
Home

Java 版幸运转盘

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (5投票s)

2020 年 7 月 4 日

CPOL

12分钟阅读

viewsIcon

29287

downloadIcon

1432

用 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 TickWheel 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

算法如下

  1. 首先,我们找到 ArrayList 中最长的 string
  2. 然后,我们将其设置为最大允许字体大小(final int MAXFONTSIZE)。
  3. 然后,我们根据 String 的最大可能高度调整字体大小。为此,我们使用 java.awt.FontMetrics 以及一些基本的三角学规则,例如毕达哥拉斯定理和三角形相似。
  4. 然后,通过反复试验调整 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 日:初始版本
© . All rights reserved.