Android Flutter繪制扇形圖詳解
簡介
在開發過程中通常會遇到一些不規則的UI,比如不規則的線條,多邊形,統計圖表等等,用那些通用組件通過組合的方式無法進行實現,這就需要我們自己進行繪制。可以通過使用CuntomPaint
組件並結合畫筆CustomPainter
去進行手動繪制各種圖形。
CustomPaint介紹
CustomPaint是一個繼承SingleChildRenderObjectWidget的Widget,這裡主要介紹幾個重要參數:
child:CustomPaint的子組件。
painter: 畫筆,繪制的圖形會顯示在child後面。
foregroundPainter:前景畫筆,繪制的圖形會顯示在child前面。
size:繪制區域大小。
CustomPainter介紹
CustomPainter
是一個抽象類,通過自定義一個類繼承自CustomPainter
,重寫paint
和shouldRepaint
方法,具體繪制主要在paint
方法裡。
paint介紹
主要兩個參數:
Canvas:畫佈,可以用於繪制各種圖形。
Size:繪制區域的大小。
void paint(Canvas canvas, Size size)
shouldRepaint介紹
在Widget重繪前會調用該方法確定時候需要重繪,shouldRepaint
返回ture
表示需要重繪,返回false
表示不需要重繪。
bool shouldRepaint(CustomPainter oldDelegate)
示例
這裡我們通過繪制一個餅狀圖來演示繪制的整體流程。
使用CustomPaint
首先,使用CustomPaint
,繪制大小為父組件最大值,傳入自定義painter
。
@override Widget build(BuildContext context) { return CustomPaint( size: Size.infinite, painter: PieChartPainter(), ); }
自定義Painter
自定義PieChartPainter
繼承CustomPainter
class PieChartPainters extends CustomPainter { @override void paint(Canvas canvas, Size size) { } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return oldDelegate != this; } }
繪制
接著我們來實現paint
方法進行繪制
@override void paint(Canvas canvas, Size size) { //移動到中心點 canvas.translate(size.width / 2, size.height / 2); //繪制餅狀圖 _drawPie(canvas, size); //繪制扇形分割線 _drawSpaceLine(canvas); //繪制中心圓 _drawHole(canvas, size); }
繪制餅狀圖
我們以整個畫佈的中點為圓點,然後計算出每個扇形的角度區域,通過canvas.drawArc
繪制扇形。
void _drawPie(Canvas canvas, Size size) { var startAngle = 0.0; var sumValue = models.fold<double>(0.0, (sum, model) => sum + model.value); for (var model in models) { Paint paint = Paint() ..style = PaintingStyle.fill ..color = model.color; var sweepAngle = model.value / sumValue * 360; canvas.drawArc(Rect.fromCircle(radius: model.radius, center: Offset.zero), startAngle * pi / 180, sweepAngle * pi / 180, true, paint); //為每一個區域繪制延長線和文字 _drawLineAndText( canvas, size, model.radius, startAngle, sweepAngle, model); startAngle += sweepAngle; } }
繪制延長線以及文本
延長線的起點為扇形區域邊緣中點位置,長度為一個固定的長度,轉折點坐標通過半徑加這個固定長度和三角函數進行計算,然後通過轉折點的位置決定橫線終點的方向,而橫線的長度則根據文字的寬度決定,然後通過canvas.drawLine
進行繪制直線。
文本繪制使用TextPainter.paint
進行繪制,paint
方法裡面最終是通過canvas.drawParagraph
進行繪制的。
最後再在文字的前面通過canvas.drawCircle
繪制一個小圓點。
void _drawLineAndText(Canvas canvas, Size size, double radius, double startAngle, double sweepAngle, PieChartModel model) { var ratio = (sweepAngle / 360.0 * 100).toStringAsFixed(2); var top = Text(model.name); var topTextPainter = getTextPainter(top); var bottom = Text("$ratio%"); var bottomTextPainter = getTextPainter(bottom); // 繪制橫線 // 計算開始坐標以及轉折點的坐標 var startX = radius * (cos((startAngle + (sweepAngle / 2)) * (pi / 180))); var startY = radius * (sin((startAngle + (sweepAngle / 2)) * (pi / 180))); var firstLine = radius / 5; var secondLine = max(bottomTextPainter.width, topTextPainter.width) + radius / 4; var pointX = (radius + firstLine) * (cos((startAngle + (sweepAngle / 2)) * (pi / 180))); var pointY = (radius + firstLine) * (sin((startAngle + (sweepAngle / 2)) * (pi / 180))); // 計算坐標在左邊還是在右邊 // 並計算橫線結束坐標 // 如果結束坐標超過瞭繪制區域,則改變結束坐標的值 var marginOffset = 20.0; // 距離繪制邊界的偏移量 var endX = 0.0; if (pointX - startX > 0) { endX = min(pointX + secondLine, size.width / 2 - marginOffset); secondLine = endX - pointX; } else { endX = max(pointX - secondLine, -size.width / 2 + marginOffset); secondLine = pointX - endX; } Paint paint = Paint() ..style = PaintingStyle.fill ..strokeWidth = 1 ..color = Colors.grey; // 繪制延長線 canvas.drawLine(Offset(startX, startY), Offset(pointX, pointY), paint); canvas.drawLine(Offset(pointX, pointY), Offset(endX, pointY), paint); // 文字距離中間橫線上下間距偏移量 var offset = 4; var textWidth = bottomTextPainter.width; var textStartX = 0.0; textStartX = _calculateTextStartX(pointX, startX, textWidth, secondLine, textStartX, offset); bottomTextPainter.paint(canvas, Offset(textStartX, pointY + offset)); textWidth = topTextPainter.width; var textHeight = topTextPainter.height; textStartX = _calculateTextStartX(pointX, startX, textWidth, secondLine, textStartX, offset); topTextPainter.paint(canvas, Offset(textStartX, pointY - offset - textHeight)); // 繪制文字前面的小圓點 paint.color = model.color; canvas.drawCircle( Offset(textStartX - 8, pointY - 4 - topTextPainter.height / 2), 4, paint); }
繪制扇形分割線
在繪制完扇形之後,然後在扇形的開始的那條邊上繪制一條直線,起點為圓點,長度為扇形半徑,終點的位置根據半徑和扇形開始的那條邊的角度用三角函數進行計算,然後通過canvas.drawLine
進行繪制。
void _drawSpaceLine(Canvas canvas) { var sumValue = models.fold<double>(0.0, (sum, model) => sum + model.value); var startAngle = 0.0; for (var model in models) { _drawLine(canvas, startAngle, model.radius); startAngle += model.value / sumValue * 360; } } void _drawLine(Canvas canvas, double angle, double radius) { var endX = cos(angle * pi / 180) * radius; var endY = sin(angle * pi / 180) * radius; Paint paint = Paint() ..style = PaintingStyle.fill ..color = Colors.white ..strokeWidth = spaceWidth; canvas.drawLine(Offset.zero, Offset(endX, endY), paint); }
繪制內部中心圓
這裡可以通過傳入的參數判斷是否需要繪制這個圓,使用canvas.drawCircle
進行繪制一個與背景色一致的圓。
void _drawHole(Canvas canvas, Size size) { if (isShowHole) { holePath.reset(); Paint paint = Paint() ..style = PaintingStyle.fill ..color = Colors.white; canvas.drawCircle(Offset.zero, holeRadius, paint); } }
觸摸事件處理
接下來我們來處理點擊事件,當我們點擊某一個扇形區域時,此扇形需要突出顯示,如下圖:
重寫hitTest
方法
註意這個方法的返回值決定是否響應事件。
默認情況下返回null
,事件不會向下傳遞,也不會進行處理; 如果返回true
則當前組件進行處理事件; 如果返回false
則當前組件不會響應點擊事件,會向下一層傳遞;
我直接在這裡處理點擊事件,通過該方法傳入的offset
確定點擊的位置,如果點擊位置是在圓形區域內並且不在中心圓內則處理事件同時判斷所點擊的具體是哪個扇形,反之則恢復默認狀態。
@override bool? hitTest(Offset offset) { if (oldTapOffset.dx==offset.dx && oldTapOffset.dy==offset.dy) { return false; } oldTapOffset = offset; for (int i = 0; i < paths.length; i++) { if (paths[i].contains(offset) && !holePath.contains(offset)) { onTap?.call(i); oldTapOffset = offset; return true; } } onTap?.call(-1); return false; }
至此,我們通過onTap
向上傳遞出點擊的是第幾個扇形,然後進行處理,更新UI就可以瞭。
動畫實現
這裡通過Widget
繼承ImplicitlyAnimatedWidget
來實現,ImplicitlyAnimatedWidget
是一個抽象類,繼承自StatefulWidget
,既然是StatefulWidget
那肯定還有一個State
,State
繼承AnimatedWidgetBaseState
(此類繼承自ImplicitlyAnimatedWidgetState
),感興趣的小夥伴可以直接去看源碼
實現AnimatedWidgetBaseState
裡面的forEachTween
方法,主要是用於來更新Tween的初始值。
@override void forEachTween(TweenVisitor<dynamic>visitor) { customPieTween = visitor(customPieTween, end, (dynamic value) { return CustomPieTween(begin: value, end: end); }) as CustomPieTween; }
自定義CustomPieTween
繼承自Tween
,重寫lerp
方法,對需要做動畫的參數進行處理
class CustomPieTween extends Tween<List<PieChartModel>> { CustomPieTween({List<PieChartModel>? begin, List<PieChartModel>? end}) : super(begin: begin, end: end); @override List<PieChartModel> lerp(double t) { List<PieChartModel> list = []; begin?.asMap().forEach((index, model) { list.add(model ..radius = lerpDouble(model.radius, end?[index].radius ?? 100.0, t)); }); return list; } double lerpDouble(double radius, double radius2, double t) { if (radius == radius2) { return radius; } var d = (radius2 - radius) * t; var value = radius + d; return value; } }
以上就是Android Flutter繪制扇形圖詳解的詳細內容,更多關於Android Flutter扇形圖的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Android Flutter利用CustomPaint繪制基本圖形詳解
- Flutter CustomPaint自定義繪畫示例詳解
- Flutter構建自定義Widgets的全過程記錄
- Flutter繪制3.4邊形及多邊形漸變動畫實現示例
- 如何使用Flutter實現手寫簽名效果