|
|
@@ -0,0 +1,258 @@
|
|
|
+import 'dart:math';
|
|
|
+import 'package:fl_chart/fl_chart.dart';
|
|
|
+import 'package:flutter/material.dart';
|
|
|
+import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
|
+import 'package:location/module/track/track_day_detail/time_proportion/pie_chat_data.dart';
|
|
|
+import 'package:location/resource/assets.gen.dart';
|
|
|
+import 'package:location/utils/common_expand.dart';
|
|
|
+
|
|
|
+import '../../track_util.dart';
|
|
|
+
|
|
|
+class TrackTimePieChat extends StatefulWidget {
|
|
|
+ final List<PieChatData> pieData;
|
|
|
+
|
|
|
+ const TrackTimePieChat({required this.pieData, super.key});
|
|
|
+
|
|
|
+ @override
|
|
|
+ State<TrackTimePieChat> createState() => _TrackTimePieChatState();
|
|
|
+}
|
|
|
+
|
|
|
+class _TrackTimePieChatState extends State<TrackTimePieChat> {
|
|
|
+ int touchedIndex = 0;
|
|
|
+ final double baseRadius = 52.w;
|
|
|
+ final double bgChatSize = 216.w;
|
|
|
+
|
|
|
+ @override
|
|
|
+ void initState() {
|
|
|
+ super.initState();
|
|
|
+ if (widget.pieData.isNotEmpty) {
|
|
|
+ setDefaultMaxIndex();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ void didUpdateWidget(covariant TrackTimePieChat oldWidget) {
|
|
|
+ super.didUpdateWidget(oldWidget);
|
|
|
+ if (widget.pieData != oldWidget.pieData && widget.pieData.isNotEmpty) {
|
|
|
+ setDefaultMaxIndex();
|
|
|
+ } else if (widget.pieData.isEmpty) {
|
|
|
+ touchedIndex = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ //设置占比最大一个为默认的index
|
|
|
+ void setDefaultMaxIndex() {
|
|
|
+ final maxProportion =
|
|
|
+ widget.pieData.map((e) => e.proportion).reduce((a, b) => a > b ? a : b);
|
|
|
+ touchedIndex =
|
|
|
+ widget.pieData.indexWhere((e) => e.proportion == maxProportion);
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context) {
|
|
|
+ return Column(
|
|
|
+ children: [
|
|
|
+ SizedBox(height: 10.w),
|
|
|
+ buildPieChatView(),
|
|
|
+ SizedBox(height: 6.w),
|
|
|
+ buildSelectPieTextView(),
|
|
|
+ Spacer(),
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget buildSelectPieTextView() {
|
|
|
+ return IntrinsicWidth(
|
|
|
+ child: Row(
|
|
|
+ mainAxisAlignment: MainAxisAlignment.center,
|
|
|
+ children: [
|
|
|
+ Container(
|
|
|
+ width: 12.w,
|
|
|
+ height: 12.w,
|
|
|
+ decoration: BoxDecoration(
|
|
|
+ color: widget.pieData[touchedIndex].color,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ SizedBox(width: 6.w),
|
|
|
+ Expanded(
|
|
|
+ child: Text(
|
|
|
+ widget.pieData[touchedIndex].address,
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: 12.sp,
|
|
|
+ color: '#333333'.color,
|
|
|
+ fontWeight: FontWeight.bold),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget buildPieChatView() {
|
|
|
+ return Container(
|
|
|
+ width: bgChatSize,
|
|
|
+ height: bgChatSize,
|
|
|
+ decoration: BoxDecoration(
|
|
|
+ image: DecorationImage(
|
|
|
+ image: Assets.images.bgTrackPieChat.provider(),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ child: Stack(
|
|
|
+ children: [
|
|
|
+ PieChart(
|
|
|
+ PieChartData(
|
|
|
+ sectionsSpace: 0,
|
|
|
+ centerSpaceRadius: 0,
|
|
|
+ startDegreeOffset: -90,
|
|
|
+ pieTouchData: PieTouchData(
|
|
|
+ touchCallback: (event, response) {
|
|
|
+ setState(() {
|
|
|
+ final index = response?.touchedSection?.touchedSectionIndex;
|
|
|
+ if (index != null && index >= 0) {
|
|
|
+ touchedIndex = index;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ sections: List.generate(widget.pieData.length, (i) {
|
|
|
+ final item = widget.pieData[i];
|
|
|
+ return PieChartSectionData(
|
|
|
+ color: item.color,
|
|
|
+ value: item.proportion,
|
|
|
+ title: '${item.proportion}%',
|
|
|
+ radius: baseRadius,
|
|
|
+ titleStyle: TextStyle(
|
|
|
+ fontSize: 12.sp,
|
|
|
+ fontWeight: FontWeight.bold,
|
|
|
+ color: Colors.white,
|
|
|
+ ),
|
|
|
+ titlePositionPercentageOffset:
|
|
|
+ widget.pieData.length <= 1 ? 0 : 0.6,
|
|
|
+ );
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ buildIgnorePointer()
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget buildIgnorePointer() {
|
|
|
+ return IgnorePointer(
|
|
|
+ child: CustomPaint(
|
|
|
+ size: Size(bgChatSize, bgChatSize),
|
|
|
+ painter: PieLineLabelPainter(
|
|
|
+ center: Offset(bgChatSize / 2, bgChatSize / 2),
|
|
|
+ baseRadius: baseRadius,
|
|
|
+ startAngleOffset: -90,
|
|
|
+ data: widget.pieData,
|
|
|
+ touchedIndex: touchedIndex,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class PieLineLabelPainter extends CustomPainter {
|
|
|
+ final Offset center;
|
|
|
+ final double baseRadius;
|
|
|
+ final double startAngleOffset;
|
|
|
+ final List<PieChatData> data;
|
|
|
+ final int? touchedIndex;
|
|
|
+
|
|
|
+ PieLineLabelPainter({
|
|
|
+ required this.center,
|
|
|
+ required this.baseRadius,
|
|
|
+ required this.startAngleOffset,
|
|
|
+ required this.data,
|
|
|
+ required this.touchedIndex,
|
|
|
+ });
|
|
|
+
|
|
|
+ @override
|
|
|
+ void paint(Canvas canvas, Size size) {
|
|
|
+ if (touchedIndex == null ||
|
|
|
+ touchedIndex! < 0 ||
|
|
|
+ touchedIndex! >= data.length) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ final paint = Paint()
|
|
|
+ ..color = data[touchedIndex!].color
|
|
|
+ ..strokeWidth = 1.5;
|
|
|
+
|
|
|
+ final total = data.fold(0, (sum, e) => sum + e.proportion.toInt());
|
|
|
+ double angle = startAngleOffset * pi / 180;
|
|
|
+
|
|
|
+ for (int i = 0; i < data.length; i++) {
|
|
|
+ final sweepAngle = (data[i].proportion / total) * 2 * pi;
|
|
|
+
|
|
|
+ if (i == touchedIndex) {
|
|
|
+ final midAngle = angle + sweepAngle / 2;
|
|
|
+
|
|
|
+ final startR = baseRadius + 1;
|
|
|
+ final bendR = startR + 25;
|
|
|
+
|
|
|
+ final start = Offset(
|
|
|
+ center.dx + cos(midAngle) * startR,
|
|
|
+ center.dy + sin(midAngle) * startR,
|
|
|
+ );
|
|
|
+
|
|
|
+ final bend = Offset(
|
|
|
+ center.dx + cos(midAngle) * bendR,
|
|
|
+ center.dy + sin(midAngle) * bendR,
|
|
|
+ );
|
|
|
+
|
|
|
+ final isRight = cos(midAngle) >= 0;
|
|
|
+ final end = Offset(
|
|
|
+ bend.dx + (isRight ? 30 : -30),
|
|
|
+ bend.dy,
|
|
|
+ );
|
|
|
+
|
|
|
+ canvas.drawLine(start, bend, paint);
|
|
|
+ canvas.drawLine(bend, end, paint);
|
|
|
+ canvas.drawCircle(end, 3, paint);
|
|
|
+
|
|
|
+ // 文本与色块显示在 end 的上方
|
|
|
+ double rectSize = 8.w;
|
|
|
+ const padding = 2;
|
|
|
+
|
|
|
+ final textPainter = TextPainter(
|
|
|
+ text: TextSpan(
|
|
|
+ text: '停留${TrackUtil.formatDurationFromMillis(data[i].duration)}',
|
|
|
+ style: TextStyle(color: '#666666'.color, fontSize: 12.sp),
|
|
|
+ ),
|
|
|
+ textDirection: TextDirection.ltr,
|
|
|
+ )..layout();
|
|
|
+
|
|
|
+ final totalHeight = max(rectSize, textPainter.height);
|
|
|
+ final topY = end.dy - totalHeight - padding;
|
|
|
+
|
|
|
+ final rectOffset = Offset(
|
|
|
+ isRight ? end.dx : end.dx - (rectSize + textPainter.width + 6),
|
|
|
+ topY + (totalHeight - rectSize) / 2,
|
|
|
+ );
|
|
|
+
|
|
|
+ final textOffset = Offset(
|
|
|
+ isRight ? end.dx + rectSize + 6 : end.dx - textPainter.width,
|
|
|
+ topY + (totalHeight - textPainter.height) / 2,
|
|
|
+ );
|
|
|
+
|
|
|
+ canvas.drawRRect(
|
|
|
+ RRect.fromRectAndRadius(
|
|
|
+ Rect.fromLTWH(rectOffset.dx, rectOffset.dy, rectSize, rectSize),
|
|
|
+ const Radius.circular(2),
|
|
|
+ ),
|
|
|
+ Paint()..color = data[i].color,
|
|
|
+ );
|
|
|
+
|
|
|
+ textPainter.paint(canvas, textOffset);
|
|
|
+ }
|
|
|
+
|
|
|
+ angle += sweepAngle;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
|
|
+}
|