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ū)正確對齊。
繪制順序:在旋轉(zhuǎn)過程中,正確處理扇區(qū)的繪制順序,避免前后扇區(qū)遮擋錯(cuò)亂。
動(dòng)畫驅(qū)動(dòng):使用
javax.swing.Timer
或高精度定時(shí)器控制旋轉(zhuǎn)流暢度。交互響應(yīng):支持暫停/繼續(xù)、方向切換、速率調(diào)節(jié)及拖拽控制。
1.2 目標(biāo)
本文將從零開始,手把手實(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)畫過渡。
方向切換:順時(shí)針或逆時(shí)針旋轉(zhuǎn)可動(dòng)態(tài)切換。
拖拽控制:鼠標(biāo)拖拽實(shí)時(shí)控制餅圖角度,打斷/恢復(fù)自動(dòng)旋轉(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 動(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ù):拖拽開始時(shí)停止自動(dòng)旋轉(zhuǎn),釋放時(shí)可繼續(xù)。
方向切換與速率調(diào)節(jié):通過暴露 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
開始拖拽,記錄初始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; // 每像素對應(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); }); } }
五、方法級功能解讀
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
中停止自動(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)遮擋問題;
熟悉 拖拽交互 在圖形組件中的集成。
6.2 性能優(yōu)化建議
Shape 緩存:對每個(gè)扇區(qū)在固定角度步長下預(yù)生成
Path2D
與Arc2D
,避免每幀大量對象創(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)角度的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
IntelliJ?IDEA?2023.1.4?無法刷新Maven項(xiàng)目模塊的問題及解決方法
這篇文章主要介紹了如何排查?IDEA?自身報(bào)錯(cuò)問題,本文以IntelliJ?IDEA?2023.1.4無法刷新項(xiàng)目Maven模塊的問題為例,給大家詳細(xì)講解,需要的朋友可以參考下2023-08-08使用JAVA8 filter對List多條件篩選的實(shí)現(xiàn)
這篇文章主要介紹了使用JAVA8 filter對List多條件篩選的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03Java如何利用狀態(tài)模式(state pattern)替代if else
這篇文章主要給大家介紹了關(guān)于Java如何利用狀態(tài)模式(state pattern)替代if else的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(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的多種方式。簡單地說,正如方法的名稱所表達(dá)的,getBean()負(fù)責(zé)從Spring?IOC容器中獲取bean實(shí)例,希望對大家有所幫助2023-02-02Java 實(shí)現(xiàn)攔截器Interceptor的攔截功能方式
這篇文章主要介紹了Java 實(shí)現(xiàn)攔截器Interceptor的攔截功能方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10Java中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)備了入門級mybatis-plus-generator代碼自動(dòng)生成器,可以提高開發(fā)效率。文中的示例代碼講解詳細(xì),感興趣的可以了解一下2022-05-05