網站首頁 編程語言 正文
簡介
在開發過程中通常會遇到一些不規則的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; } }
原文鏈接:https://juejin.cn/post/7098140878945927175
相關推薦
- 2022-07-15 在SQL?Server中使用子查詢更新語句_MsSql
- 2022-05-07 以tensorflow庫為例講解Pycharm中如何更新第三方庫_python
- 2023-02-25 go-micro微服務domain層開發示例詳解_Golang
- 2022-05-22 在Centos?8.0中安裝Redis服務器的教程詳解_Redis
- 2023-05-15 Android?onMeasure與onDraw及自定義屬性使用示例_Android
- 2022-12-24 Python利用雪花算法實現生成唯一ID_python
- 2023-01-23 React新擴展函數setState與lazyLoad及hook介紹_React
- 2022-08-15 gin框架中使用websocket發送消息及群聊
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支