Java實(shí)現(xiàn)餅圖旋轉(zhuǎn)角度的代碼詳解
一、項(xiàng)目介紹
1.1 背景
在現(xiàn)代數(shù)據(jù)可視化領(lǐng)域,餅圖(Pie Chart)因其直觀展示各部分占整體比例而被廣泛采用。為了增強(qiáng)互動(dòng)性和吸引力,常會(huì)賦予餅圖 旋轉(zhuǎn) 動(dòng)畫:自動(dòng)、平滑地旋轉(zhuǎn),讓用戶從不同角度重點(diǎn)查看扇區(qū)。旋轉(zhuǎn)角度可以突出數(shù)據(jù)變化、引導(dǎo)觀看順序、提升界面動(dòng)感。然而,要在 Java Swing/Java2D 環(huán)境下實(shí)現(xiàn)一個(gè)既平滑又可交互的餅圖旋轉(zhuǎn),需要深入掌握以下難點(diǎn):
角度映射:將時(shí)間或幀數(shù)映射到旋轉(zhuǎn)角度,并與餅圖扇區(qū)正確對(duì)齊。
繪制順序:在旋轉(zhuǎn)過(guò)程中,正確處理扇區(qū)的繪制順序,避免前后扇區(qū)遮擋錯(cuò)亂。
動(dòng)畫驅(qū)動(dòng):使用
javax.swing.Timer或高精度定時(shí)器控制旋轉(zhuǎn)流暢度。交互響應(yīng):支持暫停/繼續(xù)、方向切換、速率調(diào)節(jié)及拖拽控制。
1.2 目標(biāo)
本文將從零開(kāi)始,手把手實(shí)現(xiàn)一個(gè) Java2D Swing 版 的 可旋轉(zhuǎn)餅圖組件,重點(diǎn)在于:
自動(dòng)旋轉(zhuǎn):按設(shè)定速率平滑且連續(xù)地旋轉(zhuǎn)。
角度控制:可隨時(shí)獲取與設(shè)置當(dāng)前旋轉(zhuǎn)角度,實(shí)現(xiàn)“瞬時(shí)跳轉(zhuǎn)”或動(dòng)畫過(guò)渡。
方向切換:順時(shí)針或逆時(shí)針旋轉(zhuǎn)可動(dòng)態(tài)切換。
拖拽控制:鼠標(biāo)拖拽實(shí)時(shí)控制餅圖角度,打斷/恢復(fù)自動(dòng)旋轉(zhuǎn)。
完整封裝:提供易用 API,支持在任意 Swing 界面中嵌入。
二、相關(guān)技術(shù)與知識(shí)
要實(shí)現(xiàn)以上功能,需要掌握和理解以下技術(shù)要點(diǎn)。
2.1 Java2D 繪圖基礎(chǔ)
Graphics2D:Java2D 的核心渲染上下文,支持抗鋸齒、變換、復(fù)合等。形狀構(gòu)造:
Arc2D繪制扇形,Path2D構(gòu)造側(cè)面形狀。抗鋸齒:通過(guò)
RenderingHint.KEY_ANTIALIASING提升繪圖質(zhì)量。透明度:使用
AlphaComposite控制半透明效果。
2.2 動(dòng)畫驅(qū)動(dòng)
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í)時(shí)映射拖動(dòng)距離到角度偏移。暫停/恢復(fù):拖拽開(kāi)始時(shí)停止自動(dòng)旋轉(zhuǎn),釋放時(shí)可繼續(xù)。
方向切換與速率調(diào)節(jié):通過(guò)暴露 API 允許調(diào)用者動(dòng)態(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)“瞬時(shí)跳轉(zhuǎn)”。
動(dòng)畫與繪制
在構(gòu)造器中創(chuàng)建
Timer(animationDelay, e->{ advanceOffset(); repaint(); })。advanceOffset()根據(jù)rpm與clockwise計(jì)算angleOffset。paintComponent()中調(diào)用drawPie(),分三步:陰影 → 側(cè)面(需深度排序) → 頂面。
交互
添加
MouseAdapter:mousePressed開(kāi)始拖拽,記錄初始angleOffset與鼠標(biāo)點(diǎn);mouseDragged根據(jù)水平方向位移映射到增量角度,更新angleOffset并repaint();mouseReleased結(jié)束拖拽,重啟動(dòng)畫。
深度排序
在繪制側(cè)面時(shí),先復(fù)制扇區(qū)列表,按每個(gè)扇區(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:可自動(dòng)/手動(dòng)旋轉(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; // 用于自動(dòng)旋轉(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) {
// 停止自動(dòng)旋轉(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;
// 每像素對(duì)應(yīng) 0.5 度
angleOffset = dragStartOffset + dx * 0.5;
repaint();
}
@Override
public void mouseReleased(MouseEvent e) {
dragging = false;
start(); // 恢復(fù)自動(dòng)旋轉(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();
}
/** 啟動(dòng)自動(dòng)旋轉(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();
}
/** 停止自動(dòng)旋轉(zhuǎn) */
public void stop() {
if (animationTimer != null) {
animationTimer.stop();
animationTimer = null;
}
}
/** 獲取當(dāng)前角度 */
public double getAngle() {
return angleOffset;
}
/** 直接設(shè)置角度(瞬時(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);
});
}
}五、方法級(jí)功能解讀
addSlice(value, color, label)創(chuàng)建
PieSlice對(duì)象,累加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ù)(越大越“前”),并對(duì)列表排序,保證先畫“后面”的側(cè)面,再畫“前面”的側(cè)面與頂面。
drawShadow底部繪制半透明黑色橢圓,使用
AlphaComposite設(shè)為 0.3f。
drawSide計(jì)算扇區(qū)邊緣兩點(diǎn)
(p1, p2),并向下延伸depth得到底部?jī)牲c(diǎn)(p4, p3);構(gòu)造
Path2D四邊形填充較暗顏色;
drawTop使用
Arc2D.PIE繪制扇形頂面;
拖拽交互
mousePressed中停止自動(dòng)旋轉(zhuǎn)并記錄初始狀態(tài);mouseDragged根據(jù)水平位移映射到增量角度更新angleOffset;mouseReleased中恢復(fù)自動(dòng)旋轉(zhuǎn)。
六、項(xiàng)目總結(jié)與擴(kuò)展思考
6.1 核心收獲
深入理解 Java2D 在復(fù)雜動(dòng)態(tài)圖形中的應(yīng)用技巧;
掌握 旋轉(zhuǎn)動(dòng)畫 與 幀率控制 的實(shí)現(xiàn);
學(xué)會(huì)使用 深度排序 解決旋轉(zhuǎn)遮擋問(wèn)題;
熟悉 拖拽交互 在圖形組件中的集成。
6.2 性能優(yōu)化建議
Shape 緩存:對(duì)每個(gè)扇區(qū)在固定角度步長(zhǎng)下預(yù)生成
Path2D與Arc2D,避免每幀大量對(duì)象創(chuàng)建。離屏緩沖:使用
BufferedImage或VolatileImage離屏渲染靜態(tài)部分(陰影、側(cè)面基礎(chǔ)形狀),只動(dòng)態(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)中動(dòng)態(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)角度的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
IntelliJ?IDEA?2023.1.4?無(wú)法刷新Maven項(xiàng)目模塊的問(wèn)題及解決方法
這篇文章主要介紹了如何排查?IDEA?自身報(bào)錯(cuò)問(wèn)題,本文以IntelliJ?IDEA?2023.1.4無(wú)法刷新項(xiàng)目Maven模塊的問(wèn)題為例,給大家詳細(xì)講解,需要的朋友可以參考下2023-08-08
使用JAVA8 filter對(duì)List多條件篩選的實(shí)現(xiàn)
這篇文章主要介紹了使用JAVA8 filter對(duì)List多條件篩選的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
Java如何利用狀態(tài)模式(state pattern)替代if else
這篇文章主要給大家介紹了關(guān)于Java如何利用狀態(tài)模式(state pattern)替代if else的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
基于HttpServletRequest 相關(guān)常用方法的應(yīng)用
本篇文章小編為大家介紹,基于HttpServletRequest 相關(guān)常用方法的應(yīng)用,需要的朋友參考下2013-04-04
深入了解Spring中g(shù)etBean()的五種方式
在本文中,我們將詳細(xì)介紹從BeanFactory中獲取bean的多種方式。簡(jiǎn)單地說(shuō),正如方法的名稱所表達(dá)的,getBean()負(fù)責(zé)從Spring?IOC容器中獲取bean實(shí)例,希望對(duì)大家有所幫助2023-02-02
Java 實(shí)現(xiàn)攔截器Interceptor的攔截功能方式
這篇文章主要介紹了Java 實(shí)現(xiàn)攔截器Interceptor的攔截功能方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10
Java中final、static關(guān)鍵字與方法的重寫和繼承易錯(cuò)點(diǎn)整理
這篇文章主要給大家介紹了關(guān)于Java中final、static關(guān)鍵字與方法的重寫和繼承易錯(cuò)點(diǎn)的相關(guān)資料,在Java編程中final關(guān)鍵字用于限制方法或類的進(jìn)一步修改,final方法不能被子類重寫,而static方法不可被重寫,只能被遮蔽,需要的朋友可以參考下2024-10-10
基于mybatis-plus-generator實(shí)現(xiàn)代碼自動(dòng)生成器
這篇文章專門為小白準(zhǔn)備了入門級(jí)mybatis-plus-generator代碼自動(dòng)生成器,可以提高開(kāi)發(fā)效率。文中的示例代碼講解詳細(xì),感興趣的可以了解一下2022-05-05

