Java實(shí)現(xiàn)餅圖旋轉(zhuǎn)角度的代碼詳解
一、項(xiàng)目介紹
1.1 背景
在現(xiàn)代數(shù)據(jù)可視化領(lǐng)域,餅圖(Pie Chart)因其直觀展示各部分占整體比例而被廣泛采用。為了增強(qiáng)互動性和吸引力,常會賦予餅圖 旋轉(zhuǎn) 動畫:自動、平滑地旋轉(zhuǎn),讓用戶從不同角度重點(diǎn)查看扇區(qū)。旋轉(zhuǎn)角度可以突出數(shù)據(jù)變化、引導(dǎo)觀看順序、提升界面動感。然而,要在 Java Swing/Java2D 環(huán)境下實(shí)現(xiàn)一個既平滑又可交互的餅圖旋轉(zhuǎn),需要深入掌握以下難點(diǎn):
角度映射:將時間或幀數(shù)映射到旋轉(zhuǎn)角度,并與餅圖扇區(qū)正確對齊。
繪制順序:在旋轉(zhuǎn)過程中,正確處理扇區(qū)的繪制順序,避免前后扇區(qū)遮擋錯亂。
動畫驅(qū)動:使用
javax.swing.Timer或高精度定時器控制旋轉(zhuǎn)流暢度。交互響應(yīng):支持暫停/繼續(xù)、方向切換、速率調(diào)節(jié)及拖拽控制。
1.2 目標(biāo)
本文將從零開始,手把手實(shí)現(xiàn)一個 Java2D Swing 版 的 可旋轉(zhuǎn)餅圖組件,重點(diǎn)在于:
自動旋轉(zhuǎn):按設(shè)定速率平滑且連續(xù)地旋轉(zhuǎn)。
角度控制:可隨時獲取與設(shè)置當(dāng)前旋轉(zhuǎn)角度,實(shí)現(xiàn)“瞬時跳轉(zhuǎn)”或動畫過渡。
方向切換:順時針或逆時針旋轉(zhuǎn)可動態(tài)切換。
拖拽控制:鼠標(biāo)拖拽實(shí)時控制餅圖角度,打斷/恢復(fù)自動旋轉(zhuǎn)。
完整封裝:提供易用 API,支持在任意 Swing 界面中嵌入。
二、相關(guān)技術(shù)與知識
要實(shí)現(xiàn)以上功能,需要掌握和理解以下技術(shù)要點(diǎn)。
2.1 Java2D 繪圖基礎(chǔ)
Graphics2D:Java2D 的核心渲染上下文,支持抗鋸齒、變換、復(fù)合等。形狀構(gòu)造:
Arc2D繪制扇形,Path2D構(gòu)造側(cè)面形狀。抗鋸齒:通過
RenderingHint.KEY_ANTIALIASING提升繪圖質(zhì)量。透明度:使用
AlphaComposite控制半透明效果。
2.2 動畫驅(qū)動
Swing Timer:
javax.swing.Timer在事件分發(fā)線程(EDT)觸發(fā)周期性 事件,安全刷圖。幀率與速率:根據(jù)延遲(delay)和每分鐘旋轉(zhuǎn)度數(shù)(RPM)計(jì)算每幀增量角度
delta = rpm * 360° / (60_000ms / delay)。平滑度:選擇合適的
delay(例如 16ms≈60FPS 或 40ms≈25FPS)平衡流暢度與性能。
2.3 深度排序
雖然我們演示的是 2D 餅圖,但若添加 3D 側(cè)面 或 陰影,則需要 深度排序:在每幀根據(jù)扇區(qū)當(dāng)前中心角度的正余弦值判斷其“前后”關(guān)系,先畫遠(yuǎn)處扇區(qū)再畫近處扇區(qū),保證遮擋效果自然。
2.4 交互處理
鼠標(biāo)拖拽:
MouseListener+MouseMotionListener捕獲按下、拖拽、釋放事件,實(shí)時映射拖動距離到角度偏移。暫停/恢復(fù):拖拽開始時停止自動旋轉(zhuǎn),釋放時可繼續(xù)。
方向切換與速率調(diào)節(jié):通過暴露 API 允許調(diào)用者動態(tài)更改
rpm與clockwise標(biāo)志。
三、實(shí)現(xiàn)思路
結(jié)合上述技術(shù)棧,我們將按以下思路實(shí)現(xiàn):
數(shù)據(jù)模型
定義內(nèi)部
PieSlice類:保存扇區(qū)value、color、label、startAngle、arcAngle。totalValue累加所有扇區(qū)數(shù)值。computeAngles()方法按比例分配角度。
組件封裝
繼承
JPanel,命名為RotatingPieChartPanel,暴露 API:addSlice(value, color, label)setRotateSpeed(rpm)setClockwise(boolean)start()/stop()setAngle(double)/getAngle()實(shí)現(xiàn)“瞬時跳轉(zhuǎn)”。
動畫與繪制
在構(gòu)造器中創(chuàng)建
Timer(animationDelay, e->{ advanceOffset(); repaint(); })。advanceOffset()根據(jù)rpm與clockwise計(jì)算angleOffset。paintComponent()中調(diào)用drawPie(),分三步:陰影 → 側(cè)面(需深度排序) → 頂面。
交互
添加
MouseAdapter:mousePressed開始拖拽,記錄初始angleOffset與鼠標(biāo)點(diǎn);mouseDragged根據(jù)水平方向位移映射到增量角度,更新angleOffset并repaint();mouseReleased結(jié)束拖拽,重啟動畫。
深度排序
在繪制側(cè)面時,先復(fù)制扇區(qū)列表,按每個扇區(qū) 中心角度 的正弦值(或余弦值)排序;
depthKey = Math.sin(Math.toRadians(startAngle + arcAngle/2 + angleOffset)),值大者后繪制。
四、完整實(shí)現(xiàn)代碼
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.*;
import java.util.List;
/**
* RotatingPieChartPanel:可自動/手動旋轉(zhuǎn)且正確排序的餅圖組件
*/
public class RotatingPieChartPanel extends JPanel {
/** 內(nèi)部扇區(qū)模型 */
private static class PieSlice {
double value; // 扇區(qū)數(shù)值
Color color; // 扇區(qū)顏色
String label; // 扇區(qū)標(biāo)簽
double startAngle; // 起始角度(度)
double arcAngle; // 扇區(qū)角度(度)
boolean highlighted; // 是否高亮
PieSlice(double value, Color color, String label) {
this.value = value;
this.color = color;
this.label = label;
this.highlighted = false;
}
}
private final List<PieSlice> slices = new ArrayList<>();
private double totalValue = 0.0;
// 旋轉(zhuǎn)控制
private double angleOffset = 0.0; // 當(dāng)前偏移角度
private double rpm = 1.0; // 每分鐘度數(shù)
private boolean clockwise = true; // 旋轉(zhuǎn)方向
private Timer animationTimer; // 用于自動旋轉(zhuǎn)
// 3D 效果深度(像素)
private double depth = 50.0;
// 拖拽交互狀態(tài)
private boolean dragging = false;
private double dragStartOffset;
private Point dragStartPoint;
public RotatingPieChartPanel() {
setBackground(Color.WHITE);
setPreferredSize(new Dimension(600, 400));
initInteraction();
}
/** 初始化鼠標(biāo)交互:拖拽控制 */
private void initInteraction() {
MouseAdapter ma = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// 停止自動旋轉(zhuǎn),進(jìn)入拖拽狀態(tài)
stop();
dragging = true;
dragStartOffset = angleOffset;
dragStartPoint = e.getPoint();
}
@Override
public void mouseDragged(MouseEvent e) {
if (!dragging) return;
Point pt = e.getPoint();
double dx = pt.x - dragStartPoint.x;
// 每像素對應(yīng) 0.5 度
angleOffset = dragStartOffset + dx * 0.5;
repaint();
}
@Override
public void mouseReleased(MouseEvent e) {
dragging = false;
start(); // 恢復(fù)自動旋轉(zhuǎn)
}
};
addMouseListener(ma);
addMouseMotionListener(ma);
}
/** 添加扇區(qū) */
public void addSlice(double value, Color color, String label) {
slices.add(new PieSlice(value, color, label));
totalValue += value;
computeAngles();
repaint();
}
/** 重新計(jì)算扇區(qū)角度 */
private void computeAngles() {
double angle = 0.0;
for (PieSlice s : slices) {
s.startAngle = angle;
s.arcAngle = s.value / totalValue * 360.0;
angle += s.arcAngle;
}
}
/** 設(shè)置旋轉(zhuǎn)速率(RPM) */
public void setRotateSpeed(double rpm) {
this.rpm = rpm;
if (animationTimer != null && animationTimer.isRunning()) {
stop();
start();
}
}
/** 設(shè)置旋轉(zhuǎn)方向 */
public void setClockwise(boolean cw) {
this.clockwise = cw;
}
/** 設(shè)置 3D 深度 */
public void setDepth(double depth) {
this.depth = depth;
repaint();
}
/** 啟動自動旋轉(zhuǎn) */
public void start() {
if (animationTimer != null && animationTimer.isRunning()) return;
int delay = 40; // 25 FPS
double deltaDeg = rpm * 360.0 / (60_000.0 / delay);
animationTimer = new Timer(delay, e -> {
angleOffset += (clockwise ? -deltaDeg : deltaDeg);
repaint();
});
animationTimer.start();
}
/** 停止自動旋轉(zhuǎn) */
public void stop() {
if (animationTimer != null) {
animationTimer.stop();
animationTimer = null;
}
}
/** 獲取當(dāng)前角度 */
public double getAngle() {
return angleOffset;
}
/** 直接設(shè)置角度(瞬時跳轉(zhuǎn)) */
public void setAngle(double angle) {
this.angleOffset = angle % 360.0;
repaint();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
renderPie((Graphics2D) g);
}
/** 繪制餅圖:陰影 → 側(cè)面(深度排序) → 頂面 */
private void renderPie(Graphics2D g2) {
// 抗鋸齒
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
int w = getWidth(), h = getHeight();
double cx = w / 2.0, cy = h / 2.0 - depth / 2.0;
double r = Math.min(w, h - depth) / 2.0 - 20.0;
// 1. 繪制陰影
drawShadow(g2, cx, cy, r);
// 2. 深度排序并繪制側(cè)面
List<PieSlice> sorted = new ArrayList<>(slices);
sorted.sort(Comparator.comparingDouble(this::depthKey));
for (PieSlice s : sorted) {
drawSide(g2, cx, cy, r, s);
}
// 3. 繪制頂面
for (PieSlice s : sorted) {
drawTop(g2, cx, cy, r, s);
}
}
/** 計(jì)算深度排序 key:扇區(qū)中心角度的 sin 值 */
private double depthKey(PieSlice s) {
double mid = s.startAngle + s.arcAngle / 2.0 + angleOffset;
return Math.sin(Math.toRadians(mid));
}
/** 繪制底部陰影 */
private void drawShadow(Graphics2D g2,
double cx, double cy, double r) {
Ellipse2D shadow = new Ellipse2D.Double(
cx - r, cy + depth - r / 3.0 * 2, 2 * r, r / 2.0
);
Composite old = g2.getComposite();
g2.setComposite(AlphaComposite.getInstance(
AlphaComposite.SRC_OVER, 0.3f
));
g2.setColor(Color.BLACK);
g2.fill(shadow);
g2.setComposite(old);
}
/** 繪制扇區(qū)側(cè)面 */
private void drawSide(Graphics2D g2,
double cx, double cy, double r, PieSlice s) {
double sa = Math.toRadians(s.startAngle + angleOffset);
double ea = Math.toRadians(s.startAngle + s.arcAngle + angleOffset);
Point2D p1 = new Point2D.Double(
cx + r * Math.cos(sa), cy + r * Math.sin(sa)
);
Point2D p2 = new Point2D.Double(
cx + r * Math.cos(ea), cy + r * Math.sin(ea)
);
Point2D p3 = new Point2D.Double(p2.getX(), p2.getY() + depth);
Point2D p4 = new Point2D.Double(p1.getX(), p1.getY() + depth);
Path2D side = new Path2D.Double();
side.moveTo(p1.getX(), p1.getY());
side.lineTo(p4.getX(), p4.getY());
side.lineTo(p3.getX(), p3.getY());
side.lineTo(p2.getX(), p2.getY());
side.closePath();
g2.setColor(s.color.darker());
g2.fill(side);
if (s.highlighted) {
g2.setColor(Color.WHITE);
g2.setStroke(new BasicStroke(2));
g2.draw(side);
}
}
/** 繪制扇區(qū)頂面 */
private void drawTop(Graphics2D g2,
double cx, double cy, double r, PieSlice s) {
Arc2D top = new Arc2D.Double(
cx - r, cy - r, 2 * r, 2 * r,
s.startAngle + angleOffset,
s.arcAngle, Arc2D.PIE
);
g2.setColor(s.color);
g2.fill(top);
if (s.highlighted) {
g2.setColor(Color.WHITE);
g2.setStroke(new BasicStroke(2));
g2.draw(top);
}
}
// 可擴(kuò)展:添加高亮與提示功能
}
/**
* DemoMain:演示 RotatingPieChartPanel 用法
*/
class DemoMain {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
RotatingPieChartPanel pie = new RotatingPieChartPanel();
pie.addSlice(30, Color.RED, "紅");
pie.addSlice(20, Color.BLUE, "藍(lán)");
pie.addSlice(40, Color.GREEN, "綠");
pie.addSlice(10, Color.ORANGE,"橙");
pie.setDepth(60);
pie.setRotateSpeed(2.5);
pie.setClockwise(false);
pie.start();
JFrame f = new JFrame("可旋轉(zhuǎn)餅圖示例");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(pie);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
});
}
}五、方法級功能解讀
addSlice(value, color, label)創(chuàng)建
PieSlice對象,累加totalValue,調(diào)用computeAngles()重新計(jì)算所有扇區(qū)的角度分布。
computeAngles()遍歷
slices列表,按比例(value / totalValue) * 360°分配各扇區(qū)arcAngle,并依次累加startAngle。
start()/stop()使用
javax.swing.Timer:delay = 40ms,每次actionPerformed中計(jì)算增量
double deltaDeg = rpm * 360.0 / (60_000.0 / delay); angleOffset += clockwise ? -deltaDeg : deltaDeg;
- 調(diào)用
repaint()刷新組件。
paintComponent(...)先
super.paintComponent(g)清除背景,然后調(diào)用renderPie(g2):啟用抗鋸齒
計(jì)算中心
(cx,cy)與半徑r調(diào)用
drawShadow、drawSide(深度排序)和drawTop
depthKey(PieSlice s)計(jì)算扇區(qū)中心角度:
s.startAngle + s.arcAngle/2 + angleOffset取正弦值作為深度排序依據(jù)(越大越“前”),并對列表排序,保證先畫“后面”的側(cè)面,再畫“前面”的側(cè)面與頂面。
drawShadow底部繪制半透明黑色橢圓,使用
AlphaComposite設(shè)為 0.3f。
drawSide計(jì)算扇區(qū)邊緣兩點(diǎn)
(p1, p2),并向下延伸depth得到底部兩點(diǎn)(p4, p3);構(gòu)造
Path2D四邊形填充較暗顏色;
drawTop使用
Arc2D.PIE繪制扇形頂面;
拖拽交互
mousePressed中停止自動旋轉(zhuǎn)并記錄初始狀態(tài);mouseDragged根據(jù)水平位移映射到增量角度更新angleOffset;mouseReleased中恢復(fù)自動旋轉(zhuǎn)。
六、項(xiàng)目總結(jié)與擴(kuò)展思考
6.1 核心收獲
深入理解 Java2D 在復(fù)雜動態(tài)圖形中的應(yīng)用技巧;
掌握 旋轉(zhuǎn)動畫 與 幀率控制 的實(shí)現(xiàn);
學(xué)會使用 深度排序 解決旋轉(zhuǎn)遮擋問題;
熟悉 拖拽交互 在圖形組件中的集成。
6.2 性能優(yōu)化建議
Shape 緩存:對每個扇區(qū)在固定角度步長下預(yù)生成
Path2D與Arc2D,避免每幀大量對象創(chuàng)建。離屏緩沖:使用
BufferedImage或VolatileImage離屏渲染靜態(tài)部分(陰影、側(cè)面基礎(chǔ)形狀),只動態(tài)繪制旋轉(zhuǎn)部分。OpenGL 加速:設(shè)置系統(tǒng)屬性
-Dsun.java2d.opengl=true啟用硬件加速。
6.3 擴(kuò)展功能
漸變與紋理:為扇面添加漸變填充或貼圖。
多層餅圖/環(huán)形圖:支持環(huán)形(Donut)或嵌套餅圖。
標(biāo)簽與引導(dǎo)線:在旋轉(zhuǎn)中動態(tài)顯示標(biāo)簽,引導(dǎo)線可選顯示。
JavaFX 版本:基于 JavaFX Canvas 或 3D API 實(shí)現(xiàn)更高性能和光照效果。
以上就是Java實(shí)現(xiàn)餅圖旋轉(zhuǎn)角度的代碼詳解的詳細(xì)內(nèi)容,更多關(guān)于Java餅圖旋轉(zhuǎn)角度的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java虛擬線程原理與實(shí)踐指南(含實(shí)際案例)
Java虛擬線程是一種輕量級線程,由JVM管理而非操作系統(tǒng),這篇文章主要介紹了Java虛擬線程原理與實(shí)踐指南的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2026-06-06
創(chuàng)建首個 Spring Boot 登錄項(xiàng)目的全攻略
在Java Web開發(fā)中,登錄功能是最基礎(chǔ)也是最重要的模塊之一,本文將手把手帶你使用IntelliJ IDEA和Maven構(gòu)建一個基于Spring Boot的簡單登錄系統(tǒng),感興趣的朋友跟隨小編一起看看吧2025-06-06
基于Java判斷網(wǎng)絡(luò)是否正常代碼實(shí)例
這篇文章主要介紹了基于Java判斷網(wǎng)絡(luò)是否正常代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-03-03
SpringMVC的注解@RequestMapping屬性及使用
這篇文章主要為大家介紹了SpringMVC注解@RequestMapping屬性及使用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
Java實(shí)現(xiàn)微信網(wǎng)頁授權(quán)的示例代碼
這篇文章主要介紹了Java實(shí)現(xiàn)微信網(wǎng)頁授權(quán)的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-07-07
Java的Hibernate框架結(jié)合MySQL的入門學(xué)習(xí)教程
Java世界中的SSH三大框架是Web開發(fā)方面的人氣組合,Hibernate便是其中之一,這里我們來整理一下Java的Hibernate框架結(jié)合MySQL的入門學(xué)習(xí)教程,需要的朋友可以參考下2016-07-07
服務(wù)器實(shí)現(xiàn)Java遠(yuǎn)程訪問Linux服務(wù)器方式(JSch)
文章介紹了如何使用Java遠(yuǎn)程訪問Linux服務(wù)器,主要包括建立SSH連接、使用JSch庫執(zhí)行命令、解析返回值以及關(guān)閉連接的步驟2024-11-11

