Android?Flutter繪制扇形圖詳解
簡介
在開發(fā)過程中通常會遇到一些不規(guī)則的UI,比如不規(guī)則的線條,多邊形,統(tǒng)計圖表等等,用那些通用組件通過組合的方式無法進行實現(xiàn),這就需要我們自己進行繪制??梢酝ㄟ^使用CuntomPaint
組件并結(jié)合畫筆CustomPainter
去進行手動繪制各種圖形。
CustomPaint介紹
CustomPaint是一個繼承SingleChildRenderObjectWidget的Widget,這里主要介紹幾個重要參數(shù):
child:CustomPaint的子組件。
painter: 畫筆,繪制的圖形會顯示在child后面。
foregroundPainter:前景畫筆,繪制的圖形會顯示在child前面。
size:繪制區(qū)域大小。
CustomPainter介紹
CustomPainter
是一個抽象類,通過自定義一個類繼承自CustomPainter
,重寫paint
和shouldRepaint
方法,具體繪制主要在paint
方法里。
paint介紹
主要兩個參數(shù):
Canvas:畫布,可以用于繪制各種圖形。
Size:繪制區(qū)域的大小。
void paint(Canvas canvas, Size size)
shouldRepaint介紹
在Widget重繪前會調(diào)用該方法確定時候需要重繪,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; } }
繪制
接著我們來實現(xiàn)paint
方法進行繪制
@override void paint(Canvas canvas, Size size) { //移動到中心點 canvas.translate(size.width / 2, size.height / 2); //繪制餅狀圖 _drawPie(canvas, size); //繪制扇形分割線 _drawSpaceLine(canvas); //繪制中心圓 _drawHole(canvas, size); }
繪制餅狀圖
我們以整個畫布的中點為圓點,然后計算出每個扇形的角度區(qū)域,通過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); //為每一個區(qū)域繪制延長線和文字 _drawLineAndText( canvas, size, model.radius, startAngle, sweepAngle, model); startAngle += sweepAngle; } }
繪制延長線以及文本
延長線的起點為扇形區(qū)域邊緣中點位置,長度為一個固定的長度,轉(zhuǎn)折點坐標通過半徑加這個固定長度和三角函數(shù)進行計算,然后通過轉(zhuǎn)折點的位置決定橫線終點的方向,而橫線的長度則根據(jù)文字的寬度決定,然后通過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); // 繪制橫線 // 計算開始坐標以及轉(zhuǎn)折點的坐標 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))); // 計算坐標在左邊還是在右邊 // 并計算橫線結(jié)束坐標 // 如果結(jié)束坐標超過了繪制區(qū)域,則改變結(jié)束坐標的值 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); }
繪制扇形分割線
在繪制完扇形之后,然后在扇形的開始的那條邊上繪制一條直線,起點為圓點,長度為扇形半徑,終點的位置根據(jù)半徑和扇形開始的那條邊的角度用三角函數(shù)進行計算,然后通過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); }
繪制內(nèi)部中心圓
這里可以通過傳入的參數(shù)判斷是否需要繪制這個圓,使用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); } }
觸摸事件處理
接下來我們來處理點擊事件,當我們點擊某一個扇形區(qū)域時,此扇形需要突出顯示,如下圖:
重寫hitTest
方法
注意這個方法的返回值決定是否響應事件。
默認情況下返回null
,事件不會向下傳遞,也不會進行處理; 如果返回true
則當前組件進行處理事件; 如果返回false
則當前組件不會響應點擊事件,會向下一層傳遞;
我直接在這里處理點擊事件,通過該方法傳入的offset
確定點擊的位置,如果點擊位置是在圓形區(qū)域內(nèi)并且不在中心圓內(nèi)則處理事件同時判斷所點擊的具體是哪個扇形,反之則恢復默認狀態(tài)。
@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就可以了。
動畫實現(xiàn)
這里通過Widget
繼承ImplicitlyAnimatedWidget
來實現(xiàn),ImplicitlyAnimatedWidget
是一個抽象類,繼承自StatefulWidget
,既然是StatefulWidget
那肯定還有一個State
,State
繼承AnimatedWidgetBaseState
(此類繼承自ImplicitlyAnimatedWidgetState
),感興趣的小伙伴可以直接去看源碼
實現(xiàn)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
方法,對需要做動畫的參數(shù)進行處理
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繪制扇形圖詳解的詳細內(nèi)容,更多關于Android Flutter扇形圖的資料請關注腳本之家其它相關文章!
相關文章
android 使用 IJKPlayer 播放視頻流的實現(xiàn)代碼
這篇文章主要介紹了android 使用 IJKPlayer 播放視頻流,這需要借助 IAndroidIO 這個接口,也可以用于播放本地文件,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-11-11詳解Android應用開發(fā)中Scroller類的屏幕滑動功能運用
這篇文章主要介紹了詳解Android應用開發(fā)中Scroller類的屏幕滑動功能運用,文中包括各種觸摸滑屏手勢相關方法的示例,需要的朋友可以參考下2016-02-02Android App開發(fā)的自動化測試框架UI Automator使用教程
UI Automator為Android程序的UI開發(fā)提供了測試環(huán)境,這里我們就來看一下Android App開發(fā)的自動化測試框架UI Automator使用教程,需要的朋友可以參考下2016-07-07AndroidStudio實現(xiàn)能在圖片上涂鴉程序
這篇文章主要為大家詳細介紹了AndroidStudio實現(xiàn)能在圖片上涂鴉程序,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-05-05